🔢

Vim scriptでいろいろな数値文字列変換 桁区切りとか漢数字とか

2023/12/27に公開

DDSKKのドキュメントを見ていたら、数値を特定のフォーマットに変換する機能があることを知りました。

https://ddskk.readthedocs.io/ja/latest/06_apps.html#number-conv

そこで、紹介されていた各変換方法を真似て、「数字の文字列を変換する関数」をVim scriptで作ってみました。
なお、#4はSKKの変換機能を呼び出す特殊な値なので省きます。

簡単な順に説明します。

#0 無変換

入力された数値をそのまま出力します。
特に変換処理は必要ありません。

function! Numconv0(numstr) abort
  return a:numstr
endfunction

#1 全角数字

各数値を全角文字に変換します。
文字列の変換には一般にsubstitute()が使えますが、今回のように文字単位で対応が取れている場合はtr()が便利です。

function! Numconv1(numstr) abort
  return a:numstr->tr('0123456789', '0123456789')
endfunction

#2 漢数字 位取りあり

1024一〇二四にするような変換です。
#1と同じ発想で可能です。

function! Numconv2(numstr) abort
  return a:numstr->tr('0123456789', '〇一二三四五六七八九')
endfunction

#9 棋譜入力

7六歩 3四金のようなやつです。1字目を全角数字、2字目を漢数字に変換します。ニッチすぎでは?
#1と#2を両方適用すればOKなのですが、tr()はすべての文字を置換してしまうので、1字目と2字目で変換処理を変えることができません。
したがって、substitute()の引数にtr()を渡すことで1字ずつ変換します。

function! Numconv9(numstr) abort
  return a:numstr
        \ ->substitute('\d', {m->tr(m[0], '123456789', '123456789')}, '')
        \ ->substitute('\d', {m->tr(m[0], '123456789', '一二三四五六七八九')}, '')
endfunction

ここは0は入らないと考えて良いでしょう。

#8 桁区切り

12345671,234,567に変換します。
後ろから数えて、数値が3桁連続したところで,を挟んでいけばOKです。
文字列を逆転させるreverse()関数を使います。
とりあえず以下のようなものを作ってみます。

" 不完全
function! Numconv8(numstr) abort
  return a:numstr->reverse()->substitute('\d\{3}', '\0,', 'g')->reverse()
endfunction

しかしこれでは123,123となってしまいます。最上位桁にはカンマは不要ですよね。
処理したあとに先頭カンマを除去しても良いのですが、substitute()がもう一つ増えます。
実は、Vimの正規表現の\zeを使うと以下のように書くことができます。

function! Numconv8(numstr) abort
  return a:numstr->reverse()->substitute('\d\{3}\ze\d', '\0,', 'g')->reverse()
endfunction

「以降に桁が続いている場合」を表現しつつ、その継続桁の部分はマッチとして消費しないので、これだけで全体を適切に置換できます。

#5 大字

#3と#5はどちらも漢数字の表記です。#5の結果から#3を作ることは簡単なので、#5をもとに説明します。

「大字」は、金額記入の際などに用いられる数字表現です。なんかかっこいい文字を使うやつですね。

https://ja.wikipedia.org/wiki/大字_(数字)

統一された規格があるわけではなさそうですが、ここでは、「壱百」のような表記もOKとします。

また、

日本の法令で定められているのは壱、弐、参、拾のみである

とのことなので、他の数字は一般的な漢数字を使うこととします。肆とか陌とかを使いたい方は調整してください。

#8と同様に文字列を後ろから数えて適当な位置に単位を打っていきます。
まず4桁以内で考えましょう。

