For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/year/pending/proposal-amount.md.
  • 简体中文
  • Amount S2

    中文标题:Amount(量值)

    提案概览
    提案速览

    该提案引入 Amount 内置类型,用于表示数值以及可选的测量单位和精度信息。它支持从 Number、BigInt、String 或用于序列单位的 Array 构造 Amount,使用 Intl 格式化,并借助 CLDR 数据在兼容单位之间转换。该设计将数据模型、用户区域设置和开发者格式化选项分离,并涵盖货币值、测量精度、本地化以及复数形式选择等用例。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    表示量值

    阶段:2

    提案人:

    作者及前提案人:Ben Allen @ben-allen

    TC39 讨论:

    目标与需求

    在现实世界中,数字很少单独出现。数字更常用来度量 某个事物的量,从碗里苹果的数量,到你银行账户中的欧元金额,从一杯水中的毫升数,到电动汽车每英里消耗的 kWh 数。在测量物理量时,数字还具有 精度,即有效数字的位数。

    Intl 格式化器长期以来一直能够格式化事物的量,但与数字相关联的量并不会随数字一起传入 Intl API,这会导致现实世界中的 bug。

    我们提议创建一个新对象,用于表示量值、生成其格式化字符串表示,并在不同标度之间转换量值。

    一个稳健的测量 API 可以满足的常见用户需求包括但不限于:

    • 需要跟踪测得值的精度。用大量有效数字表示的测量值可能暗示测量本身比用于进行测量的仪器所能支持的更精确。

    • 需要表示货币值。用户常常希望将货币值与其计价货币一起跟踪。

    • 需要将测量值格式化为字符串表示

    • 需要将测量值从一种标度转换为另一种标度

    • 与上述两者相关,需要将测量值本地化。

    描述

    我们提议创建一个新的 Amount primordial,其中包含不可变的数值、精度和单位。

    属性

    Amount 将具有以下只读属性:

    • value: number | bigint | string | ReadonlyArray<number | bigint | string> —— Amount 的数值。 构造函数中使用的值的类型会被保留, 但任何可能受精度选项影响的值都是 String。 非有限值始终表示为 Number(Infinity、-Infinity 或 NaN)。

      String 类型的 value 始终采用 Number.p.toExponential 返回的格式 (十进制指数记法,指数带有显式符号, 有效数要么恰好为 0,要么其绝对值大于 0 且小于 10), 但不一定受相同边界的限制。

      当 unit 包含至少一个 -and- 子字符串时,会使用 Array 类型的 value。 这些表示 sequence units,例如“5 feet, 11 inches”。 Array 长度与 unit 中由 -and- 分隔的部分数量匹配, 并且除最后一个 Array 条目外,所有条目始终表示整数。

    构造函数

    • new Amount(value[, options])。使用 value 和可选的 options 构造一个 Amount,其中支持以下选项(全部可选):

      • unit(String):与数值关联的单位标识符。 单位标识符是由单个连字符(-)分隔的一个或多个非空段, 其中每个段由在 ECMAScript 标识符中有效的码点组成 (IdentifierPartChar), 这种语法受到 UTS #35 Unit Identifiers 的启发。 提供不是单位标识符的字符串(例如空字符串)会抛出 RangeError。 作为一种简写,options 可以直接作为 String 给出,这等价于传入 { unit: options }, 因此 new Amount(42, "meter") 与 new Amount(42, { unit: "meter" }) 相同。

        如果单位标识符包含一个或多个 -and- 子字符串,则它表示一个 [sequence unit], 并且由 -and- 分隔的每一段都必须非空,否则会抛出 RangeError。

      • fractionDigits:数学值应具有的小数位数(可以小于、等于或大于底层数学值在呈现为十进制数字字符串时实际具有的小数位数)

      • significantDigits:数学值应具有的有效数字位数(可以小于、等于或大于底层数学值在呈现为十进制数字字符串时实际具有的有效数字位数)

      • roundingMode:七种受支持的 Intl 舍入模式之一。当提供了 fractionDigits 和 significantDigits 选项,并且需要进行舍入以确保值确实具有指定的小数/有效数字位数时,会使用此选项。

      尝试从不是 Number、BigInt、String,或不是由 Number、BigInt、String 值组成的 Array 的 value 构造 Amount,会抛出 TypeError。 当从 String 类型的 value 或包含 String 值的 Array 构造 Amount 时, 字符串必须恰好是表示有限值的数字字面量: 一个 StrNumericLiteral, 但不能是 "Infinity"、"+Infinity" 或 "-Infinity"。 其他任何内容(包括 "NaN"、空字符串, 以及带有前导或尾随空白的字符串)都会抛出 RangeError。 String 值的 Amount 的 value 属性始终按上述方式规范化为十进制指数记法。

      当且仅当 unit 选项已定义且包含至少一个 -and- 子字符串时,才期望并要求 Array 类型的 value。 否则,使用 Array value 会抛出 TypeError。 这些表示 sequence units,例如“5 feet, 11 inches”。 Array 长度必须与 unit 中由 -and- 分隔的部分数量匹配, 并且除最后一个 Array 条目外,所有条目都必须表示整数; 否则会抛出 TypeError。

      Array 中的 String 值会被规范化:对于不是最后一个条目的值,使用普通整数表示; 对于最后一个条目,使用与 String value 相同的指数表示。 例如,Array value 如 ['5.0', '11.0'] 会被规范化为 ['5', '1.10e+1']。

      如果设置了 fractionDigits 或 significantDigits, 则 value 会相应地舍入, 并存储为 String(如果有限)或 Number(如果非有限)。 如果同时设置 fractionDigits 和 significantDigits,会抛出 RangeError。 如果 value 是 Array 且设置了 fractionDigits, 则舍入仅应用于 Array 中的最后一个值。 如果 value 是 Array 且设置了 significantDigits,会抛出 TypeError。

    对象原型将提供以下方法:

    • convertTo(options)。此方法返回一个处于 options 参数所指示标度的 Amount, 新 Amount 的值是调用该方法的 Amount 的值转换为新标度后的结果。 options 对象支持以下属性:

      • unit(String):显式的转换目标单位标识符。 作为一种简写,options 可以直接作为 String 给出,这等价于传入 { unit: options }, 因此 amount.convertTo("milliliter") 与 amount.convertTo({ unit: "milliliter" }) 相同。
      • locale(String、Strings 的 Array 或 undefined): 用于确定相应类别首选单位的区域设置。 单位偏好仅取决于区域设置的地区,确定方式如 ECMA-402 的 RegionPreference 所述:(1) 如果存在 rg Unicode 扩展,则使用它(对于 en-GB-u-rg-uszzzz 为 "US"),(2) 如果存在区域设置的地区子标签,则使用它(对于 de-CH 为 "CH"),如果已知该区域设置最可能的地区,则使用该地区(对于 pt 为 "BR"),最后以 "001"(世界)作为最终回退。
      • usage(String):Amount 的用例,例如质量单位使用 "person"。
      • 与 Amount 构造函数中含义相同的可选属性:
        • fractionDigits
        • significantDigits
        • roundingMode

      options 必须至少包含 unit、locale 或 usage 之一。 如果 options 包含显式的 unit 值,则不得包含 locale 或 usage。 如果设置了 locale 且 usage 为 undefined,则假定使用 "default" 用法。 如果设置了 usage 且 locale 为 undefined,则假定使用默认区域设置。

      如果单位转换的结果是有限的,并且设置了 fractionDigits 或 significantDigits 选项, 则结果会根据精度选项进行舍入, 返回的 Amount 将具有 String value。 否则,返回的 Amount 将具有 Number value。 如果同时设置 fractionDigits 和 significantDigits,会抛出 RangeError。 如果转换目标是 sequence unit 且设置了 fractionDigits, 则舍入仅应用于 Array 中的最后一个值。 如果转换目标是 sequence unit 且设置了 significantDigits,会抛出 TypeError。

      如果 Amount 的单位(例如货币单位)不支持转换, 或者解析出的转换目标对 Amount 的单位无效 (例如尝试将质量单位转换为长度单位), 调用 convertTo() 会抛出 TypeError。

      如果转换目标是 sequence unit, 且各个单位无法使用整数乘数按量级递减地相互转换, 则会抛出 TypeError。 换句话说,转换为 foot-and-inch 是有效的,但 inch-and-foot、foot-and-centimeter 和 foot-and-gallon 不是有效的转换目标。

    • toString():Amount 的字符串表示。 返回一个数字字符串以及单位,并用方括号包围(例如 "[1.23+e0 kilogram]")。 如果 Amount 没有单位, 则用波浪号 ~(U+007E)代替单位(例如 "[4.2+e1 ~]")。 如果 Amount 具有 sequence unit,则单位标识符会被拆分为其组成单位, 组成单位及其值用 ", " 连接。 对于不是最后一个的 sequence unit 值, 使用普通整数序列化,因为这些值必须始终为整数 (例如 "[60 degree, 11 arc-minute, 3.141e+1 arc-second]")。

    • toLocaleString(locale[, options]):返回适合该区域设置的格式化字符串表示 (例如在使用逗号作为小数分隔符的区域设置中为 "1,23 kg")。 这些选项是 Intl.NumberFormat 构造函数选项的子集。

    单位转换

    某些单位支持单位转换,其数据由 CLDR 在其文件 common/supplemental/units.xml 中提供。 该文件还提供按用法和按区域设置的单位偏好数据。

    对于每种单位类型,CLDR 中给出的数据定义了 从源单位转换到该单位类型的基本单位所需的乘法因子 (对于温度单位还有偏移量)。 例如,长度的基本单位是 meter,从 foot 到 meter 的转换因子为 0.3048, 而从 inch 到 meter 的转换因子为 0.3048/12。

    为避免舍入,转换按以下方式应用

    value × 𝔽(sourceFactor / targetFactor) + 𝔽((sourceOffset - targetOffset) / targetFactor)。

    其中因子项 sourceFactor / targetFactor 和偏移项 (sourceOffset − targetOffset) / targetFactor 按数学值计算,然后转换为 Number 值以进行乘法和加法。 对于非偏移转换(绝大多数情况),偏移项为 0 并且会跳过加法,以保留输入 value 为 -0𝔽。

    例如,要将 1.75 feet 转换为 inches,内部会执行以下数学运算:

    1.75 × 𝔽(0.3048 / (0.3048 / 12))
    = 1.75 × 𝔽(12)
    = 21

    当从 sequence unit 转换时, 单位的每个单独部分分别转换, 然后将 Number 结果相加。

    当转换到 sequence unit 时, 源单位值首先转换到目标 sequence unit 中量级最小的单位。 然后使用从较小量级单位到下一个量级单位的整数乘数, 对这个 Number 应用欧几里得除法。 余数分配给较小量级单位, 商分配给较大量级单位。 如果 sequence unit 包含两个以上单位,则重复此过程。

    根据转换方法的 options 中设置的精度选项(如果有), 仅对最终结果应用舍入。 源 Amount 的精度不会被保留, 结果的精度受 Number 精度上限的限制。

    转换中可能使用过的 locale 和 usage 值不会被保留, 但结果 Amount 当然会设置适当的 unit。

    例如:

    let feet = new Amount(1.75, "foot"); // shorthand for { unit: "foot" }
    feet.convertTo("inch"); // 21 inches; shorthand for { unit: "inch" }
    feet.convertTo({ locale: "fr", usage: "person", significantDigits: 3 }); // 53.3 cm

    示例

    首先,一个只有值的 Amount:

    let a = new Amount(123.456, { fractionDigits: 4 });
    a.value; // "1.234560e+2"
    typeof a.value; // "string"
    a.toString(); // "[1.234560e+2 ~]"
    a.toLocaleString("fr"); // "123,4560"

    以下是一个带单位的示例:

    let a = new Amount(42.7, { unit: "kilogram" });
    a.value; // 42.7
    typeof a.value; // "number"
    a.toString(); // "[4.27e+1 kilogram]"
    a.toLocaleString("fr"); // "42,7 kg"

    一个 sequence unit 示例:

    let a = new Amount([5, 11], "foot-and-inch");
    a.value; // [5, 11]
    typeof a.value; // "object"
    a.toString(); // "[5 foot, 1.1e+1 inch]"
    a.toLocaleString("fr"); // "5 pi et 11 po"

    使用 Intl 格式化

    Amount 通过正确分离数据模型、用户区域设置和开发者设置,显著改善了数字格式化的易用性,并鼓励更有利于 i18n 正确性的设计模式。

    如果没有 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 后,它更符合人体工程学,因此更容易做正确的事:

    // Data model: the thing being formatted
    let amt = new Amount("42.7", { unit: "kilogram", significantDigits: 4 });
    
    // User locale: how to localize
    let locale = "zh-CN";
    
    // Developer options: how much space is available, for example.
    let options = { unitDisplay: "long" };
    
    // Put it all together:
    let localizedString = amt.toLocaleString(locale, options);
    console.log(localizedString);  // "42.70千克"

    Amount 类型也可以插值到 MessageFormat 实现中,先从用户侧开始,未来可能进入标准库。

    选择复数形式

    i18n 中一个常见的坑是需要在 Intl.PluralRules 和 Intl.NumberFormat 上设置相同的精度。例如:

    // This code is buggy! Do you see why?
    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);
    // Note: This uses a potential toLocalePlural method.
    switch (stars.toLocalePlural(locale)) {
    case "one":
        console.log(`The rating is ${numberString} star`);
        break;
    default:
        console.log(`The rating is ${numberString} stars`);
        break;
    }

    舍入

    如果给定的精度小于输入值的精度,就会发生舍入。 (提高精度只会添加尾随零。)

    let a = new Amount("123.456", { significantDigits: 5 });
    a.value; // "1.2346e+2"

    默认情况下,我们使用 ties-to-even 舍入模式,IEEE 754 标准使用该模式,因此 Number 和 Decimal 也使用该模式。可以指定舍入模式:

    let b = new Amount("123.456", { significantDigits: 5, roundingMode: "trunc" });
    b.value; // "1.2345e+2"

    单位(包括货币)

    该提案的一个核心功能是支持单位(mile、kilogram 等)以及货币(EUR、USD 等)。Amount 不必具有单位/货币,如果有,则具有其中一种(而不是两者兼有)。示例:

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

    请注意,目前在 Amount 中,除了单位转换所支持的内容外,没有为其他单位指定含义。 你可以使用 "XYZ" 或 "keelogramz" 作为单位。 对具有 Intl.NumberFormat 不支持单位的 Amount 调用 toLocaleString() 会抛出 Error。 由三个大写 ASCII 字母组成的单位标识符将使用 style: 'currency' 格式化, 而所有其他单位将使用 style: 'unit' 格式化。

    关于货币的顾虑和讨论见 issue 18。

    相关但超出范围的功能

    Amount 旨在成为一个小型、可直接实现的功能内核,供 JavaScript 程序员使用;如果数据支持,或许可以在后续提案中扩展。有些功能可能被认为属于 Amount 很自然且可以理解,但目前超出范围。这些功能如下:

    数学运算

    下面列出了可以考虑支持的数学运算。然而,为了避免在算术运算中传播精度的含义产生混淆和歧义,我们不打算支持数学运算。一个自然的数据来源是 CLDR 数据,我们的单位名称和转换常数都来自 CLDR。可以设想如下运算:

    • 将 Amount 提高到某个指数
    • 将 Amount 乘以/除以一个标量
    • 将两个同量纲的 Amount 相加/相减
    • 将 Amount 乘以/除以另一个 Amount
    • 在不同标度之间转换(例如,从 grams 转换为 kilograms)

    这些都可以设想,但在本提案中超出范围。 本提案专注于未来提案可以构建于其上的数值核心。

    导出单位

    有些单位可以派生出其他单位,例如平方米和立方码(仅举几个例子!)。对此类单位的支持目前不在本提案范围内。

    常见问题

    为什么是语言特性而不是库?

    该类型存在的主要目的是与现有原生语言特性(如 Intl)以及库之间进行互操作。

    如果 Intl 是主要用例,为什么不叫 Intl.Amount?

    尽管 Intl 是促使一些提案人推进本提案的原因,但用例并不限于 Intl。Amount 类型是一个在数值类型之上普遍有用的抽象,并具有一些非 Intl 功能,例如序列化和库互操作。Web 平台上最佳的 i18n 取决于 Amount 成为被广泛接受和使用的类型,而不是只供已经使用 Intl 的开发者使用的东西。这与 Temporal 类型获得广泛采用从而改善 Web 上日期时间本地化的方式并无不同。

    为什么是 primordial 而不是协议?

    一些代表不认同非 Intl 用例,他们建议 Intl.NumberFormat.prototype.format 可以像传入真正的 Amount 对象一样从其参数读取字段,我们称之为基于“协议”的方法。

    primordial 有助于可发现性和采用。如果它只是 Intl.NumberFormat 支持的协议,那么它的使用量将显著低于 Amount 实际存在并为开发者所发现、使用和受益的情况。primordial 还允许接受它作为参数的引擎 API 使用快速路径。

    该协议很可能应该共存,因为它支持 polyfill 和跨 membrane 代码。

    为什么将精度表示为有效数字位数,而不是像误差范围这样的其他东西?

    现有的 ECMA-262 和 ECMA-402 API 都按有效数字来处理精度:例如 Number.prototype.toPrecision 以及 Intl.NumberFormat 和 Intl.PluralRules 中的 minimumSignificantDigits。我们不希望在这一领域进行创新。此外,CLDR 不提供以其他方式格式化精度的数据,我们也不知道有这样的功能请求。

    相关/另见

    • Amount element explainer —— Mozilla 关于 HTML <amount> 元素的提案/孵化
    • Smart Units(多次被提及为本提案的自然后续提案)
    • Decimal,用于精确十进制算术
    • Keep trailing zeroes,确保 Intl 处理数字字符串时不会自动去除尾随零(例如,静默地将 "1.20" 规范化为 "1.2")。

    Polyfill

    一个 polyfill 可用于测试。由于本提案仍处于 第 2 阶段,预计会有破坏性变更;总体而言,它不 适合生产使用。