2012年04月21日

C++/CLIでフィルタを作ってみる

先日バッチファイルを作ったとき、文字セット変換の部分をpythonのワンライナーで書いてみました。

python -c"import codecs,sys;sys.stdout=codecs.lookup('%CHARSET%')[-1](sys.stdout);f=codecs.open('text.tmp','r','sjis');print f.read();f.close()">text.txt

これで、SJISの文書をUTF-8にしたり、EUC-JPにしたりするわけです。でも、こんなことするのに、わざわざpythonインストールするのも大変なんで、どうしようかと考えてました。

まず考えたのは、perlで書くこと。patchコマンドを使うものを次に書こうと思っていて、Windows に patchコマンドを入れるんだったらMinGW/MSYSのものが入れやすいので( MinGW Developer Toolkitにチェックを入れる)、MinGW/MSYSを入れるとき、perlも入るので、perlでもいいと思ったわけです。

perlだと、かなりすっきりして、こんな感じになりました。(結局公開しなかったけど。)

perl -MEncode -pe "$_=encode('%CHARSET%',decode('SHIFT_JIS',$_));" text.tmp>text.txt

でも、patchコマンドの作業は、無ければ無いで手作業でできることなので、わざわざMinGW/MSYSを入れるのも大変だなと思って、他の手は無いかと考えてみました。

灯台もと暗し。C++でビルドする作業を自動化しようとしているわけですから、それもC++で書けばいいのです。C++/CLIで作る標準入力のデータを加工して、標準出力に出すだけのサンプルのような簡単なフィルタプログラムです。

以前、UTF-8に変換するプログラムをMultiByteToWideCharを使って書いたことありますが、面倒だった記憶しか残ってません。それは嫌なので、C++/CLIで作ってみました。.NET FrameworkのEncodingクラスが使えるなら、利用しない手はないわけです。結局こうなりました。

#using <System.dll>
using namespace System;
using namespace System::Text;
using namespace System::IO;

int main(array<System::String ^> ^args)
{
    TextReader^ input = Console::In;
    String^ charset;

    if (args->Length != 1)
    {
        return -1;
    }

    String^ sw = args[0]->ToLower();

    if (String::Compare(sw, "-utf-8") == 0) {
        charset = "UTF-8";
    }else if (String::Compare(sw, "-utf_8") == 0) {
        charset = "UTF-8";
    }else if (String::Compare(sw, "-utf8") == 0) {
        charset = "UTF-8";
    }else if (String::Compare(sw, "-euc-jp") == 0) {
        charset = "EUC-JP";
    }else if (String::Compare(sw, "-euc_jp") == 0) {
        charset = "EUC-JP";
    }else if (String::Compare(sw, "-eucjp") == 0) {
        charset = "EUC-JP";
    }else if (String::Compare(sw, "-euc") == 0) {
        charset = "EUC-JP";
    } else {
        Console::Error->Write(" 引数は-UTF_8と -EUC_JP のどちらか一方です。");
        return -1;
    }

    Encoding^ src = Encoding::Unicode;
    Encoding^ dst = Encoding::GetEncoding( charset );
    Stream^ output = Console::OpenStandardOutput();
    String^ line;

    while ((line = input->ReadLine()) != nullptr)
    {
        array<Byte>^temp = Encoding::Convert(src, dst, src->GetBytes(line + Environment::NewLine));
        output->Write(temp, 0, temp->Length);
    }
    return 0;
}

これを、cscnv.cpp なんて名前で保存して、こんなバッチファイルを作ってコンパイルします。VSVER=10.0 はVisual Studio 2010のことです。9.0 だと Visual Studio 2008 です。現在、既に評価版公開中の次期バージョンだと 11.0 です。

@echo off
set VSVER=10.0
set ARCH=x86
if %PROCESSOR_ARCHITECTURE% == x86 (
    set VCVARSALL_PATH="C:\Program Files\Microsoft Visual Studio %VSVER%\VC\vcvarsall.bat"
) else (
    set VCVARSALL_PATH="C:\Program Files (x86)\Microsoft Visual Studio %VSVER%\VC\vcvarsall.bat"
)
call %VCVARSALL_PATH% %ARCH%
cl /clr:safe cscnv.cpp

VC++ のツールメニューにあるVisual Studio のコマンドプロンプトを呼び出してから、ファイルのある場所に移動して、cl /clr:safe cscnv.cpp と入力してエンターでもいいです。

コンパイルが終わったら、コマンドプロンプトから、次のように打ち込んで、

echo こんにちは | cscnv –UTF8>test.txt

そのあと test.txt ファイルを開いて確かめます。うまくいっていれば、文字コードがUTF-8(BOM無し)のファイルができているはずです。

これで、python や perl などのコマンドを使わずに、shift-jis から、urf-8 や euc-jp への文字セットの変換ができるようになりました。

perl で一行で済ませられることを書くにしては、実質的な部分を比べても長いのは長いのですが、この基本形さえ押さえておけば、いろいろ応用できそうです。



