書式付き出力をパーツごとに分割してスタイリングする方法

formatToParts() を使って書式付き出力の各パーツにアクセスし、カスタムスタイリングを適用する

はじめに

JavaScript の format() メソッドは、"$1,234.56" や "2025年1月15日" のような完全な文字列を返します。シンプルな表示ではこれで十分ですが、各パーツごとに異なるスタイリングを施すことはできません。たとえば、通貨記号だけ太字にしたり、月名だけ色を変えたり、特定の要素に独自のマークアップを適用したりすることはできません。

この課題を解決するために、JavaScript には formatToParts() メソッドがあります。これは単一の文字列ではなく、書式付き出力の各パーツをオブジェクトとして含む配列を返します。各パーツは currencymonthelement などのタイプと、実際の文字列が入った値を持ちます。これらのパーツごとにカスタムスタイリングを適用したり、複雑なレイアウトを組んだり、リッチなユーザーインターフェースへ柔軟に組み込むことができます。

formatToParts() メソッドは、NumberFormatDateTimeFormatListFormatRelativeTimeFormatDurationFormat など、複数の Intl フォーマッターで利用できます。これにより、JavaScript のあらゆる国際化書式処理で一貫した方式として利用できます。

なぜ書式付き文字列は簡単にスタイル変更できないのか

"$1,234.56" のような書式付き文字列を受け取っても、どこまでが通貨記号で、どこからが数字なのか簡単には判別できません。ロケールによって記号の位置も違えば、区切り文字も異なります。こうした文字列を正確に解析するのは、既に Intl API が持つ書式ルールを自ら複製する複雑なロジックが必要になります。

たとえば、ダッシュボードで通貨記号だけ色分けした金額を表示したいとします。format() だけでは、以下の対応が必要です:

  1. どの文字が通貨記号かを判定する
  2. 記号と数字の間のスペースも考慮する
  3. ロケールごとの記号の位置の違いに対応する
  4. 数字を壊さないよう文字列を慎重に解析する

この方法は壊れやすく、エラーが発生しやすいです。ロケールの書式ルールが変更されると、解析ロジックが機能しなくなります。

同じ問題は、日付やリストなど、他の書式付き出力でも発生します。ロケール固有の書式ルールを再実装しない限り、フォーマット済み文字列から各要素を確実に判別することはできません。

formatToParts() メソッドを使えば、この問題は解消されます。各要素が個別に提供され、どの部分がどれに該当するか、ロケールに関係なく正確に把握できる構造化データを受け取れます。

formatToPartsの仕組み

formatToParts() メソッドは、返り値を除けば format() とまったく同じ方法で動作します。同じオプションでフォーマッターを作成し、format() の代わりに formatToParts() を呼び出します。

このメソッドはオブジェクトの配列を返します。それぞれのオブジェクトは2つのプロパティを持ちます:

  • type: その部分が何を表すか(例:currencymonthliteral など)
  • value: その部分の実際の文字列データ

各パートは、書式付き出力の並び順と同じ順序で並びます。すべての値を結合してみると、format() を呼び出した時とまったく同じ出力になることがわかります。

この仕組みは、formatToParts() をサポートするすべてのフォーマッターで共通です。パートの種類はフォーマッターごとに異なりますが、構造は常に一定です。

書式付きの数値をパートに分割

NumberFormat フォーマッターは、数値をパートごとに分解するために 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" }
]

各オブジェクトは、そのパートが何を表しているかと、その値を示します。currency タイプは通貨記号、integer タイプは整数部の数字、group タイプは千区切り記号、decimal タイプは小数点、fraction タイプは小数点以下の数字を表します。

パーツが書式化された出力と一致するか確認できます:

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

結合したパーツを使うと、format() を呼び出した場合とまったく同じ出力になります。

書式付き数字の通貨記号のスタイル設定

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);
// Output: "<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);
// Output: "1,234<span class="text-gray-500">.50</span>"

このパターンは、価格表示で小数部分を小さくしたり薄く表示したりする際によく使われます。

書式化された日付のパーツ分割

DateTimeFormat フォーマッターは書式化された日付や時刻を分解できる formatToParts() を提供します。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
console.log(parts);

