如何格式化 3-5 或 100-200 这样的区间

使用 JavaScript 以符合本地习惯的格式显示数字区间

引言

数字区间在用户界面中随处可见。例如,价格区间显示为 $100-$200,页码为 1-10,数量估算为 3-5 个。这些区间表达了一个值在两个端点之间,为用户提供了范围信息,而不是单一的精确数值。

如果你将区间分隔符硬编码,就默认所有用户都遵循相同的排版规范。英语用户通常用连字符或短横线表示区间,但其他语言可能使用不同的符号或词语。例如,德语在数字之间用“bis”,有些语言则在分隔符两侧加空格。

JavaScript 在 Intl.NumberFormat 上提供了 formatRange() 方法,可以自动处理区间格式化。该方法会根据不同地区的习惯格式化数字和分隔符,确保全球用户都能正确看到区间。

为什么数字区间需要本地化格式

不同文化有各自表达区间的规范,这些规范涉及分隔符的符号以及其前后的空格。

在美式英语中,区间通常用无空格的短横线表示:3-5,100-200。有些风格指南会在短横线两侧加空格:3 - 5。具体规范因场景和出版标准而异。

在德语中,区间常用“bis”作为分隔符:3 bis 5,100 bis 200。这种基于词语的方式让区间关系更加明确,而不是仅依赖标点符号。

在西班牙语中,区间既可以用短横线,也可以用“a”这个词:3-5 或 3 a 5。具体用法取决于西班牙语地区和语境。

在格式化包含货币或单位的区间时,复杂性会增加。例如,价格区间在美式英语中可能显示为 $100-$200,而在德语中则可能为 100 €-200 €,或者只在区间末尾显示一次货币符号,如 100-200 €。不同的本地化环境对货币符号的位置和重复方式有不同的处理方式。

手动格式化区间时,需要了解这些本地化习惯并实现对应的本地化逻辑。Intl API 封装了这些知识,能够根据不同的本地化环境自动应用合适的格式。

使用 formatRange 格式化数字区间

formatRange() 方法接受两个数字,并返回一个表示区间的格式化字符串。您可以使用所需的本地化环境和选项创建 Intl.NumberFormat 实例,然后用起始值和结束值调用 formatRange()

const formatter = new Intl.NumberFormat("en-US");

console.log(formatter.formatRange(3, 5));
// Output: "3–5"

console.log(formatter.formatRange(100, 200));
// Output: "100–200"

console.log(formatter.formatRange(1000, 5000));
// Output: "1,000–5,000"

格式化器会为区间内的两个数字都添加千位分隔符,并使用合适的分隔符连接它们。对于美式英语,区间分隔符是无空格的短横线(en dash)。

您可以通过更改本地化标识符,将同一个区间格式化为不同的本地化格式。

const usFormatter = new Intl.NumberFormat("en-US");
console.log(usFormatter.formatRange(100, 200));
// Output: "100–200"

const deFormatter = new Intl.NumberFormat("de-DE");
console.log(deFormatter.formatRange(100, 200));
// Output: "100–200"

const esFormatter = new Intl.NumberFormat("es-ES");
console.log(esFormatter.formatRange(100, 200));
// Output: "100-200"

每个本地化环境都有自己的区间分隔符和空格规范。API 会根据本地化的排版标准自动处理这些细节。

格式化货币区间

区间格式化支持所有数字格式化选项,包括货币。当您格式化货币区间时,格式化器会自动处理货币符号的位置和区间分隔符。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

console.log(formatter.formatRange(100, 200));
// Output: "$100 – $200"

console.log(formatter.formatRange(1000, 5000));
// Output: "$1,000 – $5,000"

格式化器会在区间内每个数字前都加上货币符号,以明确表示两个值都是货币金额。

不同的本地化环境对货币符号的位置有不同的处理方式。

const usFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

console.log(usFormatter.formatRange(100, 200));
// Output: "$100 – $200"

const deFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR",
  maximumFractionDigits: 0
});

console.log(deFormatter.formatRange(100, 200));
// Output: "100–200 €"

德语格式化器会将欧元符号放在区间末尾,而不是放在每个数字前。这符合德语在货币区间书写上的排版规范。

当区间值近似相等时会发生什么

当起始值和结束值在格式化后四舍五入为同一个数字时,格式化器会将区间合并,并可能添加近似符号。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

console.log(formatter.formatRange(100, 200));
// Output: "$100 – $200"

console.log(formatter.formatRange(100, 120));
// Output: "$100 – $120"

console.log(formatter.formatRange(100.2, 100.8));
// Output: "~$100"

第三个示例展示了两个值在四舍五入后为同一个整数。与其显示“$100 – $100”(没有区间信息),格式化器会输出“~$100”。波浪号表示该值为近似值。

当格式化选项导致起始值和结束值显示相同,格式化器就会采用这种处理方式。

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 1
});

console.log(formatter.formatRange(2.9, 3.1));
// Output: "~3"

console.log(formatter.formatRange(2.94, 2.96));
// Output: "~2.9"

格式化器只在必要时插入近似符号。如果两个值四舍五入后不同,则会以标准区间形式显示。

带小数位的区间格式化

区间格式化会保留格式化选项中的小数位设置。你可以分别控制区间两端的精度。

const formatter = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

console.log(formatter.formatRange(3.5, 5.7));
// Output: "3.50–5.70"

console.log(formatter.formatRange(100, 200));
// Output: "100.00–200.00"

格式化器会将小数位设置应用于区间内的两个数字,确保整个区间显示的精度一致。

你可以将小数格式与货币或其他样式结合使用。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

console.log(formatter.formatRange(99.99, 199.99));
// Output: "$99.99 – $199.99"

不同语言的数字区间格式化

区间格式化会根据每个语言环境的数字、分隔符和空格规范进行适配。

const enFormatter = new Intl.NumberFormat("en-US");
console.log(enFormatter.formatRange(1000, 5000));
// Output: "1,000–5,000"

const deFormatter = new Intl.NumberFormat("de-DE");
console.log(deFormatter.formatRange(1000, 5000));
// Output: "1.000–5.000"

const frFormatter = new Intl.NumberFormat("fr-FR");
console.log(frFormatter.formatRange(1000, 5000));
// Output: "1 000–5 000"

const jaFormatter = new Intl.NumberFormat("ja-JP");
console.log(jaFormatter.formatRange(1000, 5000));
// Output: "1,000~5,000"

英文用逗号作千位分隔符,区间用短横线(en dash)表示。德语用句点作千位分隔符,也用短横线。法语用空格作千位分隔符,也用短横线。日语用逗号作千位分隔符,区间用波浪线(~)表示。

这些差异还体现在货币格式化上。

const enFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});
console.log(enFormatter.formatRange(100, 200));
// Output: "$100.00 – $200.00"

const deFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});
console.log(deFormatter.formatRange(100, 200));
// Output: "100,00–200,00 €"

const jaFormatter = new Intl.NumberFormat("ja-JP", {
  style: "currency",
  currency: "JPY"
});
console.log(jaFormatter.formatRange(100, 200));
// Output: "¥100~¥200"

每个本地化区域都有自己的货币符号位置、小数点分隔符和区间分隔符规则。API 会自动处理所有这些差异。

结合 formatRange 与紧凑记数法

区间格式化支持紧凑记数法,可以显示如 1K-5K 或 1M-5M 这样的区间。

const formatter = new Intl.NumberFormat("en-US", {
  notation: "compact"
});

console.log(formatter.formatRange(1000, 5000));
// Output: "1K–5K"

console.log(formatter.formatRange(1000000, 5000000));
// Output: "1M–5M"

console.log(formatter.formatRange(1200, 4800));
// Output: "1.2K–4.8K"

