正規表現で整数の桁区切り

(1/1)
整数をカンマ区切りに変換するのは  number_format  関数を使えば簡単にできる。では、テキスト中に何度も登場する整数をカンマ区切りにするにはどうしたらいいか、さらには「12345」を「1万2345」のように漢字区切り数字に変換するにはどうしたらいいだろうか。
これも、PHPと正規表現を使えば簡単に変換できる。

(2024年2月23日)int2comma() -- 正規表現を追加

目次

サンプル・プログラムの実行例

正規表現で整数の桁区切り

サンプル・プログラム

圧縮ファイルの内容
number_format2.phpサンプル・プログラム本体
number_format2.php 更新履歴
バージョン 更新日 内容
2.1.0 2024/02/23 int2comma() -- 正規表現を追加
2.01 2021/08/27 bug-fix
2.0 2021/08/16 PHP8対応,大幅改訂
1.1 2007/02/21 指数表現整数に対応した
1.0 2007/02/12 初版

変換の考え方

カンマ区切りと漢字区切りは分けて考えることにする。

まずカンマ区切りの方だが、テキスト中から整数だけを切り出せれば、あとは  number_format  関数を使って変換することができる。テキスト中から数字のみを切り出すなら、正規表現を使って簡単に実現できるはずだ。
PHPには便利な関数がある。 preg_replace_callback  関数は、1バイト文字にしか対応していないが、指定した正規表現にマッチする部分文字列に対し、指定した関数を実行し、その結果を使って部分文字列を置換するというものである。
日本語で書くと冗長になるが、要は、'[0-9]+' にマッチする部分文字列に対して  number_format  関数を適用すれば良い。

次に、漢字区切りの方だが、万、億、兆、京‥‥と、コード上は何の規則性もない1文字を差し込んでいかなければならないので、力技で処理するしか無さそうだ。
最初に、万、億、兆、京‥‥のテーブルを作っておき、与えられた整数を1京で割った商に '京' を付加し、剰余を1兆で割った商に'兆'を付加し‥‥と繰り返して処理することにする。

解説:カンマ区切りテキストに変換

 205: /**
 206:  * 数値をカンマ区切り文字列に変換する
 207:  * @param   float $n 数値
 208:  * @return  string 変換後文字列
 209: */
 210: function int2comma($n) {
 211:     preg_match("/([0-9]+)(\.[0-9]*)/", $n, $matches);
 212:     if (isset($matches[2]))     return $n;      //小数はそのまま返す
 213: 
 214:     return number_format($n);
 215: //  return preg_replace('/(\d)(?=(\d{3})+(?!\d))/', '$1,', $n); //正規表現
 216: }

与えられた数値をカンマ区切りテキストに変換する int2comma 関数をユーザー定義した。
後述する正規表現処理との兼ね合いで、小数も引数として許可することにした。ただし、引数が小数だったら、そのまま返す。小数かどうか判断するのに、"$n - intval ($n)" という式が使えるのだが、処理系によっては intval  関数が32 ビット整数まで(-2,147,483,648 ~ 2,147,483,647 )しか対応していないため、ここでは正規表現を使って、小数点がある場合を小数と判断している。

カンマ区切りにするのに  number_format  関数をそのまま使ったが、正規表現による方法をコメントアウトしてあるので参考にしてほしい。

少々複雑な正規表現だが、正規表現は右から見ていくと分かりやすい。
一番右は \( \backslash d \) で、これは数字(0, 1, 2...9)にマッチする。\( ?! \) は否定的先読みで、\( (?! \backslash d) \) と書くことで数字ではない文字(実際には挿入したカンマ)にマッチする。
次は \( \backslash d\{3\} \) だが、これは数字3桁にマッチする。\( ?\!\!= \) は肯定的先読みで、\( A (?\!\!= B) \) と書くことで、後方文字がBにマッチするときにBにマッチする。ここでは \( (\backslash d) (?\!\!= (\backslash d\{3\})+(?! \backslash d)) \) と書いているので、後方が数字3桁+数字以外の文字にマッチするとき、その前の数字にマッチ――つまり、カンマを入れるべき位置を取り出すことができる。

