最初に
本記事は「改訂版・チェリー本発売記念の企画によるアドベントカレンダー」の11日目の記事で、点字プログラムの問題を解いてます。
詳細は、次のリンク先をご覧下さい。
自分のRuby歴について簡単に紹介すると、以前は下記のようなことをしてました。
- チェリー本の輪読会にときどき参加
- Rubyで競技プログラミング
- Railsアプリをちょっと作成
で、最近はRubyを書いてなかったのですが、
この企画で自分の書いたコードを見たいというメッセージを自分に送ってくださった方がいて、
企画を読んでみると面白そうだしRubyのリハビリがしたく、今回はやってみることにしました。
プルリクエストのリンク、自分が書いたコード
下記リンク先が、実際のPRです。
作ったコードですが、
濁音・半濁音など一切対応しておらず、
課題の要件に対して最小限の実装で済ませました!
PRと同じ内容ですが、コードも下記に貼っておきます。
class TenjiMaker
TENJI_TABLE = {
A: '1', I: '12', U: '14', E: '124', O: '24',
KA: '16', KI: '126', KU: '146', KE: '1246', KO: '246',
SA: '156', SI: '1256', SU: '1456', SE: '12456', SO: '2456',
TA: '135', TI: '1235', TU: '1435', TE: '12435', TO: '2435',
NA: '13', NI: '123', NU: '143', NE: '1243', NO: '243',
HA: '136', HI: '1236', HU: '1436', HE: '12436', HO: '2436',
MA: '1356', MI: '12356', MU: '14356', ME: '124356', MO: '24356',
YA: '34', YU: '364', YO: '354',
RA: '15', RI: '125', RU: '145', RE: '1245', RO: '245',
WA: '3', N: '356'
}.transform_keys(&:to_s).freeze
def to_tenji(text)
cells = text.split.map do |sound|
dots = %w[1 2 3 4 5 6].map do |dot_number|
TENJI_TABLE[sound].include?(dot_number) ? 'o' : '-'
end
[
dots[0] + dots[3],
dots[1] + dots[4],
dots[2] + dots[5]
]
end
cells.transpose.map{ |row| row.join(' ') }.join("\n")
end
end
ロジックの解説
概要
変換手段
何かしら「ローマ字綴りの音」を「点字」に変換する手段が必要です。
今回は、五十音ごとの、音と点字(点を打つ位置の数字)の情報の対応付けを、
Hashの定数TENJI_TABLE
に持たせました。
処理の大まかな流れ
短いのでコードだけ見た方が理解が早いのではという主観がありますが、
ざっくりコードの補助として流れを記述します。
- 入力(引数)で、ローマ字の文字列がある。
'KA N ……'
- 音ごとの要素に分離して、マスが要素の点字の配列を作る。2次元配列を作る。
-
split
で、音ごとに分離して配列に。'KA N ……'
を['KA', 'N', ……]
に。 - それぞれの音の、点字の点ごとに打つ打たないを
'o'
と'-'
で表現する。-
TENJI_TABLE
で、音を点を打つ位置に変換。'KA'
なら'16'
に。 - 点を打つ位置がわかっているので、実際に表示する
'o'
と'-'
に変換する。
'16'
なら['o', '-', '-', '-', '-', 'o']
-
- 表示の準備で、各マスを1行目・2行目・3行目の配列にする。点字の1マスが出来る。
['o', '-', '-', '-', '-', 'o']
なら['o-', '--', '-o']
とする。
全体で2次元配列cells
ができる。中身は次のような形。
[['o-', '--', '-o'], ['-o', '-o', '-o'], ……, ]
- 表示のために、
tranpose
でマス単位の配列から行ごとの配列に転置。
[['o-', '-o', ……], ['--', '-o', ……], ['-o', '-o', ……]]
- 2次元配列のデータを文字列になるように、それぞれ
join
で結合。
細かい処理について
定数のキーについて
TENJI_TABLE = {
A: '1', I: '12', U: '14', E: '124', O: '24',
KA: '16', KI: '126', KU: '146', KE: '1246', KO: '246',
# ~中略~
WA: '3', N: '356'
}.transform_keys(&:to_s).freeze
定数のHashのキーをtransform_keys
でクラスを変えるのは邪道な気もしますが、
キーをシンボルで書くリテラルが1番スッキリするし書きやすいし、
逆に1周まわってこうやってtransform_keys
を使うのは上級者っぽいと納得してます。納得しましょう!
なお、実は、もともと入力側の文字列をシンボルに変換してたのですが、
その全ての文字列をいちいちシンボルに変換するのが格好悪く感じて、
定数側を文字列に揃えた方がいいと思って今の形になりました。
もし入力が本当に膨大な場合は、いちいちシンボルにすると実行時間にちょっと影響がでかねないですし、そういう風に感じられるコードがイヤで……。
出力部分のtranspose
について
音ごとに並んでいるデータですが、点字の1音は3行にまたがり、
1行目の音ごと、2行目の音ごと、3行目の音ごとに並べ替えて出力する必要があります。
これは、2次元配列でデータを持ってtranspose
で転置させると楽そうだったので、そうしました。
cells.transpose.map{ |row| row.join(' ') }.join("\n")
終わってから他の方のコードも拝見しましたが、
この部分はほとんど同じ処理を書いてる人が何人もいました。
自分より先に公開してる10日までの中で、
12/2, 12/6, 12/8, 12/10はtranspose
を使っていてほぼ同じでした。
行き着くところは、皆同じっぽいです。
詳細な説明は、他の方がたくさん書いて下さってるので、割愛します!
コードのアピールポイント
頑張ったところ
頑張った実装方針
最初は、実装方針をどうしようかとかなり考えました。
メインの実装方針は大きく2つあると思っていて、どっちが楽かかなり悩みました。
- 五十音それぞれに点字の情報を持たせる。
- 母音、子音それぞれに点字の情報を持たせ、合成するようにする。
※ こっちは、や行、「わ」、「ん」についてイレギュラーな対応をする必要あり。
前者のように五十音表を作るのはわかりやすいけれど、
後者のように母音と子音を別々に点字との対応表を作った方がプログラマっぽいのではないかと。
しかし、母音・子音を別々に情報を持たせて作ろうとすると、
や行・わ・んのイレギュラー対応をしなければならないし、
母音と子音に分ける等の何らかの作業(正規表現とか)が増え、
メインのコードで書く処理が増えて面倒そうだと自分は思いました。
五十音それぞれの点字の情報を持たせる場合には、どうするか。
CSVやJSONの形で持たせるか? それともHashか。
点字の情報はどのように持たせるか? "oo---o"
の文字列か、["oo", "--", "-o"]
の配列か、'126'
の文字列か、126
の数値か、[1, 2, 6]
の配列か、それとも2進数0b110001
か?
色々な案を思い浮かべたものの、
五十音ごとの点字のテーブル(Hash)を作ることにしました。
CSVやJSONでシンプルなコードを書けると考えられず、まずは1ファイルで簡単に作ろうという気持ちでした。
また、文字列の形式にしました。
'126'
タイプの文字列は、すぐ書けて短くまとまって見やすいはずと感覚的に思いました。
126
と数値で書く案もありましたが、本来計算するものではなくやり過ぎだと思いやめました。
なお、もし自分が点字を書いて目で読むことができるタイプの人間なら、実際の点字に近い表記で書いてたと思います。
TENJI_TABLE = {
A: '1', I: '12', U: '14', E: '124', O: '24',
KA: '16', KI: '126', KU: '146', KE: '1246', KO: '246',
SA: '156', SI: '1256', SU: '1456', SE: '12456', SO: '2456',
# ~中略~
WA: '3', N: '356'
}.transform_keys(&:to_s).freeze
やる前は「どれぐらいかかるんだろう? 大変かな?(他よりはマシか)」と少しネガティブな気持ちでしたが、
やってみるとかなり簡単で、ストレスなくすぐ作れました。
あ行を作ったら、それをコピペでベースにして、他のか行・さ行……を作っていくだけでした。
行(子音)ごとに点字の打つ位置が決まっているのもあって、あ段を1個作ったら残りの4個もコピペで済んだので楽でした。
縦に見ればイ段の打つ位置は'12'
だとわかるし、
横に見ればサ行の打つ位置は'456'
だとわかり、
他の形式に比べて間違い等を見つけやすい表になったように思います。
1音ごとの点字の対応表を作るのはやってみると想像以上に簡単で、
イレギュラーに対応する必要がないので条件分岐もなく、
短く簡潔でスタイリッシュなコードが出来上がったと思います!
それに、コードを作ったあとに何人かのコードを拝見しましたが、
パッと見で母音と子音と例外の対応を作っている方しかいませんでした。
だから、企画の中でいい差別化になったと思います!
頑張った変数名
変数名は、自分なりに気を使いました。
例えば、点字は6点で1セットですが、この1セットが要素の配列名を何にしようか迷いました。
配列だから、できれば変数名は複数形がいいな〜と。
6点をdots
にしたら、6点の集合はsets_of_dots
? これだと少し長い……。
調べると、日本語では点字の6点を「マス」というらしいです。
さらに少し調べると、英語では"Cell"っていうみたい(違ってたら、教えて下さい!)。
変数名にcells
を使ったら、コードが引き締まって良くなりました。
初見だと「cells
って何だ?」ってなる気がしますが。
ちなみに、変数名で悩まないようにメソッドチェーンにする案もありましたが、
チェーン内に間に10行ほどの大きめなブロックを挟むことになりやっぱり不格好かなと思いやめました。
自慢したいところ
問題の要件を満たす最小限に作り、
コードは短くシンプルに出来たと思ってます。
それから、次の部分について、
[
dots[0] + dots[3],
dots[1] + dots[4],
dots[2] + dots[5]
]
この部分が1音の点字のマス(6点)ですが、
実際の点字の位置関係と合っててオシャレに出来たって思ってます!
やめたところ: Tenjiクラス
余談ですが、最初はto_tenji
というメソッド名だから、
Tenji
クラスを作って返すメソッドにしようと思いました。
to_s
がString
, to_i
がInteger
を返すように。
実際、最初はそう作ったのですが、テストで"
がつくつかないで失敗してしまい、
考え抜きたいところでもなかったし、コードもスッキリさせたかったので、
Tenji
クラスは消滅させました。
ポジティブに捉えれば、短くてシンプルな実装になりました!
伊藤さんにメッセージ
改訂版・チェリー本の発売おめでとうございます!
また、面白い企画、ありがとうございました。
FBCにいたけど、伊藤さんから直接レビューされた記憶はなく、御手柔らかにお願いします!
レビューを受けて、追記
Hashのリテラルの中で「N(ん)」がズレてるのは、イ段と並べないようにしたためです!
最後に
最近はあまりコードを書けてなかったので、書けて良かったです!
やる前は「みんな、似たようなコードになるのでは」と思いましたが、
考え始めると「色々なパターンがありそうだ」となったし、
実際に自分のコードは他の方と異なる方針だったし、
自分らしいコードが書けて良かったです! ありがとうございました!!