如何获取格式化数字的各个部分以进行自定义显示

将格式化数字分解为组件,以应用自定义样式并构建复杂界面

介绍

format() 方法返回一个完整的格式化字符串,例如 "$1,234.56" 或 "1.5M"。这种方法适用于简单的显示,但无法对单独的部分进行不同的样式处理。您无法将货币符号加粗、将小数部分设置为不同的颜色,或者对特定组件应用自定义标记。

JavaScript 提供了 formatToParts() 方法来解决这个问题。它不是返回一个单一的字符串,而是返回一个对象数组,每个对象代表格式化数字的一个部分。每个部分都有一个类型,例如 currencyintegerdecimal,以及一个值,例如 $1234.。然后,您可以处理这些部分以应用自定义样式、构建复杂布局或将格式化数字集成到丰富的用户界面中。

为什么格式化字符串难以自定义

当您收到一个格式化字符串,例如 "$1,234.56",您无法轻松地识别货币符号结束的位置以及数字开始的位置。不同的语言环境会将符号放在不同的位置。一些语言环境使用不同的分隔符。可靠地解析这些字符串需要复杂的逻辑,这些逻辑重复了 Intl API 中已经实现的格式化规则。

考虑一个仪表板,它以不同的颜色显示货币符号。如果使用 format(),您需要:

  1. 检测哪些字符是货币符号
  2. 考虑符号和数字之间的空格
  3. 处理不同语言环境中符号的位置
  4. 仔细解析字符串以避免破坏数字

这种方法既脆弱又容易出错。任何语言环境格式规则的更改都会破坏您的解析逻辑。

formatToParts() 方法通过单独提供组件解决了这个问题。您会收到结构化数据,明确告诉您每个部分的具体内容,无论语言环境如何。

使用 formatToParts 获取数字组件

formatToParts() 方法的工作方式与 format() 完全相同,除了它的返回值不同。您可以使用相同的选项创建格式化器,然后调用 formatToParts() 而不是 format()

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

const parts = formatter.formatToParts(1234.56);
console.log(parts);

这将输出一个对象数组:

[
  { type: "currency", value: "$" },
  { type: "integer", value: "1" },
  { type: "group", value: "," },
  { type: "integer", value: "234" },
  { type: "decimal", value: "." },
  { type: "fraction", value: "56" }
]

每个对象包含一个 type 属性,用于标识该部分的含义,以及一个 value 属性,包含实际的字符串。各部分的顺序与格式化输出中的顺序相同。

您可以通过将所有值连接在一起来验证这一点:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// 输出: "$1,234.56"

连接的部分生成的输出与调用 format() 的结果完全相同。

理解部分类型

type 属性标识每个组件。不同的格式选项会生成不同的部分类型。

对于基本数字格式:

const formatter = new Intl.NumberFormat("en-US");
const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

integer 类型表示整数部分。当分组分隔符将数字分割时,会出现多个 integer 部分。group 类型表示千位分隔符。decimal 类型表示小数点。fraction 类型表示小数点后的数字。

对于货币格式:

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

const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "currency", value: "€" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

currency 类型根据区域设置的习惯出现在数字之前或之后。

对于百分比:

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

const parts = formatter.formatToParts(0.1234);
console.log(parts);
// [
//   { type: "integer", value: "12" },
//   { type: "percentSign", value: "%" }
// ]

percentSign 类型表示百分号。

对于紧凑表示法:

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

const parts = formatter.formatToParts(1500000);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "5" },
//   { type: "compact", value: "M" }
// ]

compact 类型表示大小指示符,例如 K、M 或 B。

为数字部分应用自定义样式

formatToParts() 的主要用例是为不同的组件应用不同的样式。您可以处理 parts 数组,将特定类型包装在 HTML 元素中。

将货币符号加粗:

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

