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/proposal-amount.md.
  • 简体中文
  • Amount S2

    中文标题:量值

    提案概览
    提案速览

    Amount 提案引入一种不可变的值对象,把数值与精度及可选单位保存在一起,避免格式化和本地化 API 丢失测量上下文。它定义了规范化的标量值与序列单位值、基于 CLDR 的单位转换,以及字符串和本地化格式化,同时不涵盖算术运算与派生单位。

    Note

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

    表示量值

    阶段:2

    提案负责人

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

    TC39 讨论

    目标与需求

    在现实世界中,数字很少单独存在。数字往往用于度量_某种事物的量_:从碗中苹果的数量、银行账户中的欧元金额,到一杯水的毫升数,以及电动汽车每英里消耗的千瓦时数。度量物理量时,数字还带有_精度_,也就是有效数字的位数。

    Intl 格式化器长期以来一直能够格式化事物的数量,但与数字关联的数量并未随数字一起传递给 Intl API,这导致了现实中的错误。

    我们提议创建一种表示量值的新对象。它可以生成格式化的字符串表示,也可以在不同尺度之间转换量值。

    一个健壮的测量 API 可以解决的常见用户需求包括但不限于:

    • 跟踪测量值的精度。使用大量有效数字表示测量值,可能让人误以为测量精度超过了测量设备实际支持的程度。

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

    • 将测量值格式化为字符串表示的需求。

    • 将测量值从一种尺度转换为另一种尺度的需求。

    • 与上述两者相关,对测量值进行本地化的需求。

    描述

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

    属性

    Amount 将具有以下只读属性:

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

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

      unit 中至少包含一个 -and- 子串时,value 使用 Array。 这类值表示序列单位,例如“5 英尺 11 英寸”。 Array 的长度与 unit 中由 -and- 分隔的部分数量一致, 并且除最后一个 Array 元素外,其他元素始终表示整数。

    • unit: string | undefined —— 与 Amount 数值关联的度量单位。 undefined 表示“未提供单位”。

    构造器

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

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

        如果单位标识符包含一个或多个 -and- 子串,它就表示一个序列单位。 由 -and- 分隔的每个部分都必须非空,否则会抛出 RangeError。

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

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

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

      如果 value 不是 Number、BigInt、String, 也不是由 Number、BigInt 或 String 值组成的 Array,构造 Amount 时会抛出 TypeError。 使用 String value 或包含 String 值的 Array 构造 Amount 时, 字符串必须严格为表示有限值的数值字面量: 即 StrNumericLiteral, 但不能是 "Infinity""+Infinity""-Infinity"。 其他任何值都会抛出 RangeError,包括 "NaN"、空字符串, 以及带有前导或尾随空白的字符串。 String 值 Amount 的 value 属性始终规范化为上述十进制指数表示法。

      当且仅当 unit 选项已定义且至少包含一个 -and- 子串时, 才需要并且必须使用 Array value。否则使用 Array value 会抛出 TypeError。 这类值表示序列单位,例如“5 英尺 11 英寸”。 Array 长度必须与 unit 中由 -and- 分隔的部分数量一致, 并且除最后一个 Array 元素外,其他元素都必须表示整数; 否则会抛出 TypeError。

      Array 中除最后一个元素外的 String 值会规范化为普通整数表示法。 最后一个元素则使用与 String value 相同的指数表示法。 例如,Array value ['5.0', '11.0'] 会规范化为 ['5', '1.10e+1']

      如果设置了 fractionDigitssignificantDigits, 则 value 会相应地四舍五入, 并存储为 String(如果有限)或 Number(如果非有限)。 如果同时设置了 fractionDigitssignificantDigits,则抛出 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 或 String 数组或 undefined): 确定相应类别首选单位的区域设置。
      • usage (String):Amount 的用途,例如质量单位的 "person"
      • 与 Amount 构造器中含义相同的可选属性:
        • fractionDigits
        • significantDigits
        • roundingMode

      options 必须至少包含 unitlocaleusage 之一。 如果 options 包含显式的 unit 值,则不得包含 localeusage。 如果设置了 localeusage 未定义,则假定使用 "default" 用途。 如果设置了 usagelocale 未定义,则假定使用默认区域设置。

      如果单位转换的结果是有限的,并且设置了 fractionDigitssignificantDigits 选项, 则结果将根据精度选项进行舍入, 并且返回的 Amount 将具有字符串 value。 否则,返回的 Amount 将具有数值 value。 如果同时设置了 fractionDigitssignificantDigits,则抛出 RangeError。 如果转换目标是序列单位并设置了 fractionDigits, 舍入只应用于 Array 中的最后一个值。 如果转换目标是序列单位并设置了 significantDigits,则抛出 TypeError。

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

      如果转换目标是序列单位,并且各个单位无法按照量级递减的顺序, 使用整数倍数彼此转换,则抛出 TypeError。 换言之,转换为 foot-and-inch 有效, 但 inch-and-footfoot-and-centimeterfoot-and-gallon 都不是有效的转换目标。

    • toString():Amount 的字符串表示。 返回一个数字字符串连同单位,用方括号括起来(例如,"[1.23+e0 kilogram]")。 如果 Amount 没有单位, 则使用波浪号 ~ (U+007E) 代替单位(例如,"[4.2+e1 ~]")。 如果 Amount 使用序列单位,单位标识符会拆分为各个组成单位, 各组成单位及其值使用 ", " 连接。 序列单位中除最后一个值外,其他值必须始终为整数, 因此使用普通整数序列化 (例如,"[60 degree, 11 arc-minute, 3.141e+1 arc-second]")。

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

    单位转换

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

    对于每种单位类型,CLDR 中给出的数据定义了 一个乘法因子(以及温度单位的偏移量) 用于将源单位转换为此类型的基本单位。 例如,长度的基本单位是 meter,从 footmeter 的转换因子是 0.3048, 而从 inchmeter 的转换因子是 0.3048/12。

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

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

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

    例如,要将 1.75 英尺转换为英寸,内部执行以下数学运算:

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

    序列单位转换时, 各组成单位会分别转换,随后把得到的 Number 结果相加。

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

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

    转换中可能使用的 localeusage 值不会被保留, 但生成的 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"

    序列单位示例:

    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 后,API 更易于使用,开发者也更容易正确处理这些信息:

    // 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.PluralRulesIntl.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"

    默认情况下,我们使用四舍六入五成双舍入模式,该模式由 IEEE 754 标准使用,因此也由 Number 和 Decimal 使用。可以指定舍入模式:

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

    单位(包括货币)

    该提案的核心功能是支持单位(milekilogram 等)以及货币(EURUSD 等)。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() 将抛出错误。 由三个大写 ASCII 字母组成的单位标识符将使用 style: 'currency' 进行格式化, 而所有其他单位将使用 style: 'unit' 进行格式化。

    有关货币的注意事项和讨论,请参阅 issue 18

    相关但不在范围内功能

    Amount 旨在为 JavaScript 程序员提供一个小巧、可直接实现的功能内核,如果数据允许,后续提案可能会对此进行扩展。一些可能被认为属于 Amount 的功能是自然且可理解的,但目前不在范围内。以下是这些功能:

    数学运算

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

    • 将 Amount 提升到指数
    • 将 Amount 乘以/除以标量
    • 将两个相同维度的 Amount 相加/相减
    • 将一个 Amount 乘以/除以另一个 Amount
    • 在刻度之间转换(例如,从克转换为千克)

    可以想象,但本提案中不在范围内。 本提案专注于未来的提案可以构建的数字核心。

    派生单位

    某些单位可以派生出其他单位,例如平方米和立方码。目前,对此类单位的支持不在本提案范围内。

    常见问题

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

    此类型主要用于与现有原生语言特性(如 Intl)以及库之间的互操作。

    如果 Intl 是推动用例,为什么不将其称为 Intl.Amount?

    尽管 Intl 是部分提案负责人推动此提案的主要动机,但用例并不限于 Intl。Amount 是建立在数值类型之上的通用抽象,也提供序列化、库之间互操作等非 Intl 功能。Web 平台要实现理想的 i18n,需要 Amount 成为被广泛接受和使用的类型,而不能只面向已经使用 Intl 的开发者。这类似于 Temporal 类型:Temporal 越普及,Web 上的日期时间本地化就越完善。

    为什么使用内建对象而不是协议?

    一些尚未被非 Intl 用例说服的代表建议,Intl.NumberFormat.prototype.format 可以直接读取参数中的字段,效果如同传入真正的 Amount 对象。我们把这种方式称为基于“协议”的方案。

    内建对象有助于开发者发现并采用这项能力。如果它只是 Intl.NumberFormat 支持的一种协议,使用率很可能远低于真正提供 Amount 对象的情况。开发者可以直接找到并使用 Amount,也能从中受益。对于接受 Amount 参数的引擎 API,内建对象还允许实现快速路径。

    协议可能也应该同时存在,因为它支持 polyfill 和跨 Realm 边界的代码。

    为什么将精度表示为有效数字位数,而不是误差范围等其他内容?

    现有的 ECMA-262 和 ECMA-402 API 以有效数字位数处理精度:例如,Number.prototype.toPrecision 以及 Intl.NumberFormatIntl.PluralRules 中的 minimumSignificantDigits。我们不想在此领域创新。此外,CLDR 不以任何其他方式提供精度格式化的数据,我们也不知道有此类功能请求。

    相关/另请参阅

    • Amount 元素解释器 — Mozilla 关于 HTML <amount> 元素的提案/孵化
    • Smart Units(多次被提为此提案的自然后续提案)
    • Decimal 用于精确十进制算术
    • 保留尾随零 以确保当 Intl 处理数字字符串时,不会自动剥离尾随零(例如,静默将 "1.20" 规范化为 "1.2")。

    Polyfill

    一个 polyfill 可用于测试。由于此提案仍处于 阶段 2,预计会有破坏性更改;一般来说,它不适合生产使用。