16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】単位をJSで取り扱えるようにしようという提案

Posted at

たとえば1t + 2kg + 3g + 4mgみたいなことはJavaScriptではできません。
単位変換ができない以前に、単位という概念がありません。
例外はくらいで、それ以外の値については値しか扱うことができません。

ということでJavaScriptで単位を扱えるようにしようというproposalが提出されました。
2025年11月現在のstageは1で、実装されるにしてもまだまだ先になりそうですが、面白そうな提案だったので紹介してみます。

ということで以下は該当のproposal、Representing Measuresです。
プロジェクト名はamountなのにタイトルはMeasure、よくわからぬ。

Representing Measures

Goals and needs

現実世界では、数値が数値だけで存在している状況は稀です。
数値は、たいてい単位と共に使用されます。
ボウルの中のリンゴの数、コップ一杯の水の量、電気自動車の消費電力まで、あらゆるものが対象になります。
また、物理量の測定には精度、すなわち有効桁数も存在します。

Intlフォーマッタは数値をフォーマットすることはできますが、その単位は扱わないため、実世界に適用するとバグります。

我々は、単位を表す新しいオブジェクトを導入し、文字列表現を生成できるようにします。

一般的なユースケースとしては、以下のようなものが考えられます。

・計測値の精度を制御する。
・通貨の単位を扱う。ユーザは金額の数値だけではなく、その通貨単位を一緒に扱いたいでしょう。
・計測値を文字列にフォーマットして出力する。

Description

数値、精度、単位を含むプリミティブ型Amountを導入します。

Properties

以下のプロパティを持ちます。

・unit:string|undefined
単位。

・significantDigits:int
有効桁数。正の整数。

・fractionalDigits:int
小数点以下の桁数。非負整数。

Constructor

new Amount(value[, options])
コンストラクタは数値valueと、以下を含むオプション引数optionsを受け取ります。

・unit:string|undefined
単位。

・significantDigits:int
有効桁数。正の整数。

・fractionalDigits:int
小数点以下の桁数。非負整数。

・roundingMode
Intlが対応している、端数の丸めモード。

オブジェクトは、以下のメソッドを持ちます。

・toString([ options ])
文字列表現を返します。
デフォルトでは単位を角括弧で囲った1.23[kg]のような形式になります。

・toLocaleString(locale[, options])
ロケールを考慮した文字列表現を返します。
たとえば小数点がカンマになるロケールであれば1,23[kg]のようになります。

・with(options)
オプションを追加した新しい単位を作成します。

Examples

まずは単位のない数値の例です。

let a = new Amount("123.456");
a.fractionDigits;              // 3
a.significantDigits;           // 6
a.with({ fractionDigits: 4 }).toString(); // "123.4560"

有効桁数を増やすと、末尾に0が増えます。

単位付きの例です。

let a = new Amount("42.7", { unit: "kg" });
a.toString();                     // "42.7[kg]"
a.toString({ numberOnly: true }); // "42.7"

Formatting with Intl

Amountは、データ・ロケール・表示オプションを分離することで、国際フォーマットのデザインパターンを向上させます。

Amountを使わない場合、引数の目的が混在してしまいます。

let numberOfKilograms = 42.7;
let locale = "zh-CN";

let localizedString = new Intl.NumberFormat(locale, {
    minimumSignificantDigits: 4,
    style: "unit",
    unit: "kilogram",
    unitDisplay: "long",
})
.format(numberOfKilograms);
console.log(localizedString);  // "42.70千克"

Amountを使うと、目的が分離され、正しく操作しやすくなります。

// データモデル
let amt = new Amount("42.7", { unit: "kilogram", significantDigits: 4 });

// ロケール
let locale = "zh-CN";

// オプション
let options = { unitDisplay: "long" };

// まとめる
let localizedString = amt.toLocaleString(locale, options);
console.log(localizedString);  // "42.70千克"

Amountは、Intl.MessageFormatへの組み込みを、当初はユーザランドで、最終的に標準実装での組み込みを見据えています。

Selecting Plural Forms

i18nのよくある問題として、Intl.PluralRulesIntl.NumberFormatの両方に同じ精度を設定をしないといけないことが挙げられます。

// バグがあります。どうしてかわかりますか?
let locale = "en-US";
let numberOfStars = 1;
let numberString = new Intl.NumberFormat(locale, { minimumFractionDigits: 1 }).format(numberOfStars);

switch (new Intl.PluralRules(locale).select(numberOfStars)) {
case "one":
    console.log(`The rating is ${numberString} star`);
    break;
default:
    console.log(`The rating is ${numberString} stars`);
    break;
}

このコードはThe rating is 1.0 starになってしまいます。
シンプルなルールの英語ですら誤りであり、複雑な語形変化を持つ言語では、問題はさらに顕著になります。

Amountを使うと、コードは期待通りになり、流れも追いやすくなります。

let locale = "en-US";
let stars = new Amount(1, { fractionDigits: 1 });
let numberString = stars.toLocaleString(locale);

switch (stars.toLocalePlural(locale)) {
case "one":
    console.log(`The rating is ${numberString} star`);
    break;
default:
    console.log(`The rating is ${numberString} stars`);
    break;
}

Rounding

精度を下げると、値は四捨五入されます。

let a = new Amount("123.456");
a.with({ significantDigits: 5 }).toString(); // "123.46"

デフォルトでは、NumberやDecimalでも使用されているIEEE754で規定されたround-ties-to-evenが使用されます。
丸めモードを指定することも可能です。

let b = new Amount("123.456");
a.with({ significantDigits: 5, roundingMode: "truncate" }).toString(); // "123.45"

Units (including currency)

proposalの核となる機能のひとつは、単位と通貨のサポートです。
単位も通貨も必須ではありませんが、指定する場合は単位と通貨のいずれか片方だけを指定可能です。

let a = new Amount("123.456", { unit: "kg" }); // 123.456 kilograms
let b = new Amount("42.55", { unit: "EUR" });  // 42.55 Euros

単位の意味は規定されていないことに注意してください。
単位としてXYZkeelogramzなども使用可能です。
ただし、Intl.NumberFormatが対応していない単位でtoLocaleStringを呼びだすとエラーになります。

Related but out-of-scope features

スコープ外となった機能。
Amountは、小規模で容易に実現できる機能の核となることを目指しています。
以下の提案は、今後の拡張に含まれる可能性はありますが、現在はスコープ外です。

Mathematical operations

算術演算への対応。

Amount同士の四則演算、スカラー値との乗算・除算など。
gからkgなどへのスケール変換。

Unit conversion

マイルからメートルなどへの単位変換。

Derived units

メートルから平方メートルなどへの単位の派生。

Compound units

単位の組み合わせ。
身長を71inch5.92feetではなく5 feet 11 inchesと表す。

Polyfill

既にPolyfillが存在します。
本proposalはステージ1であるため、今後互換性のない変更が入る可能性があります。
本番環境での使用には適していません。

感想

mathjs使うわ。

mathjs
1t + 2kg + 3g + 4mg; // 1.002003004 t
16
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?