Ruby には nkf コマンドを利用するための NKF というライブラリがあります。
文字コードを変換する関数 NKF.nkf オプションは、nkf コマンドのオプションと同じものを文字列で指定します。
# -W 入力に UTF-8 を仮定する # -s Shift_JIS を出力する # -Lw 改行として CRLF を出力する # --katakana-hiragana 片仮名を平仮名に、平仮名を片仮名に変換する NKF.nkf('-W -s -Lw --katakana-hiragana', 'ソフトウェア的愛情') #=> "\x{82BB}\x{82D3}\x{82C6}\x{82A4}\x{82A5}\x{82A0}\x{9349}\x{88A4}\x{8FEE}"
$ echo ソフトウェア的愛情 | nkf -W -s -Lw --katakana-hiragana | hexdump -C 00000000 82 bb 82 d3 82 c6 82 a4 82 a5 82 a0 93 49 88 a4 |.............I..| 00000010 8f ee 0d 0a |....| 00000014
これを演算子を使って簡便に記述できないか、と考えてみたのがきっかけでした。
演算子を使ったオプションの組み立てのアイディア
「演算子を使って」というのがどういうことかというと。 例えば UTF-8 から Shift_JIS への変換であれば、こんなふうに書けたらということです。
utf_8 >> sjis
オブジェクトから見れば演算子も 1 引数のメソッドです。
class Foo def initialize(lhs, rhs) # ... end end class Bar def >>(rhs) Foo.new(self, rhs) end end bar1 = Bar.new bar2 = Bar.new bar1 >> bar2 #=> Foo.new(bar1, bar2)
これを踏まえてオプションを組み立てる方法を考えます。
入力エンコーディングと出力エンコーディングを組み立てる
#>> の左側が入力を右側が出力を表すので、レシーバのオブジェクトが入力の指定になり引数のオブジェクトが出力の指定になります。
ここでの実装では、各々のオブジェクトが入力の指定と出力の指定を自分で知っていて、#>> の中でどちらを利用するか選択することにしました。
演算の結果として生成されるオブジェクトに #to_s を定義し、文字列として表示できるようにしています。
class NKF_OPT def initialize(input, output) @input = input @output = output end def to_s "#{@input} #{@output}" end Encoding = Data.define(:input, :output) do def >>(output) NKF_OPT.new(input, output.output) end end end
実行。
sjis = NKF_OPT::Encoding.new('-S', '-s') euc_jp = NKF_OPT::Encoding.new('-E', '-e') utf_8 = NKF_OPT::Encoding.new('-W', '-w') puts utf_8 >> sjis #=> -W -s puts sjis >> euc_jp #=> -S -e
入力と出力を指定できるようになりました。
そのほかのオプションを指定する
エンコーディングの他にもオプションが複数あります。
これらのオプションは #+ を使って追加できるようにしてみます。
まずエンコーディング以外のオプションは @options に格納するようにします。
def initialize(input, output, options = []) @input = input @output = output @options = options end
オプション文字列をオブジェクトとして扱えるようにデータ型を追加します。
Option = Data.define(:option) do def to_s option end end
次に #+ を定義してオプションを追加できるようにします。
ここで self を返すことで複数のオプションを #+ で次々に連結して指定できるようにしています。
def +(option) @options << option self end
オプションが出力されるように #to_s も修正します。
def to_s [@input, @output, *@options.map(&:to_s)].join(' ') end
コード全体は次のようになります。
class NKF_OPT def initialize(input, output, options = []) @input = input @output = output @options = options end def +(option) @options << option self end def to_s [@input, @output, *@options.map(&:to_s)].join(' ') end Encoding = Data.define(:input, :output) do def >>(output) NKF_OPT.new(input, output.output) end end Option = Data.define(:option) do def to_s option end end end
実行。
sjis = NKF_OPT::Encoding.new('-S', '-s') utf_8 = NKF_OPT::Encoding.new('-W', '-w') crlf = NKF_OPT::Option.new('-Lw') puts (utf_8 >> sjis) + crlf #=> -W -s -Lw
追加のオプションを指定できるようになりました。
しかしここで >> よりも + の方が結合が強いため、utf_8 >> sjis + crlf と記述すると utf_8 >> (sjis + crlf) と解釈され、期待する結果が得られません。
puts utf_8 >> sjis + crlf
#=> undefined method '+' for an instance of NKF_OPT::Encoding (NoMethodError)
次にこれを改善します。
演算の順序を制御する
とはいえ演算の結合の順序は Ruby の言語仕様として定義されているので変更することはできません。
utf_8 >> (sjis + crlf) の解釈を受け入れた上で、最終的に期待する出力を得られるようにすることを目指します。
ここでは Encoding にもオプションを格納できるようにすることにします。
冗長になりますが実現が簡単な方法を選びました。
まず Encoding もオプションを格納できるようにします。
Encoding = Data.define(:input, :output, :options) do def initialize(input:, output:, options: []) super end # ...
同様に #+ も定義し、オプションを追加したオブジェクトを作成できるようにします。
# ... def +(option) self.class.new(input, output, options + [option]) end end
ここで Encoding オブジェクトを変更するような実装にしてしまうと、そのオブジェクトが指定するオプションの内容が変わってしまい都合がよくありません。
# sjis を変更できてしまうと… sjis + crlf #=> sjis オブジェクトに crlf の内容が追加されてしまう sjis + lf #=> sjis と crlf と lf が指定されたことになる
このため新しいオプジェクトを作成して返すようにしています。
コードの全体は次のようになりました。
class NKF_OPT def initialize(input, output, options = []) @input = input @output = output @options = options end def +(option) @options << option self end def to_s [@input, @output, *@options.map(&:to_s)].join(' ') end Encoding = Data.define(:input, :output, :options) do def initialize(input:, output:, options: []) super end def >>(output) NKF_OPT.new(input, output.output, options + output.options) end def +(option) self.class.new(input, output, options + [option]) end end Option = Data.define(:option) do def to_s option end end end
実行。
sjis = NKF_OPT::Encoding.new('-S', '-s') utf_8 = NKF_OPT::Encoding.new('-W', '-w') crlf = NKF_OPT::Option.new('-Lw') puts utf_8 >> sjis + crlf #=> -W -s -Lw
括弧が不要になりました。
これまでの書き方も引き続き有効です。
puts (utf_8 >> sjis) + crlf #=> -W -s -Lw puts utf_8 >> sjis #=> -W -s
追加のオプションも + で連結して指定できます。
katakana_hiragana = NKF_OPT::Option.new('--katakana-hiragana') puts utf_8 >> sjis + crlf + katakana_hiragana #=> -W -s -Lw --katakana-hiragana
NKF.nfk のオプションとして利用する
組み立てた文字列を実際に NKF のオプションとして利用してみます。
式のまま NKF.nkf の引数に渡したいところですが、今のままでは直接渡すことはできません。
require 'nkf' NKF.nkf(utf_8 >> sjis + crlf + katakana_hiragana, 'ソフトウェア的愛情') #=> no implicit conversion of NKF_OPT into String (TypeError)
文字列に変換する必要があります。
NKF.nkf((utf_8 >> sjis + crlf + katakana_hiragana).to_s, 'ソフトウェア的愛情') #=> "\x{82BB}\x{82D3}\x{82C6}\x{82A4}\x{82A5}\x{82A0}\x{9349}\x{88A4}\x{8FEE}"
そして Ruby には文字列への暗黙の変換を可能にする仕組みが用意されています。
文字列への変換としてすでに #to_s を定義しているので、#to_str はこれのエイリアスとすることにします。
class NKF_OPT # ... alias to_str to_s # ... end
これで式を NKF.nkf のオプションとして渡すことができるようになりました。
NKF.nkf(utf_8 >> sjis + crlf + katakana_hiragana, 'ソフトウェア的愛情') #=> "\x{82BB}\x{82D3}\x{82C6}\x{82A4}\x{82A5}\x{82A0}\x{9349}\x{88A4}\x{8FEE}"
おまけ
ここまで来たのなら、オプションの式に文字列を流し込んで変換できるようにしてみます。
#<< を定義してその中で NKF.nkf を呼び出すようにしてみます。
class NKF_OPT # ... def <<(string) NKF.nkf(self, string) end # ... end
これでオプションで組み立てた式に << で文字列を流し込むことで変換できる、変態的な記述が可能になりました。
utf_8 >> sjis + crlf + katakana_hiragana << 'ソフトウェア的愛情' #=> "\x{82BB}\x{82D3}\x{82C6}\x{82A4}\x{82A5}\x{82A0}\x{9349}\x{88A4}\x{8FEE}"
余談
Factor に触れたとき、例えば数値を文字列へ変換する関数が number>string という名前になっていることを知りました。
他の言語ではあまり見かけない名前です。
number>string ( n -- str ) - Factor Documentation
10 number>string
このとき同時に頭に浮かんだのが、C++ の Boost でした。 Boost のパーサライブラリは演算子を使った変態ライブラリとして有名です。
group = '(' >> expression >> ')'; factor = integer | group; term = factor >> *(('*' >> factor) | ('/' >> factor)); expression = term >> *(('+' >> term) | ('-' >> term));
(ドキュメントから抜粋)
すぐに頭に浮かんだのですから「演算子を再定義してうまいことやる」機会がないか、どこかで思っていたのでしょう。
思いつきで書いたので、この記事を書くにあたって読み返してみると色々と拙いところが目につくんですが、意外に悪くないかもという気分になっています。
いつか読むはずっと読まない:Big 5
古生物学にはなぜか惹かれます。 今とは違う姿の生物たちが気になるのか、あるいは姿を変えつつも生命が現在まで繋がっていることが気になるのか。