格式化器会对区间的两个值都应用紧凑记数法。这样输出既简洁又能准确传达区间信息。

当区间跨越不同数量级时,格式化器会针对每个值做出相应处理。

const formatter = new Intl.NumberFormat("en-US", {
  notation: "compact"
});

console.log(formatter.formatRange(500, 1500));
// Output: "500–1.5K"

console.log(formatter.formatRange(900000, 1200000));
// Output: "900K–1.2M"

起始值可能不使用紧凑记数法,而结束值使用,或者两者采用不同的数量级标记。格式化器会根据每个值的大小自动决定。

使用 formatRangeToParts 进行自定义样式

formatRangeToParts() 方法会返回一个对象数组,表示格式化区间的各个部分。这样你可以对区间的不同组件进行样式化或操作。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

const parts = formatter.formatRangeToParts(100, 200);
console.log(parts);

输出是一个对象数组,每个对象包含 typevaluesource 属性。

[
  { type: "currency", value: "$", source: "startRange" },
  { type: "integer", value: "100", source: "startRange" },
  { type: "literal", value: " – ", source: "shared" },
  { type: "currency", value: "$", source: "endRange" },
  { type: "integer", value: "200", source: "endRange" }
]

type 属性用于标识该部分的类型:货币符号、整数、小数点分隔符或文本。value 属性包含格式化后的文本。source 属性指示该部分属于起始值、结束值还是两者共享。

你可以利用这些部分为不同组件生成自定义 HTML 并应用不同样式。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

const parts = formatter.formatRangeToParts(100, 200);
let html = "";

parts.forEach(part => {
  if (part.type === "currency") {
    html += `<span class="currency-symbol">${part.value}</span>`;
  } else if (part.type === "integer") {
    html += `<span class="amount">${part.value}</span>`;
  } else if (part.type === "literal") {
    html += `<span class="separator">${part.value}</span>`;
  } else {
    html += part.value;
  }
});

console.log(html);
// Output: <span class="currency-symbol">$</span><span class="amount">100</span><span class="separator"> – </span><span class="currency-symbol">$</span><span class="amount">200</span>

这种方法可以让你应用 CSS 类、添加提示信息或实现其他自定义行为,同时保证本地化格式的正确性。

使用 formatRange 处理边界情况

formatRange() 方法包含对无效输入的错误处理。如果任一参数为 undefined,则会抛出 TypeError。如果任一参数为 NaN 或无法转换为数字,则会抛出 RangeError

const formatter = new Intl.NumberFormat("en-US");

try {
  console.log(formatter.formatRange(100, undefined));
} catch (error) {
  console.log(error.name);
  // Output: "TypeError"
}

try {
  console.log(formatter.formatRange(NaN, 200));
} catch (error) {
  console.log(error.name);
  // Output: "RangeError"
}

在处理用户输入或外部数据时,应在将值传递给 formatRange() 之前,验证其是否为有效数字。

该方法接受数字、BigInt 值或可表示为有效数字的字符串。

const formatter = new Intl.NumberFormat("en-US");

console.log(formatter.formatRange(100, 200));
// Output: "100–200"

console.log(formatter.formatRange(100n, 200n));
// Output: "100–200"

console.log(formatter.formatRange("100", "200"));
// Output: "100–200"

字符串输入会被解析为数字,能够在不产生浮点精度问题的情况下保留精度。

何时使用 formatRange 与手动格式化

在向用户展示区间时,建议使用 formatRange()。这适用于价格区间、数量区间、测量区间、页码或其他有界值。该方法可确保正确的本地化格式,无需自行实现分隔符逻辑。

当需要展示多个彼此无区间语义关联的独立值时,应避免使用 formatRange()。例如,显示价格列表如“$100, $150, $200”时,应对每个值分别调用 format(),而不是将其视为区间。

当值之间的关系不是数值区间时,也应避免使用 formatRange()。如果需要展示对比或差异,应根据具体场景采用合适的格式化方式,而不是区间格式化。