かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript 数字をカンマ区切りの文字列に変換したい

以前ゼロパディングで桁数を合わせる方法のメモを書いていましたが、三桁ごとにカンマ区切りのフォーマットもよくやるよな〜と思ったのでやり方のメモ。

こんなの

1234567890 => "1,234,567,890"
1234567890.1235 => "1,234,567,890.1235"
-1234567 => "-1,234,567"
-1234567.89054321 => "-1,234,567.89054321"

結論 Number.toLocaleStringIntl.NumberFormat を使うのが簡単

※ 但し小数点以下の有効桁数に注意

Number.toLocaleString を使う

Number.prototype.toLocaleString()
numObj.toLocaleString([locales [, options]])
新しい localesoptions 引数で アプリケーションは フォーマット変換で使われる言語を指定でき、関数の振る舞いをカスタマイズできます。古い実装では、localesoptions 引数は無視され、使われるロケールや返される文字列の形式は完全に実装依存です。
cf. Number.prototype.toLocaleString() - JavaScript | MDN

引数を指定しないとフォーマットはロケール依存になり、小数点以下は特定桁数で四捨五入される

const numbers = [1234567890, 1234567890.1235, -1234567, -1234567.89054321];

const numberFormat = (num: number): string => {
  return num.toLocaleString();
};

numbers.map((num) => numberFormat(num));
// => '1,234,567,890'
// => '1,234,567,890.124'
// => '-1,234,567'
// => '-1,234,567.891'

maximumFractionDigits オプションを使い小数点以下の桁数を保持する (※但し最大 20桁)

const numbers = [1234567890, 1234567890.1235, -1234567, -1234567.89054321];

const numberFormat = (
  num: number,
  options?: Intl.NumberFormatOptions
): string => {
  return num.toLocaleString(undefined, options);
};

numbers.map((num) => numberFormat(num, {maximumFractionDigits: 20}));
// => '1,234,567,890'
// => '1,234,567,890.1235'
// => '-1,234,567'
// => '-1,234,567.89054321'

₍ ᐢ. ̫ .ᐢ ₎👌

Intl.NumberFormat を使う

Intl.NumberFormat
new Intl.NumberFormat([locales [, options]]).format(number)
ローケルを指定しない基本的な使い方では、既定のローケルとオプションで書式化された文字列が返されます。
cf. Intl.NumberFormat - JavaScript | MDN

locales, optionstoLocaleString と同じ

引数を指定しないとフォーマットはロケール依存になり、小数点以下は特定桁数で四捨五入される

const numbers = [1234567890, 1234567890.1235, -1234567, -1234567.89054321];

const numberFormat = (num: number): string => {
  return new Intl.NumberFormat().format(num);
};

numbers.map((num) => numberFormat(num));
// => '1,234,567,890'
// => '1,234,567,890.124'
// => '-1,234,567',
// => -1,234,567.891'

maximumFractionDigits オプションを使い小数点以下の桁数を保持する (※但し最大 20桁)

const numbers = [1234567890, 1234567890.1235, -1234567, -1234567.89054321];

const numberFormat = 
  (options?: Intl.NumberFormatOptions) => 
  (locales?: string | string[]) =>
  (num: number): string => {
  return new Intl.NumberFormat(locales, options).format(num);
};

// 小数点以下 20 桁まで有効にするフォーマット
const format = numberFormat({ maximumFractionDigits: 20 });

const formatJP = numberFormat('ja-JP')
numbers.map((num) => formatJP(num));
// => '1,234,567,890',
// => '1,234,567,890.1235',
// => '-1,234,567',
// => '-1,234,567.89054321'

const formatDE = numberFormat('de-DE')
numbers.map((num) => formatDE(num));
// => 1.234.567.890',
// => '1.234.567.890,1235',
// => '-1.234.567',
// => '-1.234.567,89054321'

const formatFI = numberFormat('fi-FI')
numbers.map((num) => formatFI(num));
// => '1 234 567 890',
// => '1 234 567 890,1235',
// => '−1 234 567',
// => '−1 234 567,89054321'

₍ ᐢ. ̫ .ᐢ ₎👌

