カスタム表示のためにフォーマットされた数値の個別部分を取得する方法

フォーマットされた数値をコンポーネントに分解してカスタムスタイリングを適用し、複雑なインターフェースを構築する

はじめに

format()メソッドは、「$1,234.56」や「1.5M」のような完全にフォーマットされた文字列を返します。これはシンプルな表示には適していますが、個別の部分に異なるスタイルを適用することはできません。通貨記号を太字にしたり、小数部分に異なる色を付けたり、特定のコンポーネントにカスタムマークアップを適用したりすることはできません。

JavaScriptは、この問題を解決するためにformatToParts()メソッドを提供しています。単一の文字列を返す代わりに、フォーマットされた数値の各部分を表すオブジェクトの配列を返します。各部分には、currencyintegerdecimalなどのタイプと、$1234.などの値があります。これらの部分を処理して、カスタムスタイリングを適用したり、複雑なレイアウトを構築したり、フォーマットされた数値をリッチなユーザーインターフェースに統合したりできます。

フォーマットされた文字列のカスタマイズが困難な理由

「$1,234.56」のようなフォーマットされた文字列を受け取った場合、通貨記号がどこで終わり、数値がどこから始まるかを簡単に識別することはできません。ロケールによって記号の位置が異なります。ロケールによって異なる区切り文字が使用されます。これらの文字列を確実に解析するには、Intl APIにすでに実装されているフォーマットルールを複製する複雑なロジックが必要です。

通貨記号を異なる色で表示する金額を表示するダッシュボードを考えてみましょう。format()を使用する場合、次のことが必要になります。

  1. どの文字が通貨記号かを検出する
  2. 記号と数値の間のスペースを考慮する
  3. ロケール間で異なる記号の位置を処理する
  4. 数値を壊さないように文字列を慎重に解析する

このアプローチは脆弱でエラーが発生しやすいです。ロケールのフォーマットルールが変更されると、パース処理が壊れてしまいます。

formatToParts()メソッドは、コンポーネントを個別に提供することでこの問題を解消します。ロケールに関係なく、どの部分が何であるかを正確に示す構造化されたデータを受け取ることができます。

formatToPartsを使用して数値コンポーネントを取得する

formatToParts()メソッドは、戻り値を除いてformat()と同じように動作します。同じオプションでフォーマッターを作成し、format()の代わりにformatToParts()を呼び出します。

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);
// Output: "$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()の主な使用例は、異なるコンポーネントに異なるスタイルを適用することです。パーツ配列を処理して、特定のタイプを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);
// Output: "<strong>$</strong>1,234.56"

このアプローチはあらゆるマークアップ言語で機能します。パーツ配列を処理することで、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);
// Output: "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));
// Output: "<span class="text-red-600">-$1,234.56</span>"

console.log(formatWithColor(1234.56));
// Output: "$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));
// Output: "<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));
// Output: "<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));
// Output: "<span class="text-green-600 font-bold">1,500</span>"

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

数値はロケールに適した書式設定を受け取り、ビジネスロジックに基づいて条件付きスタイルが適用されます。

formatToPartsとformatの使い分け

format()は、カスタマイズなしでシンプルな書式設定された文字列が必要な場合に使用します。これはほとんどの数値表示における一般的なケースです。

formatToParts()は、次のような場合に使用します。

  • 数値の異なる部分に異なるスタイルを適用する
  • 書式設定された数値を使用してHTMLまたはJSXを構築する
  • 特定のコンポーネントに属性またはメタデータを追加する
  • 書式設定された数値を複雑なレイアウトに統合する
  • 書式設定された出力をプログラムで処理する

formatToParts()メソッドは、単一の文字列ではなくオブジェクトの配列を作成するため、format()よりもわずかにオーバーヘッドがあります。この違いは一般的なアプリケーションでは無視できますが、1秒間に数千の数値を書式設定する場合は、format()の方がパフォーマンスが優れています。

ほとんどのアプリケーションでは、パフォーマンスの懸念よりもスタイリングのニーズに基づいて選択してください。出力をカスタマイズする必要がない場合は、format()を使用してください。カスタムスタイリングやマークアップが必要な場合は、formatToParts()を使用してください。

パーツがロケール固有のフォーマットを保持する方法

パーツ配列は、ロケール固有のフォーマットルールを自動的に維持します。ロケールによって記号の位置や区切り文字が異なりますが、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: "€" }
// ]

ドイツ語のフォーマットでは、通貨記号が数値の後にスペースを挟んで配置されます。桁区切り記号はピリオドで、小数点記号はカンマです。スタイリングコードは、ロケールに関係なく同じ方法でパーツ配列を処理し、フォーマットは自動的に適応します。

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>
  );
}

このコンポーネントは、任意のロケールと通貨に対して適切なフォーマットを維持しながら、異なるパーツに異なるスタイリングを適用します。