この記事について
この記事は、Qiita Advent Calendar 2021「Rubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念-」の 2 日目の記事です。何も考えずに空いているところに入れたら改訂版チェリー本の発売日でした。記念すべき日を私ごときが取ってしまい申し訳ありません…
方針
まずタイトル(はいそうですジョジョ4部です)にある通り、点の有無で情報を表す「点字」は、「2進法」と非常に相性がいいのではないでしょうか。点の有無をそのまま 1・0 として置き換えることで、点字の 1 文字 1 文字を 2 進法の数として表すことができます。
そこで、直接ローマ字を点字に変換するのではなく、まずは点字に対応する 2 進法の数値(の配列)に変換し、それを点字に変換するという 2 段階を踏んで変換をしようと考えました。
ローマ字を「点字に対応する 2 進法の数値」に変換する
まず、与えられたローマ字を点字に対応する 2 進法の数値に変換する処理です。一番シンプルな方法は以下のように、音節と それに対応する 2 進法の数値の間の変換テーブルを用意するものでしょう。Ruby には 2 進数値リテラル がありますから、それを使えば点字との対応関係を見やすい形でテーブルを作成できます。
TRANS_TABLE = {
'A' => 0b100000,
'I' => 0b110000,
'U' => 0b100100,
'E' => 0b110100,
'O' => 0b010100,
'KA' => 0b100001,
# 以下略
}
しかしこの方法では、いわゆる 50 音(実際は濁音等もあるのでそれ以上)すべてに対して対応する数を書く必要があります。これはなかなか面倒な作業です。
ところで点字は基本的に、ある意味ローマ字のように子音と母音の組み合わせで作られています。例えば下のように、「き(KI)」は「か行(K)」の⑥点と「い(I)」の①②点との組み合わせで作ることができます。
…これは、|
によるビット論理和演算で実現できるのでは?1
#↓か行(K) ↓い(I) ↓き(KI)
0b000001 | 0b110000 == 0b110001
というわけで、この方法では変換できない特殊なパターン(やゆよわをんっー。
)のみ例外的な処理をする必要がありますが、それ以外はビット演算を使ってやっていきます。
この方法の面白いところは、拗音です。点字の拗音はひらがなやローマ字での書き方とはかなり異なる(無理やりひらがなで説明するなら、「ぎゃ」「ぎゅ」「ぎょ」が「★゛か」「★゛く」「★゛こ」みたいなのになる。)にも関わらず、この方法を使えば自然に扱うことができるのです。
# ↓ G ↓ Y ↓ A ↓ぎゃ
0b000010_000001 | 0b000100_000000 | 0b100000 == 0b000110_100001
実装解説
binaries = text.split.map do |syllable|
EXCEPTION_TABLE[syllable] || syllable.chars.map { |char| TRANS_TABLE[char] }.inject(:|)
end
binaries.map { _1 > 0b111111 ? [_1 >> 6, _1 & 0b111111] : _1 }.flatten
まず、与えられたローマ字文字列を String#split で空白区切りで分割し、その各音節部分 syllable
が例外テーブル EXCETION_TABLE
にあればその値を使います。
例外テーブルに無かった場合はその音節を String#chars で 1 文字ごとに分解し、変換テーブル TRANS_TABLE
で変換し、inject(:|)
で論理和をとります。(参考:Enumerable#inject)ところで (:|)
の部分が可愛いと思うんですがどうでしょう。
最後に、1 音が点字では 2 文字になる場合があるので、その場合(0b111111
すなわち 63
より大きい数が得られた時)は [_1 >> 6, _1 & 0b111111]
で上位・下位 6 ビットずつに分割し、最後に Array#flatten で平坦化しています。
「点字に対応する 2 進法の数値」を、点字に変換する
この処理は、さらに「1つの『点字に対応する 2 進法の数値』を点字に変換する」「点字の 1 文字 1 文字の配列を、1 つの文字列に連結する」という処理に分かれています。
1つの「点字に対応する 2 進法の数値」を点字に変換する:実装解説
解説のために改行を入れています。
format("%06b", binary)
.tr("01", "-o")
.chars.each_slice(3).to_a
.transpose
.map(&:join).join("\n")
- まず Kernel.#format を使って、数値を 6 桁の 2 進文字列に変換します。
- さらに String#tr を使って、「
0
・1
」を「-
・o
」に変換します。 - それを String#chars で 1 文字ごとに分解した配列を作り、
each_slice(3).to_a
(参考:Enumerable#each_slice) でさらに 3 文字ごとに分けます。 - Array#tranpose で、行と列を入れ替えます。
-
Array#join で、内側の配列はそのまま、外側の配列は
"\n"
を間に挟んで連結します。
点字の 1 文字 1 文字の配列を、1 つの文字列に連結する:実装解説
解説のため、少し実際のコードとは変更しています。
tenji_array.map { _1.lines(chomp: true) }
.transpose
.map { _1.join(" ") }.join("\n")
- 点字文字列を、String#lines で行ごとに分解します。
- Array#transpose で、行と列を入れ替えます。
- それぞれの行では
join(" ")
で空白を間に挟んで連結し、行間にはjoin("\n")
で改行を挟んで連結します。
おまけ
ところで点字は 6 つの点からなるので、可能な点の組み合わせは $ 2^6 = 64 $ 通りです。2
64 と言えば…そう、BASE64ですね!「点字」と「BASE64」ッ!この世にこれほど相性のいいものがあるだろうかッ!?
というわけで、点字に対応した BASE64 文字列を生成する機能もつけました。一体どのような利用価値があるのか皆目見当がつきませんが!
TenjiMaker.new.to_base64('A HI RU') # => g5m
補足
「仕様を簡単にするため、以下のローマ字は対応不要です。」とのことでしたが、一通り不要と言われたローマ字にも対応しました。ただし促音「っ」についてはどのような入力を想定すればよいのかわからなかった3ので、XTU
を「っ」扱いにさせてもらいました。また、長音・句点は -
.
が入力されるものとしました。
なお、異常系への考慮はしていません。
伊藤さんにメッセージ
プロを目指しているわけではなかったので実はチェリー本を買っていなかったのですが、評判を聞くと趣味レベルの私のような人間にも充分役立ちそうですし、私がプロを目指す人に教える機会もあると思うので、改訂版が出るのを機にやはり買うことにしました。というわけで本日発売ですね!買ってきます!
12/02 追記
私なりに工夫したつもりだったのですが、初日の @cielavenir さんの記事を見たら結構似たようなことをしていました。(まずは 0~63 の数値にするところとか、transpose
使ってるところとか。)
同じものを作る以上、どうしても必然的に似てしまうのかもしれませんね。…必然!そうコーラを飲んだらゲップが出るっていうくらい必然じゃッ!