posted by takayan at 12:46 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2012年04月22日

16進エスケープシーケンスにするフィルタ

今度は、SJIS以外の文書を読み込むとVC++が警告を出してしまう問題の対策をしてみます。

やろうとしていることは単純です。怒られそうな文字コードを16進数表記の文字リテラルに置き換えるだけです。シフトJISならば、2バイト文字の2バイト目に隠れている「 \ 」が表に出てきて、この記号にはいろいろな役割があるせいで、処理が大変ですが、EUC-JP の2バイト文字は 0x80よりも大きいものばかりで、UTF-8 の方も、1バイト文字以外は0x80以上の列になるので、どちらの文字セットでも0x80以上のものだけを16進数表記の文字リテラル(例えば\x80)にすることで、うまく解決できるはずです。

この問題に対しては、以前から escapize.py なんて名前の python スクリプトを対処していました。これもワンライナーで簡潔に表記できるなら、こんな別ファイルのスクリプトを書くより扱いやすいでしょう。

そういうわけで perl で書くと、バッチコマンド for の再帰探索を利用してこうなります。

for /R %%i in (*_%CHARSET%.h) do (
perl -i.backup -pe "s/(.)/(ord($1)>=128)?sprintf('\\x%%x',ord($1)):$1/eg" "%%i"
)

ビルド処理が終わったら .backup を元に戻します。perl が使える環境ならば、これで解決です。

 

今度は、perl は入っていないけれどVC++ は入れてある環境で考えてみます。 これも前回のように C++/CLI で作るならば、簡単に正規表現が使えるので、上と同じものを書けるだろう、と作る前は思ってました。でもそう簡単にはいきません。正規表現は Stringクラス、つまり Unicode が対象です。UTF-8 や EUC-JP の生の文字コードが扱えません。

そもそも正規表現を使うまでもない内容なので、それはすんなり諦めて、他にC++/CLI でやる方法を考えてみました。C++/CLI では指定すれば UTF-8 や EUC-JP で文書を読み込むことができると分かりましたが、それは String クラス(Unicode)に変換するためのものでした。今回のような場合はバイナリー配列として読み込む必要があるようです。実際いろいろ作ってみたら、キーボードからの入力だと思い通りにならなかったり、難しいので、結局、純粋な C++ のプログラムとして書くことにしました。

ということで、

#include <string>
#include <iostream>
int main()
{
while ( !std::cin.eof() )
{
std::string line;
std::getline(std::cin, line);
for ( int i = 0; i < line.length(); i++ )
{
unsigned char c = line[i];
printf( (c >= 128)?"\\x%02X":"%c", c );
}
std::cout << std::endl;
}
return 0;
}

このファイル名を csesc.cpp などとして、前回のようにビルド用の環境設定をやったあと、次のコマンドで実行ファイル csesc.exe ができあがります。

cl /EHsc csesc.cpp

前回のコマンドと組み合わせて、次のような感じで動作確認できます。

echo こんにちは|csesc
echo こんにちは|cscnv –utf8|csesc

結果:
\x82\xB1\x82\xF1\x82\xC9\x82\xBF\x82\xCD
\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF

こういう実験もできます。csesc.cpp に適当に日本語のコメントを入れて、UTF-8(BOM無し)かEUC-JPで保存してみます。ちなみに、BOM無しUTF-8やEUC-JPで保存できる無料のエディタは サクラエディタEmEditor Free などがあります。

これをコンパイルすると警告が出ます。そこで、このフィルタ・コマンドの登場です。

cl /EHsc csesc.cpp
csesc <csesc.cpp >csesc_new.cpp
cl /EHsc csesc_new.cpp

なお、今回はEUC-JPのファイルも対象だったので、この方法をとりましたが、これが UTF-8 のファイルだけだったら、ファイルの頭にUTF-8であることを示すBOM("\xEF\xBB\xBF")をくっつけても解決できます。

(修正 \xAA の形式で、 \の後ろの’x’は必ず小文字じゃないといけません。)



posted by takayan at 19:34 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2012年05月28日

Windows での mecab 0.993 python バインディングモジュール(32/64ビット)の作り方メモ

MeCab サイトでは、pythonバインディングのファイルを配布していますが、setup.py は Windows 環境向けには書かれていません。そこで、修正してVC++でコンパイルしたときのメモを書いておきます。以前も同じようなことを書きましたが、それはビルド環境が VC2003 でなければならない5年ほど前でした。

今回ビルドした環境は、Windows7で、32ビット版python2.7 と VC2008 がインストールされています。まず、python2.7 は http://www.python.org/ からダウンロードした通常の32bit版です。VC++はpython2.7では2008版が必要になります。

まず、Windows 向けの MeCab をインストールします。現在の最新版は0.993で、ファイル名は、mecab-0.993.exe です。下記のMeCabのダウンロードサイトからダウンロードし、インストールします。

http://code.google.com/p/mecab/downloads/list