この操作はオブジェクトの配列を出力します:

[
  { type: "month", value: "January" },
  { type: "literal", value: " " },
  { type: "day", value: "15" },
  { type: "literal", value: ", " },
  { type: "year", value: "2025" }
]

month タイプは月の名前や番号を表します。day タイプは月の日付、year は年を、literal はスペースや句読点、またはフォーマッターが挿入するその他のテキストを示します。

書式付き日付の月名のスタイル設定

数字と同様のパターンで日付の各要素にカスタムスタイルを適用できます。

月名を太字にする例:

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
const html = parts
  .map(part => {
    if (part.type === "month") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>January</strong> 15, 2025"

日付の複数要素をスタイル設定する例:

const formatter = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);

const html = parts
  .map(part => {
    switch (part.type) {
      case "weekday":
        return `<span class="font-bold">${part.value}</span>`;
      case "month":
        return `<span class="text-blue-600">${part.value}</span>`;
      case "year":
        return `<span class="text-gray-500">${part.value}</span>`;
      default:
        return part.value;
    }
  })
  .join("");

console.log(html);
// Output: "<span class="font-bold">Wednesday</span>, <span class="text-blue-600">January</span> 15, <span class="text-gray-500">2025</span>"

この細かいコントロールにより、各要素ごとに正確なスタイリングが可能になります。

書式付きリストのパーツ分割

ListFormat フォーマッターは、フォーマット済みリストを分解するための formatToParts() を提供します。

const formatter = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction"
});

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
console.log(parts);

これによりオブジェクトの配列が出力されます:

[
  { type: "element", value: "apples" },
  { type: "literal", value: ", " },
  { type: "element", value: "oranges" },
  { type: "literal", value: ", and " },
  { type: "element", value: "bananas" }
]

element 型はリスト内の各アイテムを表します。literal 型は、フォーマッターによって追加された区切り文字や接続詞を表します。

リストアイテムごとに個別でスタイリングする

同じパターンでリスト要素にカスタムスタイリングを適用できます。

リストアイテムを太字にする例:

const formatter = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction"
});

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
const html = parts
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>apples</strong>, <strong>oranges</strong>, and <strong>bananas</strong>"

特定のリストアイテムをスタイリングする:

const formatter = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction"
});

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);

let itemIndex = 0;
const html = parts
  .map(part => {
    if (part.type === "element") {
      const currentIndex = itemIndex++;
      if (currentIndex === 0) {
        return `<span class="text-green-600">${part.value}</span>`;
      }
      return part.value;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<span class="text-green-600">apples</span>, oranges, and bananas"

この方法なら、適切なロケールごとのフォーマットを維持しつつ、特定のアイテムを強調できます。

フォーマット済み相対時間をパーツで分割する

RelativeTimeFormat フォーマッターは、相対時間表現を分解するための formatToParts() を提供します。

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "auto"
});

const parts = formatter.formatToParts(-1, "day");
console.log(parts);

これによりオブジェクトの配列が出力されます:

[
  { type: "literal", value: "yesterday" }
]

数値のみの相対時間の場合:

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "always"
});

const parts = formatter.formatToParts(-3, "day");
console.log(parts);
// [
//   { type: "integer", value: "3" },
//   { type: "literal", value: " days ago" }
// ]

integer 型は数値を表します。literal 型は相対的な時間の単位と方向を表します。

フォーマット済み期間をパーツで分割する

DurationFormat フォーマッターは、フォーマット済み期間を分解するための formatToParts() を提供します。

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

const parts = formatter.formatToParts({
  hours: 2,
  minutes: 30,
  seconds: 15
});
console.log(parts);

これにより、次のようなオブジェクト配列が出力されます:

[
  { type: "integer", value: "2" },
  { type: "literal", value: " hours, " },
  { type: "integer", value: "30" },
  { type: "literal", value: " minutes, " },
  { type: "integer", value: "15" },
  { type: "literal", value: " seconds" }
]

integer 型は数値を表します。literal 型は単位名や区切り文字を表します。

フォーマット済みパーツからHTMLを生成する

