2011年01月18日

PHPで漢数字を数値に変換する

漢数字の表記はどういうものか。ルールが明らかになれば、変換できる。ああ、面倒だ。。。

・漢数字で使われる文字は、数字そのものを表す「〇一二三四五六七八九」と、桁数を表す「十百千万億…」
数字そのものを表す文字を数漢字、桁数を表す文字を桁漢字とする。
数漢字だけで表記されているものは、アラビア数字と同じなので簡単である。右が下位桁で、左に向かって上位桁。これは単純に置換すればいい。「二〇一一年」は、置換して「2011年」となる。
問題は桁漢字の混ざる場合である。

・桁漢字は、上位桁漢字の入れ子になり得る「十百千」と、一般的には入れ子にならない「万」以上で異なる
「一〇〇〇億円」「五〇〇億円」以外に、「一千億円」「五百億円」はあり得る、ということの計算機的表現をどうするかということ。
上位桁漢字の入れ子になり得る「十百千」を下桁字、「万」より上の文字を上桁字とする。
上桁字は単純にその先行部分の倍率を表す。「六万六」とあれば、" 6 x 10^4 + 6 " となるので、文字列を上桁字で区切って、上桁字の先行部分を数値化した上で倍率を掛ける。桁漢字の続かない部分は倍率が1である。
下桁字は、上桁字の先行部分に現れる可能性がある。「一千円」の「千」は倍率10^3だが、「一千億円」の「千」は、後続の「億」に絡んで10^3の10^12倍なので、倍率10^15を掛けることになる。

・下桁字は、単体でその1つ上の桁の「1」を表す可能性がある。
アラビア数字では、12は " 1 x 10^1 + 2 " で、位の値が1であっても1は省略されない。漢数字では、「十二」で、下桁字は先行部分があれば桁漢字として、なければ数漢字として機能する。「二十二」は "2/10/2" の3文字を " 2 x 10^1 + 2 " で、十 = 10^1 と単純に置き換えればいいが、「十二」は "10/2" の2文字で " 1 x 10^1 + 2 " となるから、"1" を補う必要がある。

と、いうことで、変換ロジックの作戦を次のように立案。

1. 入力された文字列を上桁字で分割する
2. 分割された各部分の上桁字を除いた部分を、下桁字を考慮して数値に変換する
  2.1. 左から入力を読み、数漢字であればそのまま数字として左詰に先行部分を入力する
  2.2. 下桁字が来たら、先行部分があれば倍率を乗じて加算、先行部分がなければ10/100/1000を加算する
  2.3. 終端が来たら、倍率1として先行部分を加算し、和を返す
3. 各部分の上桁字に応じて結果に倍率を乗じ、和を返す

作戦案に従ってコードに書き起こした結果。
define(SOURCE_CHARSET,	'EUC-JP');	// ソースコードの文字コード

function knum2arabic_10000($kanji, $incharset = 'EUC-JP')
{
// $kanji: 10000未満の漢数字表記文字列 $incharset: 入力文字コード
$kannum = array( // 数漢字-数値マップ
'0' => 0, '〇' => 0,
'1' => 1, '一' => 1,
'2' => 2, '二' => 2,
'3' => 3, '三' => 3,
'4' => 4, '四' => 4,
'5' => 5, '五' => 5,
'6' => 6, '六' => 6,
'7' => 7, '七' => 7,
'8' => 8, '八' => 8,
'9' => 9, '九' => 9,
);
$decinum = array( // 下桁字-倍率マップ
'十' => pow(10,1),
'百' => pow(10,2),
'千' => pow(10,3),
);
if( $incharset != SOURCE_CHARSET )
$kanji = mb_convert_encoding($kanji, SOURCE_CHARSET, $incharset);

$ival = 0;
$lnum = false;
for($pos=0;$pos<mb_strlen($kanji,SOURCE_CHARSET);$pos++){
$ch = mb_substr($kanji, $pos, 1,SOURCE_CHARSET);
if( isset($kannum[$ch]) ){
$lnum .= $kannum[$ch];
continue;
}
if( is_numeric($ch) ){
$lnum .= $ch;
continue;
}
if( isset($decinum[$ch]) ){
$ival += $lnum ? intval($lnum) * $decinum[$ch] : $decinum[$ch];
$lnum = false;
}
}
if( $lnum ) $ival += intval($lnum);
return $ival;
}

function knum2arabic($kanji, $incharset = 'EUC-JP')
{
// $kanji: 漢数字表記文字列 $incharset: 入力文字コード
$deci = array( // 上桁字-倍率マップ
'万' => pow(10,4),
'億' => pow(10,8),
'兆' => pow(10,12),
// 対応桁数を増やすときはここに追加
);
if( $incharset != SOURCE_CHARSET )
$kanji = mb_convert_encoding($kanji, SOURCE_CHARSET, $incharset);

$oldencoding = mb_regex_encoding();
mb_regex_encoding(SOURCE_CHARSET);

if( !mb_ereg_search_init($kanji) ){
mb_regex_encoding($oldencoding);
return -1;
}

foreach($deci as $i => $v){
$pat_sep .= $i;
}
$pattern = '[十百千一二三四五六七八九〇0-90-9]+['.$pat_sep.']'; // 数漢字として扱う文字

$i = 0;
while( ($match = mb_ereg_search_pos($pattern)) ) {
$part[$i] = substr($kanji, $match[0], $match[1]);
$kanji = str_replace($part[$i],'',$kanji);
$i++;
mb_ereg_search_init($kanji);
}
$part[$i] = $kanji;

$ival = 0;
for($i=0;$i<count($part);$i++){
$ipart = knum2arabic_10000($part[$i], SOURCE_CHARSET);
$imult = $deci[ mb_substr($part[$i], mb_strlen($part[$i], SOURCE_CHARSET)-1, 1, SOURCE_CHARSET) ];
$ival += $ipart * ($imult != 0 ? $imult : 1);
}

mb_regex_encoding($oldencoding);
return $ival;
}

実行すると

$k = '二〇〇八';
$r = knum2arabic($k, 'EUC-JP');
echo $k.' = '.$r."\n";

$k = '六千九百三十三万二千三百十四';
$r = knum2arabic($k, 'EUC-JP');
echo $k.' = '.$r."\n";

$k = '2億33万2310';
$r = knum2arabic($k, 'EUC-JP');
echo $k.' = '.$r."\n";

結果
二〇〇八 = 2008
六千九百三十三万二千三百十四 = 69332314
2億33万2310 = 200332310


ただ、この関数、変な表記のチェックはしないので、たとえば「二〇〇十」とか「十百万」とかは正しく変換できない。まあ、前者は200x10 = 2000 で正しいとも言えるし、後者は百万を取っ払った上で変換して結果に 10^6 を掛けるべきだろう。

※数漢字とか桁漢字とかの用語は表記の簡便のため適当に今思いついたもので、本当はなんて言うかはわかりません。


posted by usoinfo at 11:18 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

×

この広告は180日以上新しい記事の投稿がないブログに表示されております。