PHP の number_format()

数値(整数)をカンマ区切りで表示したい場合, PHP だと number_format()という関数がある。

echo number_format(1234567); // 結果 : 1,234,567

桁数の多い個数や金額などを表示させたいときに重宝する関数である。

javascript で number_format() ?

同じ事を javascript でやろうとすると少し厄介である。javascript には number_format() に相当する関数が存在しない。
ではどうやって実現するか。WEBで検索すると以下のような正規表現を用いるのが一般的らしい。

/**
 * 整数値を3桁ずつカンマ区切りで出力する
 *
 * @param int num 整数値
 * @return string カンマ区切りされた文字列
 */
function number_format(num) {
  return num.toString().replace(
    /(\d+?)(?=(?:\d{3})+$)/g,
    function (x) {
      return x + ',';
    }
  );
}

// テスト
console.log(number_format(1234567)); // 結果 : 1,234,567

replace() の挙動は解るとして,この正規表現は初心者には何やら難しそうに見えるのではないだろうか。
今回はこの正規表現を1歩ずつ詳しく見ていこうと思う。

正規表現の詳細

初めにどういう方針で正規表現で置換を行うか簡単に説明しておく。
例として 1234567890 を3桁ずつカンマ区切りにしたいとする。
このとき正規表現を用いて「1」「234」「567」の3つをキャプチャ(後述)して,
これらをそれぞれ「1,」「234,」「567,」に置換するというからくりである。

/\d/

半角数字にマッチ。[0-9]と同等。これは初心者でもわかる。

var match = '12abcde3456fgh'.match(/\d/);
console.log(match);
// 結果 : ["1", index: 0, input: "12abcde3456fgh"]

match[0] にはマッチされた文字列が入り,
match[‘index’] にはマッチされた文字列の場所(インデックス)が入り,
match[‘input’] には正規表現の対象文字列が入る。

/\d{3}/

半角数字の3回繰り返しにマッチ。{n}はn回繰り返し。これも簡単。

var match = '12abcde3456fgh'.match(/\d{3}/);
console.log(match);
// 結果 : ["345", index: 7, input: "12abcde3456fgh"]

/(\d{3})/

「半角数字の3回繰り返し」のキャプチャ。括弧で囲むことでキャプチャすることができ,マッチ配列に追加される。

var match = '12abcde3456fgh'.match(/(\d{3})/);
console.log(match);
// 結果 : ["345", "345", index: 7, input: "12abcde3456fgh"]

match[1] にキャプチャされた文字列が入る。
さらにキャプチャが増えると match[2], match[3], ・・・に追加される。

/(\d{3})+/

「半角数字の3回繰り返し」の1回以上の繰り返し。この辺からややこしくなってくるが,結果をみればなるほど納得できる。

var match;

match = '1234567890'.match(/(\d{3})+/);
console.log(match);
// 結果 : ["123456789", "789", index: 0, input: "1234567890"]

match = '123456789'.match(/(\d{3})+/);
console.log(match);
// 結果 : ["123456789", "789", index: 0, input: "123456789"]

match = '12345678'.match(/(\d{3})+/);
console.log(match);
// 結果 : ["123456", "456", index: 0, input: "12345678"]

match = '1234567'.match(/(\d{3})+/);
console.log(match);
// 結果 : ["123456", "456", index: 0, input: "1234567"]

match = '123456'.match(/(\d{3})+/);
console.log(match);
// 結果 : ["123456", "456", index: 0, input: "123456"]

match = '12345'.match(/(\d{3})+/);
console.log(match);
// 結果 : ["123", "123", index: 0, input: "12345"]

ここで注意したいのは 1234567890 でマッチされるのは 123456789 だが キャプチャされるのは 789 であるということ。
「123」「456」「789」と繰り返される場合の最後の結果のみをキャプチャする。

/(\d{3})+$/

「半角数字の3回繰り返し」の1回以上の繰り返し+末尾。
最後に末尾アンカーの$を付けたので3桁の繰り返しは末尾から検索される。

var match;

match = '1234567890'.match(/(\d{3})+$/);
console.log(match);
// 結果 : ["234567890", "890", index: 1, input: "1234567890"]

match = '123456789'.match(/(\d{3})+$/);
console.log(match);
// 結果 : ["123456789", "789", index: 0, input: "123456789"]

match = '12345678'.match(/(\d{3})+$/);
console.log(match);
// 結果 : ["345678", "678", index: 2, input: "12345678"]

match = '1234567'.match(/(\d{3})+$/);
console.log(match);
// 結果 : ["234567", "567", index: 1, input: "1234567"]

match = '123456'.match(/(\d{3})+$/);
console.log(match);
// 結果 : ["123456", "456", index: 0, input: "123456"]

match = '12345'.match(/(\d{3})+$/);
console.log(match);
// 結果 : ["345", "345", index: 2, input: "12345"]

これによって最後尾の3桁を検索することに成功した。

/(?=・・・)/

肯定先読み。この辺から中級者レベルだろうか。
/hoge$/ で「直後に末尾があるhoge」にマッチするのと同じ感覚で,
/hoge(?=fuga)/ で「直後にfugaがあるhoge」にマッチする。

var match;

match = '12345654321'.match(/4(?=3)/);
console.log(match);
// 結果 : ["4", index: 7, input: "12345654321"]
// 直後に3がある4にマッチする。

