Intl.NumberFormatでゼロ埋めや%表記などの数値表現を楽に実装する

 · 9 min read

Number#toFixed や Math.round/floor/ceil を駆使して表示用の値を整形することってないでしょうか。カンマ区切りをオレオレユーティリティ関数で実装したりそういったことを実現するライブラリを探したことはないでしょうか。

ほとんどの JavaScript の実行環境にはIntlという i18n のためのオブジェクトが組み込まれており、その中の1つにIntl.NumberFormatというクラスがあります。 オプションが色々あり、かなり便利で多機能なんですが整理された情報が少なく、身の回りで使用してる人も少ないと感じました。

(2019/03/04あたりに結果表示されると思います。途中経過を見る場合元ツイートに飛んでください。)

「それ自前で実装しなくても NumberFormat 使えば一発だよ」とレビューし続ける bot は人間がやるべきではないので、Intl.NumberFormat を布教するための記事を書きます。
私的ユースケースごとにまとめて紹介します。

動作環境

さほど新しい機能でもないので、IE10 以外のブラウザ、0.12 以上の Node.js、多くのスマートフォンで Intl.NumberFormat が利用できます。
「多くの」と表記しているように使えない環境ももちろんあります。

また、Node.js では英語以外の言語を使う場合はビルド時のオプションが影響します。公式のガイドを確認してください。
full-icu でビルドされてない Node.js で英語以外にも対応するにはfull-icuモジュールを install しnode --icu-data-dir=./node_modules/full-icuオプションを指定するか、環境変数NODE_ICU_DATAに同様の値を指定すれば OK です。わざわざ再ビルドする必要はありません。

React Native に関しては古い Android/iOS で使えないという Issueがあるようです。どのバージョンからなら対応しているかというきちんとしたソースは見つけられませんでした。
代わりに簡易的な動作確認ができる Expo snack プロジェクトを作っておいたので、気になる方は下記リンクから動作確認してください。

Intl.NumberFormat playground | Snack

ただ、仮に動かない環境があったとしても polyfill という選択肢もあります。

polyfill

サポートしてない環境でも利用したい場合はandyearnshaw/Intl.jsを使うか、polyfill.ioから polyfill を利用できます。

以上で Intl.NumberFormat を利用する前提条件は整ったので、ユースケース別の紹介に移ります。

ユースケース:ゼロ埋め

ゼロ埋めは、よくオレオレしがちな処理の代表格だと思います。
String(num).padStart(2, '0')とかconst zeroPad = (n, digits) => ('0'.repeat(digits) + n).slice(-digits)みたいな。
NumberFormat で一撃です。minimumIntegerDigitsで整数部の最小桁数を指定できます。桁数が足りない場合はゼロ埋めされます。

const zeroPad3Digits = new Intl.NumberFormat('ja', { minimumIntegerDigits: 3 })

zeroPad3Digits.format(1) // => '001'
zeroPad3Digits.format(54) // => '054'
zeroPad3Digits.format(100) // => '100'

ユースケース:カンマ区切り

桁数の多い数値をカンマ区切りで表示することはよくあると思います。
useGrouping を ON にすることでカンマ区切りで整形できます。
デフォルトは ON なので、useGrouping に明示的に false を指定すればそのまま表示もできます。

const commaFormatter = new Intl.NumberFormat('ja')
const rawFormatter = new Intl.NumberFormat('ja'), { useGrouping: false })

commaFormatter.format(1234567890) // => '1,234,567,890'
rawFormatter.format(1234567890) // => '1234567890'

ユースケース:%表記

0 <= N <= 1な値を%表記にするために(N * 100).toFixed(2) + '%'みたいな処理をしたことないでしょうか。 NumberFormat にはstyleオプションがあり、これをpercentに変更することで%表記になります。

const percentFormatter = new Intl.NumberFormat('ja', { style: 'percent' })

percentFormatter.format(0.5) // 50%
percentFormatter.format(0.12345) // 12%

小数点以下をどう扱いたいかもオプションで指定可能です。
minimumFractionDigitsで最小桁数(足りない場合は 0 で埋める)、maximumFractionDigitsで最大桁数(超える場合はこの桁数に収まるよう四捨五入)を指定できます。

const fraction2 = new Intl.NumberFormat('ja', {
  style: 'percent',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
})