パーツを処理し、スタイリングルールを一貫して適用する再利用可能な関数を作成できます。

function formatWithStyles(parts, styleMap) {
  return parts
    .map(part => {
      const style = styleMap[part.type];
      if (style) {
        return `<span class="${style}">${part.value}</span>`;
      }
      return part.value;
    })
    .join("");
}

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

const parts = numberFormatter.formatToParts(1234.56);
const html = formatWithStyles(parts, {
  currency: "font-bold text-gray-700",
  integer: "text-2xl",
  fraction: "text-sm text-gray-500"
});

console.log(html);
// Output: "<span class="font-bold text-gray-700">$</span><span class="text-2xl">1</span>,<span class="text-2xl">234</span>.<span class="text-sm text-gray-500">56</span>"

このパターンでは、スタイリングのルールとフォーマットのロジックを分離するため、保守性や再利用性が向上します。

ロケールごとのパーツ配置を理解する

パーツ配列は、ロケールに合わせたフォーマットルールを自動で適用します。ロケールごとに要素の並びや形式が異なりますが、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: "€" }
// ]

ドイツのフォーマットでは、通貨が数字の後ろにスペース付きで配置されます。グループ区切りはピリオド、少数区切りはカンマです。スタイリングのコードはロケールに関係なくパーツ配列を同じように処理し、フォーマットの違いには自動で対応します。

アクセシブルなフォーマット表示の作成

formatToParts() を使うことで、フォーマット済み出力にアクセシビリティ属性を追加できます。これにより、スクリーンリーダーが値を正しく読み上げられるようになります。

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

function formatAccessible(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(formatAccessible(1234.56));
// Output: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

これにより、スクリーンリーダーがフォーマット表示値だけでなく、基になる数値も適切な文脈で読み上げられるようになります。

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

このコンポーネントは、各パーツごとに異なるスタイリングを適用し、どのロケールや通貨にも対応した正しいフォーマットを維持します。

formatToParts の活用シーン

カスタマイズ不要でシンプルなフォーマット済み文字列が必要な場合は、format() を使いましょう。ほとんどの表示ケースではこれで十分です。

次のような場合は formatToParts() を使いましょう:

  • フォーマット済み出力内の各パーツに異なるスタイリングを適用したい場合
  • フォーマットした内容でHTMLやJSXを生成したい場合
  • 特定の要素に属性やメタデータを追加したい場合
  • 複雑なレイアウトにフォーマット結果を組み込みたい場合
  • フォーマット済み出力をプログラム的に処理したい場合
  • より細かなカスタムデザインやコントロールが必要な場合

formatToParts() メソッドは、format() よりもわずかにオーバーヘッドが大きくなります。これは、1つの文字列ではなくオブジェクトの配列を生成するためです。通常のアプリケーションではこの差はほとんど無視できますが、1秒あたり何千もの値をフォーマットする場合は、format() の方が高パフォーマンスです。

多くの場合、パフォーマンスよりもスタイリングの必要性に応じて使い分けてください。出力のカスタマイズが不要であれば format() を使用してください。カスタムな装飾やマークアップが必要なときは formatToParts() を選びましょう。

フォーマッタ間で共通するパートタイプ

異なるフォーマッタはそれぞれ異なるパートタイプを生成しますが、いくつかのタイプは複数のフォーマッタで共通して現れます:

  • literal: 書式設定で追加されたスペース、句読点、その他のテキスト。日付、数値、リスト、期間などに現れます。
  • integer: 整数部。数値、相対時刻、期間などに現れます。
  • decimal: 小数点の区切り。数値に現れます。
  • fraction: 小数部の桁。数値に現れます。

フォーマッタ特有のタイプには以下のものがあります:

  • 数値: currency, group, percentSign, minusSign, plusSign, unit, compact, exponentInteger
  • 日付: weekday, era, year, month, day, hour, minute, second, dayPeriod, timeZoneName
  • リスト: element
  • 相対時刻: 数値は integer、テキストは literal のように現れます。

これらのタイプを理解しておくことで、どんなフォーマッタでも出力を正しく処理できるスタイリングコードを書くことができます。