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

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

はじめに

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);
// 出力: "$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()よりもわずかにオーバーヘッドが大きくなります。この違いは一般的なアプリケーションでは無視できますが、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>
  );
}

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