fraction2.format(0) // => '0.00%'
fraction2.format(0.12345) // => '12.35%'

また、有効数字も指定できます。

new Intl.NumberFormat('ja', {
  style: 'percent',
  maximumSignificantDigits: 1,
}).format(0.1234) // => '10%'

new Intl.NumberFormat('ja', {
  style: 'percent',
  maximumSignificantDigits: 2,
}).format(0.1234) // => '12%'

new Intl.NumberFormat('ja', {
  style: 'percent',
  maximumSignificantDigits: 3,
}).format(0.1234) // => '12.3%'

ユースケース:通貨の表記

EC や暗号通貨周りでありそうなユースケースだと思いますが、使い心地は「要件による」かなと思います。

stylecurrencyを指定し、currencyに表示したい通貨を指定し、currencyDisplayで表示方法を指定します。 為替をもとに通貨の変換をしてくれるわけではなく、あくまで渡された値の表示方法を整形してくれるだけなので、難しく考えることはないと思います。

new Intl.NumberFormat('ja', {
  style: 'currency',
  currency: 'JPY',
  currencyDisplay: 'symbol',
}).format(1000) // => '¥1,000'

new Intl.NumberFormat('ja', {
  style: 'currency',
  currency: 'JPY',
  currencyDisplay: 'code',
}).format(1000) // => 'JPY 1,000'

new Intl.NumberFormat('ja', {
  style: 'currency',
  currency: 'JPY',
  currencyDisplay: 'name',
}).format(1024) // => '1,024 円'

currencyDisplay がsymbol(デフォルト)の場合はどの言語でも表記は固定(通貨の記号)ですが、nameの場合は第一引数のロケールによって表記も変わります。
また、currencyDisplay がnameの場合は Node.js の場合はビルド設定(もしくは full-icu モジュールの有無)の影響を受けます。

new Intl.NumberFormat('en', {
  style: 'currency',
  currency: 'JPY',
}).format(1000) // => '¥1,000'

new Intl.NumberFormat('zh', {
  style: 'currency',
  currency: 'JPY',
  currencyDisplay: 'name',
}).format(1024) // => '1,024 日元'

new Intl.NumberFormat('ja', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'name',
}).format(1000) // => '1,000.00 米ドル'

また、カンマ区切りを OFF にすれば非カンマ区切りな値も作れます。

new Intl.NumberFormat('en', {
  style: 'currency',
  currency: 'JPY',
  useGrouping: false,
}).format(1000) // => '¥1000'

ユースケース:単位、通貨、%、カンマや小数点を数値とは別スタイルにしたい

さらに、デザインに合わせて%や通貨に色を付けたりフォント変えたりサイズ変えたいなどもあると思います。そういうときはformatToPartsを利用できます。

// React想定で書いてます
function Price({ price, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  })
  const [symbol, ...others] = formatter.formatToParts(price)

  return (
    <span>
      <span className="currency">{symbol.value}</span>
      <span>{others.map(({ value }) => value).join('')}</span>
    </span>
  )
}

;<Price price={1000} locale={'en'} currency={'USD'} />

デモ(codesandbox)

typeプロパティの値は種類が豊富なのでformatToParts のドキュメントをご覧ください。

策定中の仕様

tc39 のproposal-unified-intl-numberformatにて NumberFormat の追加 API について議論されています。

+-を常に表示するsignDisplayや、単位(m/s)の表記、特定領域の表現力を増すためのnotationオプションなどが議論されています。

Number.prototype.toLocaleString

Intl.NumberFormat クラスを利用する他に、Number.prototype.toLocaleStringというメソッドからも利用可能です。

実質的には Intl.NumberFormat のコンストラクタと同じ引数を取ります。好みに合わせて使い分けるといいと思います。
インスタンス自体を別ファイルに分けて再利用したいので、個人的には Intl.NumberFormat の方をよく使います。

さいごに

Intl には他にも色々な Format クラスがあるのですが、自然言語に寄りすぎており言葉尻が微妙に要件と合わないってことが多く、活用しきれていません。

その中でも NumberFormat クラスは「数値表現」だけにフォーカスされているので、汎用的で強力な API だと思います。使えるところではここぞとばかりに使っていきましょう!

JavaScriptNode.js
© 2012-2022 Leko