インストール中、辞書の文字コードを聞いてきますが、文字コードは特に理由が無い限り WindowsではSHIFT-JISを選んだほうがいいでしょう。インストールが終わったら、mecab.dll があるフォルダまでPATHを通しておきます。

次に、同じダウンロードサイトから、python バインディングのソースファイルもダウンロードし展開します。最新版のファイル名は、mecab-python-0.993.tar.gz です。ダウンロードし、展開しておきます。

展開したファイルの中にあるsetup.py を次の内容に書き換えます。linux などでは、プログラム中でコマンドの実行結果を値にするのですが、Windows ではそのコマンドがないため、直接その結果を与えています。将来バージョンや配置先が変わった場合は、それに合わせて修正することになります。

#!/usr/bin/env python
from distutils.core import setup,Extension
if platform.machine() == 'AMD64':
    dir = 'C:/Program Files (x86)/MeCab/sdk'
else:
    dir = 'C:/Program Files/MeCab/sdk'
setup(name = 'mecab-python',
	version = '0.993',
	py_modules=['MeCab'],
	ext_modules = [
		Extension('_MeCab',
			['MeCab_wrap.cxx',],
			include_dirs=[dir],
			library_dirs=[dir],
			libraries=['libmecab'])])

次に、setup.pyがある場所に次の内容のバッチファイルを作って実行します。python のインストール場所などが違う場合は適宜書き換えます。

set MACHINE=x86
set PROGRAMFILESx86_PATH=%PROGRAMFILES%
if not "%PROCESSOR_ARCHITECTURE%" == "x86" set PROGRAMFILESx86_PATH=%PROGRAMFILES(x86)%
call "%PROGRAMFILESx86_PATH%\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" %MACHINE%
c:\python27\python setup.py bdist_wininst
echo %PROGRAMFILESx86_PATH%

うまくいけば、distフォルダの中に、mecab-python-0.993.win32-py2.7.exe というファイルができているはずです。mecab.dll までパスが通っていないときは、このままでは実行できません。パスを通すか、python インストールフォルダの Lib\site-packages にmecab.dll をコピーします。

動作テストは、mecab-python-0.993.tar.gz 内にある test.py を使います。ただし、mecab の辞書の文字コードとtest.pyが保存されている文字コードが同じでないといけません。またSHIFT-JIS以外のときはコマンドプロンプト画面では文字化けします。

こんな結果が出ます:

0.993
太郎	名詞,固有名詞,人名,名,*,*,太郎,タロウ,タロー
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
この	連体詞,*,*,*,*,*,この,コノ,コノ
本	名詞,一般,*,*,*,*,本,ホン,ホン
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
二	名詞,数,*,*,*,*,二,ニ,ニ
郎	名詞,一般,*,*,*,*,郎,ロウ,ロー
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
見	動詞,自立,*,*,一段,連用形,見る,ミ,ミ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
女性	名詞,一般,*,*,*,*,女性,ジョセイ,ジョセイ
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
渡し	動詞,自立,*,*,五段・サ行,連用形,渡す,ワタシ,ワタシ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。	記号,句点,*,*,*,*,。,。,。
EOS

 	BOS/EOS,*,*,*,*,*,*,*,*
太郎 	名詞,固有名詞,人名,名,*,*,太郎,タロウ,タロー
は 	助詞,係助詞,*,*,*,*,は,ハ,ワ
この 	連体詞,*,*,*,*,*,この,コノ,コノ
本 	名詞,一般,*,*,*,*,本,ホン,ホン
を 	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
二 	名詞,数,*,*,*,*,二,ニ,ニ
郎 	名詞,一般,*,*,*,*,郎,ロウ,ロー
を 	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
見 	動詞,自立,*,*,一段,連用形,見る,ミ,ミ
た 	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
女性 	名詞,一般,*,*,*,*,女性,ジョセイ,ジョセイ
に 	助詞,格助詞,一般,*,*,*,に,ニ,ニ
渡し 	動詞,自立,*,*,五段・サ行,連用形,渡す,ワタシ,ワタシ
た 	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。 	記号,句点,*,*,*,*,。,。,。
 	BOS/EOS,*,*,*,*,*,*,*,*
EOS
EOS
filename: C:\Program Files (x86)\MeCab\etc\..\dic\ipadic\sys.dic
charset: SHIFT-JIS
size: 392126
type: 0
lsize: 1316
rsize: 1316
version: 102

 

これだけだと面白くないので、64ビット版も作ってみました。

64ビット版のpython2.7を入れ直します。http://www.python.org/ から。最新版は、python-2.7.3.amd64.msi です。MeCab の方は、32ビット版の設定をそのまま流用したほうが楽なので、そのままインストールされているものとします。

モジュールをビルドするには、64ビット版の libmecab.lib が必要になるので、ソースからmecabをビルドします。ソースは上記のMeCabのダウンロードサイトからダウンロードしてきます。最新版 mecab-0.993.tar.gz です。