const parts = formatter.formatToParts(1234.56);
const html = parts
  .map(part => {
    if (part.type === "currency") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// 输出: "<strong>$</strong>1,234.56"

这种方法适用于任何标记语言。通过处理 parts 数组,您可以生成 HTML、JSX 或任何其他格式。

为小数部分应用不同样式:

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

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// 输出: "1,234<span class="text-gray-500">.50</span>"

这种模式在价格显示中很常见,其中小数部分通常显示得更小或更浅。

为负数添加颜色编码

金融应用程序通常以红色显示负数。通过 formatToParts(),您可以检测到负号并相应地应用样式。

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

function formatWithColor(number) {
  const parts = formatter.formatToParts(number);
  const hasMinusSign = parts.some(part => part.type === "minusSign");

  const html = parts
    .map(part => part.value)
    .join("");

  if (hasMinusSign) {
    return `<span class="text-red-600">${html}</span>`;
  }

  return html;
}

console.log(formatWithColor(-1234.56));
// 输出: "<span class="text-red-600">-$1,234.56</span>"

console.log(formatWithColor(1234.56));
// 输出: "$1,234.56"

这种方法可以可靠地检测所有语言环境中的负数,即使这些语言环境使用不同的符号或负号位置。

使用多种样式构建自定义数字显示

复杂的界面通常结合多种样式规则。您可以同时将不同的类或元素应用于不同的部分类型。

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

function formatCurrency(number) {
  const parts = formatter.formatToParts(number);

  return parts
    .map(part => {
      switch (part.type) {
        case "currency":
          return `<span class="currency-symbol">${part.value}</span>`;
        case "integer":
          return `<span class="integer">${part.value}</span>`;
        case "group":
          return `<span class="group">${part.value}</span>`;
        case "decimal":
          return `<span class="decimal">${part.value}</span>`;
        case "fraction":
          return `<span class="fraction">${part.value}</span>`;
        case "minusSign":
          return `<span class="minus">${part.value}</span>`;
        default:
          return part.value;
      }
    })
    .join("");
}

console.log(formatCurrency(1234.56));
// 输出: "<span class="currency-symbol">$</span><span class="integer">1</span><span class="group">,</span><span class="integer">234</span><span class="decimal">.</span><span class="fraction">56</span>"

这种细粒度的控制可以为每个组件提供精确的样式。然后,您可以使用 CSS 为每个类设置不同的样式。

所有可用的部分类型

type 属性根据使用的格式选项可以具有以下值:

  • integer:整数部分的数字
  • fraction:小数部分的数字
  • decimal:小数点分隔符
  • group:千位分隔符
  • currency:货币符号
  • literal:格式化时添加的空格或其他文字
  • percentSign:百分号
  • minusSign:负数符号
  • plusSign:正数符号(当 signDisplay 设置时)
  • unit:单位格式的单位字符串
  • compact:紧凑表示法中的数量指示符(K、M、B)
  • exponentInteger:科学计数法中的指数值
  • exponentMinusSign:指数中的负号
  • exponentSeparator:将尾数与指数分隔的符号
  • infinity:无穷大的表示
  • nan:非数字的表示
  • unknown:未识别的标记

并非每种格式选项都会生成所有部分类型。您收到的部分取决于数字值和格式化器的配置。

科学计数法会生成与指数相关的部分:

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

const parts = formatter.formatToParts(1234);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "234" },
//   { type: "exponentSeparator", value: "E" },
//   { type: "exponentInteger", value: "3" }
// ]

特殊值会生成特定的部分类型:

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

console.log(formatter.formatToParts(Infinity));
// [{ type: "infinity", value: "∞" }]

console.log(formatter.formatToParts(NaN));
// [{ type: "nan", value: "NaN" }]

创建无障碍的数字显示

您可以使用 formatToParts() 为格式化的数字添加无障碍属性。这有助于屏幕阅读器正确朗读数值。

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

function formatAccessibleCurrency(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} US dollars">${formatted}</span>`;
}

console.log(formatAccessibleCurrency(1234.56));
// 输出: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

这确保了屏幕阅读器能够在提供适当上下文的情况下同时朗读格式化的显示值和底层的数值。

突出显示特定的数字范围

某些应用程序会突出显示落在特定范围内的数字。通过 formatToParts(),您可以根据数值应用样式,同时保持正确的格式。

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

function formatWithThreshold(number, threshold) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  if (number >= threshold) {
    return `<span class="text-green-600 font-bold">${formatted}</span>`;
  }

  return formatted;
}

console.log(formatWithThreshold(1500, 1000));
// 输出: "<span class="text-green-600 font-bold">1,500</span>"

console.log(formatWithThreshold(500, 1000));
// 输出: "500"

数字会根据区域设置正确格式化,同时根据业务逻辑应用条件样式。

何时使用 formatToParts 与 format

当您需要一个简单的格式化字符串且无需任何自定义时,请使用 format()。这是大多数数字显示的常见情况。

当您需要以下功能时,请使用 formatToParts()

  • 对数字的不同部分应用不同的样式
  • 使用格式化的数字构建 HTML 或 JSX
  • 为特定组件添加属性或元数据
  • 将格式化的数字集成到复杂布局中
  • 以编程方式处理格式化的输出

formatToParts() 方法的开销略高于 format(),因为它会创建一个对象数组而不是单个字符串。这种差异对于典型应用程序来说可以忽略不计,但如果您每秒格式化数千个数字,format() 的性能会更好。

对于大多数应用程序,根据您的样式需求进行选择,而不是性能问题。如果您不需要自定义输出,请使用 format()。如果您需要自定义样式或标记,请使用 formatToParts()

如何保留特定语言环境的格式

parts 数组会自动维护特定语言环境的格式规则。不同的语言环境会将符号放置在不同的位置,并使用不同的分隔符,但 formatToParts() 会处理这些差异。

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

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

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

console.log(eurFormatter.formatToParts(1234.56));
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "." },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "56" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" }
// ]

德语格式会将货币符号放在数字后面并加一个空格。分组分隔符是句号,小数分隔符是逗号。无论语言环境如何,您的样式代码都会以相同的方式处理 parts 数组,而格式会自动适应。

literal 类型表示格式化程序插入的任何不属于其他类别的空格或文本。在德语货币格式中,它表示数字和货币符号之间的空格。

将 formatToParts 与框架组件结合使用

现代框架(如 React)可以使用 formatToParts() 来高效构建组件。

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

此组件为不同的部分应用不同的样式,同时保持任何语言环境和货币的正确格式。