Number#toFixed や Math.round/floor/ceil を駆使して表示用の値を整形することってないでしょうか。カンマ区切りをオレオレユーティリティ関数で実装したりそういったことを実現するライブラリを探したことはないでしょうか。
ほとんどの JavaScript の実行環境にはIntl
という i18n のためのオブジェクトが組み込まれており、その中の1つにIntl.NumberFormatというクラスがあります。
オプションが色々あり、かなり便利で多機能なんですが整理された情報が少なく、身の回りで使用してる人も少ないと感じました。
(2019/03/04あたりに結果表示されると思います。途中経過を見る場合元ツイートに飛んでください。)Do you use Intl.NumberFormat?
— れこ (@L_e_k_o) February 25, 2019
「それ自前で実装しなくても 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 プロジェクトを作っておいたので、気になる方は下記リンクから動作確認してください。
ただ、仮に動かない環境があったとしても 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 や暗号通貨周りでありそうなユースケースだと思いますが、使い心地は「要件による」かなと思います。
style
にcurrency
を指定し、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'} />
type
プロパティの値は種類が豊富なのでformatToParts のドキュメントをご覧ください。
tc39 のproposal-unified-intl-numberformatにて NumberFormat の追加 API について議論されています。
+-を常に表示するsignDisplay
や、単位(m/s
)の表記、特定領域の表現力を増すためのnotation
オプションなどが議論されています。
Intl.NumberFormat クラスを利用する他に、Number.prototype.toLocaleStringというメソッドからも利用可能です。
実質的には Intl.NumberFormat のコンストラクタと同じ引数を取ります。好みに合わせて使い分けるといいと思います。
インスタンス自体を別ファイルに分けて再利用したいので、個人的には Intl.NumberFormat の方をよく使います。
Intl には他にも色々な Format クラスがあるのですが、自然言語に寄りすぎており言葉尻が微妙に要件と合わないってことが多く、活用しきれていません。
その中でも NumberFormat クラスは「数値表現」だけにフォーカスされているので、汎用的で強力な API だと思います。使えるところではここぞとばかりに使っていきましょう!