以前指摘したように、MeCab をWindowsの64ビットアプリケーションとしてコンパイルすると、型があいまいなせいでとエラーになるので、修正します。参考にしたのは以前も紹介したこのページです。今回のバージョン 0.993 での修正箇所は下のpatchに書いてある二カ所です。

diff -ru mecab-0.993.orginal/src/feature_index.cpp mecab-0.993/src/feature_index.cpp
--- mecab-0.993.orginal/src/feature_index.cpp	2012-02-11 00:35:10 +0900
+++ mecab-0.993/src/feature_index.cpp	2012-05-27 14:52:56 +0900
@@ -308,7 +308,7 @@
               if (!r) goto NEXT;
               os_ << r;
             } break;
-            case 't':  os_ << (size_t)path->rnode->char_type;     break;
+            case 't':  os_ << (unsigned int)path->rnode->char_type;     break;
             case 'u':  os_ << ufeature; break;
             default:
               CHECK_DIE(false) << "unknown meta char: " <<  *p;
diff -ru mecab-0.993.orginal/src/writer.cpp mecab-0.993/src/writer.cpp
--- mecab-0.993.orginal/src/writer.cpp	2012-02-11 00:35:10 +0900
+++ mecab-0.993/src/writer.cpp	2012-05-27 14:51:41 +0900
@@ -257,7 +257,7 @@
             // input sentence
           case 'S': os->write(lattice->sentence(), lattice->size()); break;
             // sentence length