toLocaleString, Intl.NumberFormat の桁数のオプション

  • minimumIntegerDigits … 整数の最小桁数。可能な値は1から21, デフォルトは 1
  • minimumFractionDigits ... 小数桁の最小数。可能な値は0から20, 数値フォーマットのデフォルトは 0
  • maximumFractionDigits … 小数桁の最大数。可能な値は0から20, 数値フォーマットのデフォルトは、minimumFractionDigits3 の大きい方
  • minimumSignificantDigits … 有効桁の最小数。可能な値は1から21, デフォルトは 1
  • maximumSignificantDigits … 有効桁の最大数。可能な値は1から21, デフォルトは 21

cf. Intl.NumberFormat() constructor - JavaScript | MDN

あまり用途はなさそうだけど、小数点以下が 20 桁以上でそれを保持したい場合は整数部と小数点以下に分離して、整数部分をフォーマットした後に小数点以下と文字列結合する必要がある

const format = (num: number): string => {
  const numStrs = String(num).split('.');
  return [
      Number(numStrs[0]).toLocaleString(),
      numStrs.slice(1).join(''),
    ].filter(Boolean).join('.');
};

format(111111.098765432109876543210987);
// => "111,111.098765432109876543210987"
format(-111111.098765432109876543210987);
// => "-111,111.098765432109876543210987" 

数字のフォーマットを自前で実装するパターン

文字列にして自前でフォーマットしてしまうなら小数点以下が丸められるトラップにははまらない。
ただ、そんなケースはあまり無さそうなので自前実装するメリットほぼ無さそうだけどスクリプト書いてみたので載せておきます。

再帰関数で , 区切りにする

整数部分を再帰関数で , 区切りにして、小数点以下と文字列結合する

const separate = (numStr: string, separator: string = ','): string => {
  const length = numStr.length;

  if (length > 3) {
    const splitIndex = length - 3;
    // 後ろから 3文字づつ確定していく
    return [
      separate(numStr.substring(0, splitIndex), separator),
      numStr.substring(splitIndex),
    ].filter(Boolean).join(separator);
  }

  return numStr;
};

type FormatOption = {
  separator?: string;
  decimalPoint?: string;
};

const formatNumber = (
  num: number,
  options?: FormatOption,
): string => {
  const numStrs = String(num).split('.');
  return [separate(numStrs[0], options?.separator), numStrs.slice(1).join('')]
    .filter(Boolean)
    .join(options?.decimalPoint || '.');
};

const numbers = [1234567890, 1234567890.1235, -1234567, -1234567.89054321];
numbers.map((num) => formatNumber(num));
// => '1 234 567 890',
// =>  '1 234 567 890.1235',
// =>  '-1 234 567',
// =>  '-1 234 567.89054321'

₍ ᐢ. ̫ .ᐢ ₎👌

正規表現を使うパターン

正規表現は自信が無いのでググって出てきた replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') を使います。
この正規表現も整数部分でしか上手く動作しないので、整数部分を正規表現, 区切りにしてから小数点以下と結合します

type FormatOption = {
  separator?: string;
  decimalPoint?: string;
};

const formatNumber = (
  num: number,
  options?: FormatOption,
): string => {
  const numStrs = String(num).split('.');
  return [
    numStrs[0].replace(
      /(\d)(?=(\d\d\d)+(?!\d))/g,
      `$1${options?.separator || ','}`
    ),
    numStrs.slice(1).join(''),
  ]
    .filter(Boolean)
    .join(options?.decimalPoint || '.');
};

const numbers = [1234567890, 1234567890.1235, -1234567, -1234567.89054321];
numbers.map((num) => formatNumber(num, { separator: '.', decimalPoint: ',' }));
// => '1.234.567.890',
// => '1.234.567.890,1235',
// => '-1.234.567',
// => '-1.234.567,89054321'

₍ ᐢ. ̫ .ᐢ ₎👌

所管

Intl.NumberFormat は今回のメモを書くにあたって調べていて初めて知りました。

オプションで locale に合ったフォーマットができるので Number.toLocaleStringIntl.NumberFormat を使えば良さそう。
ただし、デフォルトでは小数点以下が丸められるので丸めたくない時は maximumFractionDigits オプションを使う必要がある。

ただこの2つの方法オプションも似ているので同じようなものが 2 つ存在していてどちらの方が良いチョット解ってない。2021年7月7日現在では toLocaleString は現行の標準になっているのでこっちを使うでいいのかな?

, これ コンマなのカンマなの???????


[参考]

夏樹みくる ちゃんハピバ 🎉🎂