match = '12345654321'.match(/\d(?=3)/);
console.log(match);
// 結果 : ["2", index: 1, input: "12345654321"]
// 直後に3がある数字にマッチする。

match = '12345654321'.match(/\d+(?=3)/);
console.log(match);
// 結果 : ["12345654", index: 0, input: "12345654321"]
// 直後に3がある数字の繰り返しにマッチする。(最長マッチ)

ここで気を付けたいのは最後の例の最長マッチ。
\d+はなるべく長くなるように検索されるので(?=3) は初めの3ではなく2回目の3を指す。
よって\d+は「12」ではなく「12345654」にマッチする。

/(\d+)(?=(\d{3})+$)/

「「半角数字の3回繰り返し」の1回以上の繰り返し+末尾」が直後にある \d+ にマッチしてキャプチャする。

var match;

match = '1234567890'.match(/(\d+)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["1234567", "1234567", "890", index: 0, input: "1234567890"]

match = '123456789'.match(/(\d+)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["123456", "123456", "789", index: 0, input: "123456789"]

match = '12345678'.match(/(\d+)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["12345", "12345", "678", index: 0, input: "12345678"]

match = '1234567'.match(/(\d+)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["1234", "1234", "567", index: 0, input: "1234567"]

match = '123456'.match(/(\d+)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["123", "123", "456", index: 0, input: "123456"]

match = '12345'.match(/(\d+)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["12", "12", "345", index: 0, input: "12345"]

ここでポイントになるのは \d+ の最長マッチが (\d{3})+ の最長マッチより優先されるということ。
1234567890 の場合
\d+ が 1234567 にマッチし,(\d{3})+ が 890 を指す。
\d+ が 1 にマッチし, (\d{3})+ が 234567890 を指すということはない。
(しかし目標は「1」にマッチさせることである。)

/(\d+?)(?=(\d{3})+$)/

\d+? にすることで最短マッチにすることができる。
これによって (\d{3})+ が最長マッチとなる。

var match;

match = '1234567890'.match(/(\d+?)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["1", "1", "890", index: 0, input: "1234567890"]

match = '123456789'.match(/(\d+?)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["123", "123", "789", index: 0, input: "123456789"]

match = '12345678'.match(/(\d+?)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["12", "12", "678", index: 0, input: "12345678"]

match = '1234567'.match(/(\d+?)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["1", "1", "567", index: 0, input: "1234567"]

match = '123456'.match(/(\d+?)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["123", "123", "456", index: 0, input: "123456"]

match = '12345'.match(/(\d+?)(?=(\d{3})+$)/);
console.log(match);
// 結果 : ["12", "12", "345", index: 0, input: "12345"]

1234567890 の場合
\d+ が 1 にマッチし, (\d{3})+ が 234567890 を指す。
めでたく「1」をキャプチャすることに成功した。
この場合の残る課題は2つ。
「890」は置換対象ではないのでキャプチャから外したいことと,
「234」「567」をキャプチャすることである。

/(\d+?)(?=(?:\d{3})+$)/

(・・・)でキャプチャすることができるが,(?:・・・)とすれば括弧の機能はそのままにしてキャプチャをしなくなる。

var match;

match = '1234567890'.match(/(\d+?)(?=(?:\d{3})+$)/);
console.log(match);
// 結果 : ["1", "1", index: 0, input: "1234567890"]

match = '123456789'.match(/(\d+?)(?=(?:\d{3})+$)/);
console.log(match);
// 結果 : ["123", "123", index: 0, input: "123456789"]

match = '12345678'.match(/(\d+?)(?=(?:\d{3})+$)/);
console.log(match);
// 結果 : ["12", "12", index: 0, input: "12345678"]

match = '1234567'.match(/(\d+?)(?=(?:\d{3})+$)/);
console.log(match);
// 結果 : ["1", "1", index: 0, input: "1234567"]

match = '123456'.match(/(\d+?)(?=(?:\d{3})+$)/);
console.log(match);
// 結果 : ["123", "123", index: 0, input: "123456"]

match = '12345'.match(/(\d+?)(?=(?:\d{3})+$)/);
console.log(match);
// 結果 : ["12", "12", index: 0, input: "12345"]

/(\d+?)(?=(?:\d{3})+$)/g

モード修飾子gをつけたので (\d{3})+ が最長以外を検索するようになる。

var match;

match = '1234567890'.match(/(\d+?)(?=(?:\d{3})+$)/g);
console.log(match);
// 結果 : ["1", "234", "567"]

match = '123456789'.match(/(\d+?)(?=(?:\d{3})+$)/g);
console.log(match);
// 結果 : ["123", "456"]

match = '12345678'.match(/(\d+?)(?=(?:\d{3})+$)/g);
console.log(match);
// 結果 : ["12", "345"]

match = '1234567'.match(/(\d+?)(?=(?:\d{3})+$)/g);
console.log(match);
// 結果 : ["1", "234"]

match = '123456'.match(/(\d+?)(?=(?:\d{3})+$)/g);
console.log(match);
// 結果 : ["123"]

match = '12345'.match(/(\d+?)(?=(?:\d{3})+$)/g);
console.log(match);
// 結果 : ["12"]

モード修飾子gをつけると返ってくる結果も少し変化する。
match[0] と match[‘index’] と match[‘input’] の3つが消え,
キャプチャした文字だけの配列を返すようになる。
(結果として今までmatch[1]に入っていた値がmatch[0]にシフトする。)

以上。