-          case 'L': *os << lattice->size(); break;
+          case 'L': *os << (unsigned int)lattice->size(); break;
             // morph
           case 'm': os->write(node->surface, node->length); break;
           case 'M': os->write(reinterpret_cast<>

そのあと、Makefile.msvc を MinGW か CYGWIN を使って./configure を実行して Makefile.msvc を作り、さらに X64向けにほんの少し書き換えます。 Makefile.msvc.in と修正結果の差分は以下の通りです。

--- mecab-0.993.original/src/Makefile.msvc.in	Wed Nov 02 23:40:30 2011
+++ mecab-0.993/src/Makefile.msvc	Sun May 27 03:15:57 2012
@@ -3,10 +3,10 @@
 LINK=link.exe
 
 CFLAGS = /EHsc /O2 /GL /GA /Ob2 /nologo /W3 /MT /Zi /wd4800 /wd4305 /wd4244
-LDFLAGS = /nologo /OPT:REF /OPT:ICF /LTCG /NXCOMPAT /DYNAMICBASE /MACHINE:X86 ADVAPI32.LIB
+LDFLAGS = /nologo /OPT:REF /OPT:ICF /LTCG /NXCOMPAT /DYNAMICBASE /MACHINE:X64 ADVAPI32.LIB
 DEFS =  -D_CRT_SECURE_NO_DEPRECATE -DMECAB_USE_THREAD \
-        -DDLL_EXPORT -DHAVE_GETENV -DHAVE_WINDOWS_H -DDIC_VERSION=@DIC_VERSION@ \
-        -DVERSION="\"@VERSION@\"" -DPACKAGE="\"mecab\"" \
+        -DDLL_EXPORT -DHAVE_GETENV -DHAVE_WINDOWS_H -DDIC_VERSION=102 \
+        -DVERSION="\"0.993\"" -DPACKAGE="\"mecab\"" \
         -DUNICODE -D_UNICODE \
         -DMECAB_DEFAULT_RC="\"c:\\Program Files\\mecab\\etc\\mecabrc\""
 INC = -I. -I..

修正がすんだら、make.bat の内容を次のようにして実行します。64ビットOSだと分かっているのでシンプルです。

call "%PROGRAMFILES(x86)%\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" X64
nmake -f Makefile.msvc

できあがった libmecab.lib を setup.py と同じディレクトリに置き、setup.py のライブラリを指定する箇所を library_dirs=['.'] とし、上記の setup.py を実行するバッチファイルの一行目を set MACHINE=X64 と書き換えて、このバッチファイルを実行します。これでdistに mecab-python-0.993.win-amd64-py2.7.exe が作成されます。

しかし、mecab-python-0.993.win-amd64-py2.7.exe を実行し、インストールしただけでは使えません。このままだと、32ビット版の libmecab.dll をロードしようとして、「%1 は有効な Win32 アプリケーションではありません。」なんて言われてしまいます。これを回避するために 64ビット版 MeCab のビルドでできた libmecab.dll を python インストールフォルダの Lib\site-packages にコピーします。これで完了です。

うまくいくかどうかは、32ビットのときと同じように、test.py を実行して確認します。

なお今回の方法では標準の辞書は mecabrc のdicdir = 行に指定されている辞書になります。mecabrc は Makefile.mvsc で指定されている C:\Program Files (x86)\MeCab\etc もしくは C:\Program Files\MeCab\etc にあって、その記述をもとに C:\Program Files (x86)\MeCab\dic\ipadic もしくは C:\Program Files\MeCab\dic\ipadic になっています。

せっかくなので、今回作ったファイルを置いておきます。
mecab-python-0.993.win32-py2.7.exe
mecab-python-0.993.win-amd64-py2.7.exe
の二つの他、64ビット版の libmecab.dll とそのインポートライブラリなども入れておきます。

ダウンロード:mecab-python-0.993.win-build.zip
(MD5:88b64ac95c08181d84398ac9f2f9a36a)



posted by takayan at 01:00 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2014年02月09日

F#で数の読みを考える

今年はF#をやっています。関数型のプログラミングは何年も前から興味はあって、本も買ってはみたものの、なかなか先に進めませんでした。でもつい最近、自分でも気づかないうちに、目から鱗が落ちたみたいです。PythonやJavascriptも好きですが、これもなかなか楽しく作れます。

入門用のプログラムで動作を確認するのも飽きたので、去年Pythonで作った日本語の読み文字列を出力するプログラムをF#で作ってみようと考えました。全体はまだ無理ですが、整数の読みを出力するぐらいなら何とかできました。コンソールアプリケーションとして使えるようにしてみます。

まず、Pythonでのコード。以前のものから無駄なところを修正したものです。標準入力の数字列を読み込んで、位取りをしないときと、したときの両方の読みの文字列を出力します。

import sys

def yomi_raretsu_kazu(num):
    yomi = {
        u'0':u'レイ',
        u'1':u'イチ',
        u'2':u'ニー',
        u'3':u'サン',
        u'4':u'ヨン',
        u'5':u'ゴー',
        u'6':u'ロク',
        u'7':u'ナナ',
        u'8':u'ハチ',
        u'9':u'キュー',
    }
    if not num:
        return u''
    for n, y in yomi.items():
        num = num.replace( n, y )
    return num

def yomi_integer(num):

    if num == None or num == u'':
        return u''

    if num == u'0':
        return u'ゼロ'

    manshin_kurai = {
        0:u'',
        1:u'マン',
        2:u'オク',
        3:u'チョー',
        4:u'ケー',
        5:u'ガイ'
    }

    kazu_yomi = {
        u'0':u'',
        u'1':u'',
        u'2':u'ニ',
        u'3':u'サン',
        u'4':u'ヨン',
        u'5':u'ゴ',
        u'6':u'ロク',
        u'7':u'ナナ',
        u'8':u'ハチ',
        u'9':u'キュー',
    }

    list = []
    rank = 0
    rev  = num[::-1] + u'000'
    while len(rev) > 3:

        if rank >= len(manshin_kurai):
            return yomi_raretsu_kazu(num)

        if rev[0] == u'1':
           s0 = u'イチ'
        else:
           s0 = kazu_yomi[rev[0]]

        if rev[1] == u'0':
           s1 = u''
        else:
           s1 = kazu_yomi[rev[1]] + u'ジュー'

        if rev[2] == u'0':
            s2 = u''
        elif rev[2] == u'3':
            s2 = u'サンビャク'
        elif rev[2] == u'6':
            s2 = u'ロッピャク'
        elif rev[2] == u'8':
            s2 = u'ハッピャク'
        else:
            s2 = kazu_yomi[rev[2]] + u'ヒャク'

        if rev[3] == u'0':
            s3 = u''
        elif rev[3] == u'1' and len(num) != 4:
            s3 = u'イッセン'
        elif rev[3] == u'3':
            s3 = u'サンゼン'
        elif rev[3] == u'8':
            s3 = u'ハッセン'
        else:
            s3 = kazu_yomi[rev[3]] + u'セン'

        if s3+s2+s1+s0 != "":
            list.insert( 0, s3+s2+s1+s0 + manshin_kurai[rank] )
        else:
            list.insert(0,"")
        rev = rev[4:]
        rank += 1
    return ''.join(list)

if __name__ == '__main__':
    item = sys.stdin.readline().rstrip('\n')
    print( yomi_raretsu_kazu( item ))
    print( yomi_integer( item ))

これを参考にF#でもやってみます。F#では2つのプログラムにしてみます。

まず、位取りしない方です。

let yomi_raretsu_kazu str =
    String.collect ( fun ch ->
        match ch with
        | '0' -> "レイ"
        | '1' -> "イチ"
        | '2' -> "ニー"
        | '3' -> "サン"
        | '4' -> "ヨン"
        | '5' -> "ゴー"
        | '6' -> "ロク"
        | '7' -> "ナナ"
        | '8' -> "ハチ"
        | '9' -> "キュー"
        |  _  -> "" ) str

とてもシンプルです。Pythonでは辞書型を使って実現しましたが、F#では、文字をパターンマッチするラムダ式を作って、その結果を高階関数のcollectで連結しました。F#の特徴的なものを使って簡潔に書けました。

この関数をコンソールアプリケーションにしてみます。引数が1つの時はそれを変換し、引数が何もないときは標準入力から文字列を取り込みます。引数が2つ以上になるとエラーです。

let yomi_raretsu_kazu str =
    String.collect ( fun ch ->
        match ch with
        | '0' -> "レイ"
        | '1' -> "イチ"
        | '2' -> "ニー"
        | '3' -> "サン"
        | '4' -> "ヨン"
        | '5' -> "ゴー"
        | '6' -> "ロク"
        | '7' -> "ナナ"
        | '8' -> "ハチ"
        | '9' -> "キュー"
        |  _  -> "" ) str

[<EntryPoint>]
let main args =
    let routine line =
        if not (String.forall(fun x -> System.Char.IsDigit x) line) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        line
        |> yomi_raretsu_kazu
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintfn "機 能: 位取りのない数字列の読みを返します"
            -1

エントリーポイントとして関数mainを定義しています。これは先ほど作った関数の呼び出しと、例外処理からなっています。この呼出のあたりは、わざわざパイプライン演算子「|>」を使って書いてます。理屈さえ分かれば、括弧を使って記述するより、処理の流れが明快に表現できるところが、面白いです。

次は、位取りをする方です。それなりに複雑になりますが、パターンマッチで書くと、見た目にも明快です。

let yomi_integer num_str =
    let rec reverse = function
        | "" -> ""
        | x  -> x.[1..] |> reverse |> (+) <| string x.[0]
    let rec loop rank str =
        let manshin_kurai = function
            |  1 -> "マン"
            |  2 -> "オク"
            |  3 -> "チョー"
            |  4 -> "ケー"
            |  5 -> "ガイ"
            |  6 -> "ジョ"
            |  7 -> "ジョー"
            |  8 -> "コー"
            |  9 -> "カン"
            | 10 -> "セイ"
            | 11 -> "サイ"
            | 12 -> "ゴク"
            | 13 -> "ゴーガシャ"
            | 14 -> "アソーギ"
            | 15 -> "ナユタ"
            | 16 -> "フカシギ"
            | 17 -> "ムリョータイスー"
            | _ -> ""
        let kazu_yomi = function
            | '2' -> "ニ"
            | '3' -> "サン"
            | '4' -> "ヨン"
            | '5' -> "ゴ"
            | '6' -> "ロク"
            | '7' -> "ナナ"
            | '8' -> "ハチ"
            | '9' -> "キュー"
            |  _  -> ""
        match (String.length str > 3, manshin_kurai rank) with
        | (false, _    ) -> ""
        | (true,  ""   ) when rank > 1 ->
            failwith  <| "エラー: 桁が多すぎます: '" + num_str + "'"
        | (true,  kurai) ->
            let s0 =
                match str.[0] with
                | '1' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "イッ"
                    | _ -> "イチ"
                | '6' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ' ->  "ロッ"
                    | _ -> "ロク"
                | '8' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "ハッ"
                    | _ -> "ハチ"
                |  x  -> kazu_yomi x
            let s1 =
                match (str.[1], str.[0]) with
                | ('0',  _ ) -> ""
                | ( x , '0') when kurai <> "" ->
                    kazu_yomi x + match kurai.[0] with
                                  |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "ジッ"
                                  | _ -> "ジュー"
                | ( x ,  _ ) -> kazu_yomi x + "ジュー"
            let s2 =
                match (str.[2],str.[1], str.[0]) with
                | ('0', _,  _ ) -> ""
                | ('3', _,  _ ) -> "サンビャク"
                | ('6', _,  _ ) -> "ロッピャク"
                | ('8', _,  _ ) -> "ハッピャク"
                | ( x ,'0','0') when kurai <> "" ->
                    kazu_yomi x + match kurai.[0] with
                                  |'カ'|'ケ'|'コ' -> "ヒャッ"
                                  | _ -> "ヒャク"
                | ( x , _,  _ ) ->  kazu_yomi x + "ヒャク"
            let s3 =
                match str.[3] with
                | '0' -> ""
                | '1' when String.length num_str <> 4 -> "イッセン"
                | '3' -> "サンゼン"
                | '8' -> "ハッセン"
                |  x  ->  kazu_yomi x + "セン"
            loop <| rank + 1 <| str.[4..]
            |> (+) <| match s3+s2+s1+s0 with | "" -> "" | x -> x + kurai
    match num_str with
    | ""  -> ""
    | "0" -> "ゼロ"
    |  x  -> x |> reverse |> (+) <| "000" |> loop 0

[<EntryPoint>]
let main args =
    let routine line =
        let number =
            String.collect(fun x -> if x=',' then "" else string x) line
        if not(String.forall(fun x -> System.Char.IsDigit x) number) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        number
        |> yomi_integer
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintf "機 能: 整数文字列の読みを返します"
            -1

(高位における音便を考慮に入れていなかったので処理を追加、ついでにパターンマッチにタプルが使えることを覚えたので整理。2014/02/11修正)

これは、再帰関数の習作になりました。まず数値文字列を逆順にしたほうが添え字を桁の番号にできるので、反転用の関数reverseを作ります。これを再帰で定義します。Pythonほどではありませんが、文字列でスライスが使えるのは楽でいいです。ついでにパイプライン演算子を使って、括弧を使わない表記にしています。

次の関数loopも再帰です。日本語での数字の読みは万進法なので4桁ずつ区切って処理していますが、これを再帰を使って連結していきます。なお、繰り返しの最後で4桁に満たない可能性もあるので、この再帰関数を最初に呼び出す段階であらかじめ”000”をくっつけています。

中心になる関数yomi_integerそのものは数字だけしか受け付けませんが、入力文字列では桁区切りのコンマはあった方が見やすいので、それを呼び出す関数mainでその桁区切りのコンマを削除しています。

作り始めたばかりですが、この言語は面白いです。元々.NETの言語ですが、Monoのおかげで、Windows以外でも使えます。今回Visual Studioだけでなく、オープンソースのgithubにあるfsharpでも動作を確認しました。今までちょっとしたことはPythonで書いていましたが、しばらくはF#です。

参考リンク:



posted by takayan at 02:08 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2014年02月15日

F#で漢数字とアラビア数字の変換を考える

前回(F#で数の読みを考える)、アラビア数字からなる数値の読みを出力するプログラムをF#で考えてみました。今度は、アラビア数字の数値文字列と漢数字文字列の変換を考えてみます。

まず、アラビア数字から漢数字への変換は、前回の位取りのある数値の読みを出力するプログラムを修正することで簡単にできます。音便などを考える必要もありません。

※垓と穣の間の「ジョ」はこのブログでは表示できないので、二つの漢字をつなげて「禾予」と表示しています。適切に表示でき、実行できる環境ならば、これを本来の文字で置き換えてください。

let san2kan num_str =
    let rec reverse = function
        | "" -> ""
        | x  -> x.[1..] |> reverse |> (+) <| string x.[0]
    let rec loop rank str =
        let jusshin_kurai = function
            |  0 -> ""
            |  1 -> "十"
            |  2 -> "百"
            |  3 -> "千"
            |  _ -> ""
        let manshin_kurai = function
            |  1 -> "万"
            |  2 -> "億"
            |  3 -> "兆"
            |  4 -> "京"
            |  5 -> "垓"
            |  6 -> "禾予"
            |  7 -> "穣"
            |  8 -> "溝"
            |  9 -> "澗"
            | 10 -> "正"
            | 11 -> "載"
            | 12 -> "極"
            | 13 -> "恒河沙"
            | 14 -> "阿僧祇"
            | 15 -> "那由他"
            | 16 -> "不可思議"
            | 17 -> "無量大数"
            |  _ -> ""
        let kazu_yomi = function
            | '2' -> "二"
            | '3' -> "三"
            | '4' -> "四"
            | '5' -> "五"
            | '6' -> "六"
            | '7' -> "七"
            | '8' -> "八"
            | '9' -> "九"
            |  _  -> ""
        match String.length str > 3 with
        | false -> ""
        | true  ->
            match manshin_kurai rank with
            | "" when rank > 1 ->
                failwith  <| "エラー: 桁が多すぎます: '" + num_str + "'"
            | kurai ->
                let s = [|
                    for i in 0..3 ->
                        if (i = 0 || (i = 3 && String.length num_str<>4))
                            && str.[i] = '1'
                        then "一" + jusshin_kurai i
                        elif str.[i] = '0' then ""
                        else kazu_yomi str.[i] + jusshin_kurai i |]
                loop <| rank + 1 <| str.[4..]
                |> (+) <|
                match s.[3]+s.[2]+s.[1]+s.[0] with
                | "" -> ""
                | x -> x + kurai
    match num_str with
    | ""  -> ""
    | "0" -> "零"
    |  x  -> x |> reverse |> (+) <| "000" |> loop 0

[<EntryPoint>]
let main args =
    let routine line =
        let number =
            String.collect( fun x -> if x = ',' then "" else string x ) line
        if not ( String.forall( fun x -> System.Char.IsDigit x ) number ) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        number
        |> san2kan
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintf "機 能: 算用数字を漢数字に変換する"
            -1

以上のようになります。今回は各桁の値は配列に保持することにし、for文を使ったシーケンス式で値を計算しています。前回は説明していませんでしたが、百と十の位は値が一のときはその表記を省略しています。千は、万や億などに属する場合は省略せずに一千と書き、単純な千のときは省略するようにしています。

 

今度は、アラビア数字を漢数字の数値に変換する方法を考えてみます。

これは漢数字の階層構造に目を付けて考えていきます。漢数字に現れる単語は、重複するものもありますが、一つずつ増えていくものと、十倍ずつ増えていくものと、万倍ずつ増えていくものの三つのグループに分類できます。この三者の階層構造によって漢数字は組み立てられています。このことを踏まえて、再帰を使って記述すると、次のプログラムができます。

open System.Text.RegularExpressions

let convert_kan2san input =
    let separate input pattern =
        let m = Regex.Match(input, "(.*?)" + pattern + "(.*)")
        if m.Success then Some (m.Groups.[1].Value, m.Groups.[2].Value)
        else None
    let place_digit = function
        | ""   -> "1"
        | "一" -> "1"
        | "二" -> "2"
        | "三" -> "3"
        | "四" -> "4"
        | "五" -> "5"
        | "六" -> "6"
        | "七" -> "7"
        | "八" -> "8"
        | "九" -> "9"
        |  _   -> failwith "エラー:正規の漢数字ではありません:" + input
    let place_4digits skip data =
        let rec place_digits skip str rank =
            match [|"千";"百";"十";""|].[rank] with
            | "" ->
                if skip || str <> "" then place_digit str else "0"
            | kurai ->
                match separate str kurai with
                | Some (left, right) ->
                    (place_digit left) + place_digits false right (rank+1)
                | None ->
                    (if skip then "" else "0") + place_digits skip str (rank+1)
        place_digits skip data 0
    let rec place_digits skip str rank =
        match
            [|
            "無量大数";"不可思議";"那由他";"阿僧祇";"恒河沙";
            "極";"載";"正";"澗";"溝";"穣";"禾予";"垓";"京";"兆";"億";"万";""|].[rank] with
        | "" ->
            if skip || str <> "" then place_4digits skip str else "0000"
        | kurai ->
            match separate str kurai with
            | Some (left, right) ->
                (place_4digits skip left) + place_digits false right (rank+1)
            | None ->
                (if skip then "" else "0000") + place_digits skip str (rank+1)
    if input = "" || input = "零" then "0" else place_digits true input 0

[<EntryPoint>]
let main args =
    let routine line =
        line
        |> convert_kan2san
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintf "機 能: 漢数字を算用数字に変換します"
            -1

位を表す漢字を見つけるための関数として、separateを定義しています。戻り値の値をオプション型にし、見つかったときは位の文字の左と右の文字列からなるタプルを返します。見つからなかったときはNoneを返します。

ここでは再帰関数place_digitsを使って文字列を連結させながらの繰り返しを記述しています。この関数は十進の位と万進の位のそれぞれの処理用に同じ名前で別々に定義しています。最初のパターンマッチで位の配列が終わりかどうか判断させて、終わっていなければ次のパターンマッチで、先の関数separateを使い文字列中に現在対象としている位があるかどうか調べています。

この一連の処理で問題となるのが、位の文字が存在しないときの処理です。解析中の漢数字中の最高位の位が見つかるまでは、文字列中に現れない万進の位も十進の位も空文字列に変換されます。しかし最高位の位がどの万進と十進の位の組み合わせかが確定すると、今度は文字列中に現れない万進の位は”0000”、現れない十進の位は”0”に変換されます。つまり最高位が見つかったかどうかを示すものがどうしても必要になります。この状態を示す変数として、再帰で渡され続ける引数skipを使っています。

 

上記の十進の位と万進の位についての再帰を使った繰り返しは、ほとんど同じ処理をしています。これはクラスを使って一つにまとめることができます。まだ使ったことのないクラスを使う練習として参考までに書いてみます。

open System.Text.RegularExpressions

type Converter( array, func, str ) =
    let ranks : string [] = array
    let subconvert = func
    let padding = str
    member this.place_digits(skip, str, rank) =
        match ranks.[rank] with
        | ""    ->
            if skip || str <> "" then subconvert skip str else padding
        | kurai ->
            let separate input pattern =
                let m = Regex.Match(input, "(.*?)" + pattern + "(.*)")
                if m.Success then Some (m.Groups.[1].Value, m.Groups.[2].Value)
                else None
            match separate str kurai with
            | Some (left, right) ->
                (subconvert skip left) + this.place_digits(false, right, rank+1)
            | None ->
                (if skip then "" else padding) + this.place_digits(skip, str, rank+1)

let convert_kan2san input =
    let rank_ju  = [|"千";"百";"十";""|]
    let rank_man = [|
        "無量大数";"不可思議";"那由他";"阿僧祇";"恒河沙";
        "極";"載";"正";"澗";"溝";"穣";"禾予";"垓";"京";"兆";"億";"万";""|]
    let place_digit skip = function
        | ""   -> "1"
        | "一" -> "1"
        | "二" -> "2"
        | "三" -> "3"
        | "四" -> "4"
        | "五" -> "5"
        | "六" -> "6"
        | "七" -> "7"
        | "八" -> "8"
        | "九" -> "9"
        |  _   -> failwith "エラー:正規の漢数字ではありません:" + input
    let place_4digits skip data =
        let c = Converter( rank_ju, place_digit, "0" )
        c.place_digits( skip, data, 0 )
    let c = Converter( rank_man, place_4digits, "0000" )
    if input = "" || input = "零" then "0" else c.place_digits( true, input, 0 )

処理を共通にするために関数place_digitに意味のない引数skipを持たせます。思いのほか、クラスを作るのも使うのも難しくはありませんでした。ただ今回はクラスを作ることで、かえって処理の流れが見えにくくなってしまったようです。



posted by takayan at 14:07 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする
×

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