'1024'を入力する場合、以下のアルゴリズムで変換できます。

  1. 逆順にする '4201'
  2. 文字ごとに分解する '4', '2', '0', '1'
  3. 2番目以降の文字に桁の文字を足す '4', '拾2', '百0', '千1'
    a. 「最初の要素には空文字を足す」と考えればインデックスによる場合分けが不要になる
  4. ゼロを含む桁を除去する [1] '4', '拾2', '千1'
  5. ひとつの文字列に接続する '4拾2千1'
  6. 漢数字に変換する(#2と同じ) '四拾弐千壱'
  7. 逆順にする '壱千弐拾四'

コードは以下の通りになります。各行は前述の処理手順と対応しています。

function! Keta1(numstr) abort
  let keta = [''] + '拾百千'->split('\zs')
  return a:numstr->reverse()
        \ ->split('\zs')
        \ ->map({i,v -> keta[i] .. v})
        \ ->filter('v:val !~ "0"')
        \ ->join('')
        \ ->tr('123456789', '壱弐参四五六七八九')
        \ ->reverse()
endfunction

echo Keta1('1234')
" => 壱千弐百参拾四

なお、処理の順序は適宜入れ替え可能です。例えば、文字列を逆順にしたあとで漢数字に変換しても同じ結果になりますね。

さて、Keta1のコードの桁区切り文字リストの定義を変え [2]split()で4字ずつ分割するようにすれば、4桁ごとに区切れるようになります。
(こちらは桁の情報を残したいのでfilter()は無し)

function! Keta2(numstr) abort
  let keta = [''] + '万億兆京垓𥝱'->split('\zs')
  return a:numstr->reverse()
        \ ->split('\d\{4}\zs')
        \ ->map({i,v -> keta[i] .. v})
        \ ->join('')
        \ ->tr('0123456789', '〇壱弐参四五六七八九')
        \ ->reverse()
endfunction

echo Keta2('123456789')
" => 壱億弐参四五万六七八九
" ↑ 見づらいが、1億2345万6789

これで「4桁ごとに区切る」「4桁以内を区切る」ができました。あとはこの2つを組み合わせて、全体を変換します。
具体的には、Keta2()map()の内部で使われる変数vに対して、Keta1()を適用します。
ちなみに、この部分では既に逆順になっているため、Keta1()の一部の処理は省略できます。

結果として、#5の変換は以下のようになります。lambdaが入れ子になっている部分では、外側の引数をi/v、内側の引数をj/uと使い分けています。

function! Numconv5(numstr) abort
  let inner_keta = [''] + '拾百千'->split('\zs')
  let outer_keta = [''] + '万億兆京垓𥝱'->split('\zs')
  return a:numstr->reverse()
        \ ->split('\d\{4}\zs')
        \ ->map({i,v -> outer_keta[i] .. v->split('\zs')
        \                                 ->map({j,u -> inner_keta[j] .. u})
        \                                 ->filter('v:val !~ "0"')
        \                                 ->join('')})
        \ ->join('')
        \ ->tr('123456789', '壱弐参四五六七八九')
        \ ->reverse()
endfunction

#3 漢数字 位取りなし

1024千二十四にするような変換です。
#5の変換結果をtr()を使って一般的な表記に書き換えます。
さらに、「一百」のような表現は不自然なので除去します。ただし、単純に「一」をすべて除去することはできません。「一億」のような場合は「一」を残したいためです。
ここでも正規表現の\zeが便利です。

function! Numconv3(numstr) abort
  return Numconv5(a:numstr)
        \ ->tr('壱弐参拾', '一二三十')
        \ ->substitute('一\ze[十百千]', '', 'g')
endfunction

「一千円」のように、「一千」を許容する場合もありそうですね。

入力値のバリデーション

載せてきた関数はどれも「数字の文字列」を引数として期待していますが、各関数ではバリデーションを行っていません。
以下のようにsubstitute()を介して各関数を呼び出すようなラッパーを作れば、入力値の型の保証ができると思います。

echo 'お支払いは税込み31900円です。'->substitute('\d\+', {m->Numconv3(m[0])}, 'g')
" => お支払いは税込み三万千九百円です。
脚注
  1. 零百のようにするのかと思っていましたが、しないみたいです。確かに10000壱万零千零百零拾零と書かれていたら鬱陶しいですね ↩︎

  2. めっっっちゃでかい桁も扱う予定がある人は定義を追加してください。筆者は小学校のときに阿僧祇とか覚えたけど使ったことはないです ↩︎

Discussion