解説:漢字区切りテキストに変換

 218: /**
 219:  * 数値を漢字区切り文字列に変換する
 220:  * @param   float $n 数値
 221:  * @return  string 変換後文字列
 222: */
 223: function int2kanji($n) {
 224:     $tbl = array(1=>"万", 2=>"億", 3=>"兆", 4=>"京");
 225: 
 226:     preg_match("/([0-9]+)(\.[0-9]*)(E\+[0-9]*)/", $n, $matches);
 227:     if (!isset($matches[3]) && isset($matches[2]))  return $n;  //小数はそのまま返す
 228: 
 229:     $m = preg_match("/(\.[0-9]*)(E\+[0-9]*)/", $n? strval2($n: $n;
 230:     $s = "";
 231:     for ($i = count($tbl); $i >1$i--) {
 232:         $b = bcpow(10, 4 * $i);
 233:         $a = bcdiv(bcsub($m, bcmod($m, $b)), $b);
 234:         if ($a > 0) {
 235:             $s .= ($a . $tbl[$i]);
 236:             $m = bcmod($m, $b);
 237:         }
 238:     }
 239:     if ($m > 0)     $s .$m;
 240: 
 241:     return $s;
 242: }

与えられた数値を漢字区切りテキストに変換するint2kanji 関数をユーザー定義した。
まず、万、億、兆、京‥‥のテーブルを配列変数 $tbl に用意しておく。ここは '無量大数' まで増やしてもらって構わない。
この関数でも、小数も引数として許可することにした。ただし、引数が小数だったら、そのまま返す。ロジックは int2comma 関数と同様である。
漢字の差し込みは、上位桁から下位桁へ向かって行う。
1京、1兆、1億‥‥を書くのはゼロがたくさんあって間違ってしまいそうなので(こういったケアレス・バグを潰す意味で)、 bcpow  関数を使って10のべき上を計算式で表現している。商と剰余の計算を行い、対応する漢字を付与する。
浮動小数演算誤差が心配なので、すべての演算は BCMath 任意精度数学関数を利用している。

解説:数値を分解

 346: //変換処理
 347: $func = getSelectFunc();
 348: if ($func == 'comma') {
 349:     $dest = preg_replace_callback("/[0-9]+[\.0-9]*/",
 350:         function($matches) {
 351:             return int2comma($matches[0]);
 352:         }, $sour
 353:     );
 354: else {
 355:     $dest = preg_replace_callback("/[0-9]+[\.0-9]*/",
 356:         function($matches) {
 357:             return int2kanji($matches[0]);
 358:         }, $sour
 359:     );
 360: }
 361: 

与えられたテキストから正規表現を使って数値を分解する部分の処理である。
小数も扱うので、数値を切り出すための正規表現は '[/[0-9]+[\.0-9]*/' である。また、与えられたテキストは変数 $dest に入ってくる。
カンマ区切りの場合は、 preg_replace_callback  関数を使って int2comma 関数を呼び出してやる。この呼び出しは少し長いので解説しよう。
 preg_replace_callback  関数 の第1引数はマッチングのための正規表現である。これは前述したとおり。第2引数は、マッチングした部分文字列を渡す関数である。ただし、マッチング結果はグループ表現 (...) に対応する配列変数として渡されるので、そのまま int2comma 関数に渡すわけにはいかない。
そこで、無名関数を用いて、一時的に関数を発生させる。つまり、マッチした配列変数 $matches に対して、int2comma 関数には $matches[0] を渡してやり、その戻値を無名関数の戻り値としてやるのだ。

漢字区切りの場合も同様である。 preg_replace_callback  関数と  create_function  関数を利用し、マッチした部分文字列(数値)を int2kanji 関数に渡してやる。ここで、部分文字列として小数もマッチさせたのには理由がある。

整数だけマッチさせたいなら、'/[^\.]([0-9]+)/' で十分なのだが、 preg_replace_callback  関数 はマルチバイト文字に対応していない。このため、全角文字が混在しているテキストでは正常に動作しない可能性があるのだ(処理系によっては動作するかもしれない)。
そこで、小数を含む数値を切り出すように、より単純な正規表現 '/[0-9]+[\.0-9]*/' を用いることにしたわけである。
あとは、各々の関数の内部で、小数かどうかを正規表現で判断し、小数であればそのまま戻すようにしているわけだ。

解説:表示処理

 244: /**
 245:  * HTML BODYを作成する
 246:  * @param   string $dest 元のテキスト
 247:  * @param   string $sour 置換後テキスト
 248:  * @return  string HTML BODY
 249: */
 250: function makeCommonBody($dest, $sour) {
 251:     global $SelectFuncs;
 252: 
 253:     $myself  = MYSELF;
 254:     $refere  = REFERENCE;
 255:     $title   = TITLE;
 256:     $version = '<span style="font-size:small;">' . date('Y/m/d版', filemtime(__FILE__)) . '</span>';
 257:     $width   = WIDTH;
 258:     $width2  = WIDTH / 2 - 30;
 259:     $debug   = '';
 260: 
 261:     //処理選択ラジオボタン
 262:     $str_radio = '';
 263:     $i = 1;
 264:     foreach ($SelectFuncs as $key=>$val) {
 265:         $str_radio ."<input type=\"radio\" name=\"funcs\" value=\"{$key}\" {$val['checked']} />{$val['title']} ";
 266:         $i++;
 267:     }
 268: 
 269:     //デバッグ情報
 270:     if (! FLAG_RELEASE) {
 271:         $phpver = phpversion();
 272:         $debug =<<< EOT
 273: <p>
 274: <span style="font-weight:bold;">★デバックモードで動作中...</span><br />
 275: PHPver : {$phpver}
 276: 
 277: EOT;
 278:     }
 279: 
 280:     $body =<<< EOT
 281: <body>
 282: <h2>{$title} {$version}</h2>
 283: <form name="myForm" method="post" action="{$myself}" enctype="multipart/form-data">
 284: <table style="border:0px; border-spacing:10px;">
 285: <tr>
 286: <td>カンマ区切り<br />
 287: <textarea id="sour" name="sour" rows="10" style="width:{$width2}px;">{$sour}</textarea>
 288: </td>
 289: <td>⇒</td>
 290: <td>漢字区切り<br />
 291: <textarea id="dest" name="dest" rows="10" style="width:{$width2}px;">{$dest}</textarea>
 292: </td>
 293: </tr>
 294: <tr>
 295: <td>{$str_radio}</td>
 296: <td>&nbsp;</td>
 297: <td>&nbsp;</td>
 298: </tr>
 299: <tr>
 300: <td>
 301: <input type="submit" id="exec" name="exec" value="変換" /> 
 302: <input type="submit" id="reset" name="reset" value="リセット" />
 303: </td>
 304: <td>&nbsp;</td>
 305: <td>
 306: <input type="button" id="btncopy" name="btncopy" value="コピー" data-clipboard-target="dest" />
 307: </td>
 308: </tr>
 309: </table>
 310: </form>
 311: 
 312: <div style="border-style:solid; border-width:1px; margin:20px 0px 0px 0px; padding:5px; width:{$width}px; font-size:small; overflow-wrap:break-word; word-break:break-all;">
 313: <h3>使い方</h3>
 314: <ol>
 315: <li>[<span style="font-weight:bold;">元のテキスト</span>]に置換したい半角数字混在テキストを入力してください.</li>
 316: <li>[<span style="font-weight:bold;">カンマ区切り</span>]または[<span style="font-weight:bold;">漢字区切り</span>]を選択してください.</li>
 317: <li>[<span style="font-weight:bold;">変換</span>]ボタンを押してください.</li>
 318: <li>[<span style="font-weight:bold;">変換後テキスト</span>]に変換後テキストを表示します.</li>
 319: <li>[<span style="font-weight:bold;">コピー</span>]ボタンをクリックすると,[<span style="font-weight:bold;">変換後テキスト</span>]の内容をクリップボードにコピーします.</li>
 320: <li>[<span style="font-weight:bold;">リセット</span>]ボタンをクリックすると,表示をクリアします.</li>
 321: </ol>
 322: ※参考サイト:<a href="{$refere}">{$refere}</a>
 323: {$debug}
 324: </div>
 325: </body>
 326: 
 327: EOT;
 328:     return $body;
 329: }

最後が表示処理だが、フローは 前回とほとんど同じである。カンマ区切りと漢字区切りの処理を切り替えるためのラジオボタンを追加した程度である。
(この項おわり)
header