この記事はアドベントカレンダー「Rubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念- Advent Calendar 2021」の19日目の記事として作成しました。
#目次
1.はじめに
2.ソースコード
3.ロジックの解説
4.コードのアピールポイント
5.苦労したところ
6.さいごに
#1. はじめに
kazkuraと申します。初Qiita記事投稿、初アドベントカレンダー参加です。至らぬ点が多いかと思いますが、何卒ご容赦いただけますと幸いです。
簡単に自己紹介ですが、回路設計・EMC設計という仕事をしており、プログラミングは趣味でやっております。Rubyについては、逐次ググって使い方を覚えた、「我流な」プログラマとなります。
そんな人間でも、今回のお題にちゃんと答えることができるのかやってみた、という内容になります。その結果が以下となります。
#2. ソースコード
Ruby の version は、3.0.2p107 です。
#3. ロジックの解説
大まかに以下の流れで処理しています。
- 入力されたローマ字をスペースで分割する
- 分割したローマ字を「点字を表す2進数」に変換する
- 2進数を点字の文字列に変換する
###1) 入力されたローマ字をスペースで分割する
ローマ字が記入された文字列を、Stringクラスのsplitメソッドを使って、1文字のローマ字に分割して配列に入れました。
text_list = text.split(' ')
文字列が1文字だけの場合、スペースが無いので、splitを実行した結果が、配列ではなく文字列のままだったらどうしようと思いました。この後の処理で、eachメソッドを使って各配列要素に対して2進数値に変換しようと思っていたからです。ところが、下記のように、ちゃんと配列になっていました。とても賢いですね。
irb(main):001:0> text = 'A'
=> "A"
irb(main):002:0> text_list = text.split(' ')
=> ["A"]
irb(main):003:0> text_list.class
=> Array
2) 分割したローマ字を「点字を表す2進数値」に変換する
点字は、下記のような 2×3 の行列でできていて、それぞれ①~⑥の番号が振られています。
① ④
② ⑤
③ ⑥
今回は、ローマ字から点字に変換するために、2進数値と論理演算を使いました。まず、母音や子音の点字の配置を、2進数のbitに対応させて変換しました。たとえば「A」なら、0b000001 という形になります。これは、bit0に①、bit1に②、という風に対応させています。これを、すべての母音・子音に対して手動で変換して、下記のようなハッシュテーブルを作成しました。
@tenji_table_one = {
# 母音
'A' => 0b000_001, # (1)
'I' => 0b000_011, # (2)(1)
'U' => 0b001_001, # (4)(1)
'E' => 0b001_011, # (4)(2)(1)
'O' => 0b001_010, # (4)(2)
# 撥音
'N' => 0b110_100 # (6)(5)(3)
}
@tenji_table_two = {
# 子音
'K' => 0b100_000, # (6)
'S' => 0b110_000, # (6)(5)
'T' => 0b010_100, # (5)(3)
'N' => 0b000_100, # (3)
'H' => 0b100_100, # (6)(3)
'M' => 0b110_100, # (6)(5)(3)
'R' => 0b010_000, # (5)
'Y' => 0b001_000, # (4)
'W' => 0b000_000 #
}
tenji_table_one は、母音と「N」を変換するテーブル、tenji_table_two は、子音を変換するテーブルです。2進数を調べているときに知ったのですが、数値の途中にアンダーバー( _ )を入れて、適当な場所で区切って見やすくすることができるみたいです。便利ですね。
このテーブルを使ってローマ字を点字にするために、論理演算を使いました。たとえば「KA」の場合「A」の2進数値と「K」の2進数値の論理和 100_000 | 000_001 = 100_001 を計算すると「KA」の点字の2進数値になります。これを点字文字列にすると
o -
-
- o
となります。
今回対象となるローマ字を2進数値に変換するための規則は、下記のようになります。
(A) アルファベット1文字なら、上記の tenji_table_one で変換するだけ
(B) アルファベット2文字で、
(B-1) 子音が「Y」か「W」で、
(B-1-1) 母音が「I」「E」「O」なら、母音に対応する2進数値を1ビット左シフトして、子音に対応する2進数値と論理和
(B-1-2) 母音が「A」「U」なら、母音に対応する2進数値を2ビット左シフトして、1文字目に対応する2進数値と論理和
(B-2) 1文字目が「Y」か「W」以外なら、2文字目の2進数値と子音の2進数値との論理和
(B-1-2)の処理は、下記のように論理和と左シフト演算を組み合わせています。
@tenji_table_two[tenji[0]] | (@tenji_table_one[tenji[1]] << 2)
たとえば「YA」なら、「A」を2ビットだけ左シフト 000_001 << 2 = 000_100 した後、「Y」と論理和 001_000 | 000_100 = 001_100 します。
3) 2進数値を点字の文字列に変換する
文字列への変換も、論理演算を利用しています。たとえば、tenjiという変数に100_001という値が入っている場合、点字パターンの①に対応するところに点があるかどうかは、①だけが1の2進数値と論理積をとります。この時、答えが0でなければ点があり、答えが0なら点は無いとわかります。コードにすると、下記のようになります。
tenji & 0b000_001 != 0 ? 'o' : '-'
たとえば、tenji に 100_000 というデータが入っている場合、上のコードを実行すると 100_000 & 000_001 = 000_000 となって、答えがゼロなので①に点は無いとなります。
上のコードで使っている 000_001 というデータのことを「マスク」と呼びます。今回は、それぞれのbitに 1 が入っているかどうか判定するので、下記のような6つのマスクを用意します。
000_001
000_010
000_100
001_000
010_000
100_000
このマスクを見るとわかる通り、実は000_001を1ずつ左にシフトした値が並んでいるだけです。なので、000_001のマスクを1つだけ用意して、そのマスクを判定毎に1bit左シフトしていけばよいとわかります。
#4. コードのアピールポイント
すべて論理演算で完結!
点字の作成や、文字列化の反転といった処理を、すべて論理演算で実現させました。で、これの何が良いのか?
私の無い脳みそを、ねじ切れるまでひねってみましたが、「論理演算を操れる私は、きっと特別な存在」「論理演算ってなんだか知的でかっこいい」といった気分に浸れる以外のメリットは見出せませんでした。
変換テーブルの更新が容易に!
最初、ローマ字を点字に変換するテーブルを、クラス変数で作っていました。しかし、クラスを継承してテーブルに新しい定義を追加すると、継承元のテーブルも変わってしまうことに気づきました。これは後々、名も知らぬクラス継承者が困るかもしれないと妄想しました。
そこで、テーブルをインスタンス変数にしました。インスタンス変数なら、継承したときに下記のようにして、継承元のテーブルを破壊せずに、既存のテーブルに新しい定義を追加する事ができます。
class tenji_maker2 < tenji_maker
def initialize()
super()
@tenji_table_one['X'] = 0b111_111 #新しい定義
end
end
そもそも、なぜ最初にクラス変数で定義したのか? いや、あの、クラスに紐づく変数だから、クラス変数なのかなって・・・
点字文字列の表示を、簡単に変更可能!
内部処理はそのままで、表示だけ変えたいと思った事、ありませんか?
下記のように表示に関わるパラメータをインスタンス変数にすることで、インスタンス変数の中身をちょっと変えれば表示を変えられるようになっています。
@tenji_dot = 'o' # ドットを表す文字
@tenji_blank = '-' # ブランクを表す文字
@tenji_sep = ' ' # 点字の区切り文字
ただ、この変数の中身を変えるのはどうすればよいか? ・・・クラス継承とかしてもらうしかないかな。
#5. 苦労したところ
真偽の罠
下記のようなコードを書いたら、期待通り動きませんでした。
tenji & 0b000_001 ? 'o' : '-'
これを実行すると、どんな値でも'o'になるんです。それはもうパニックですよ。Ruby は論理演算もわからないのか、と絶望しました。
もちろん私が間違っていたのですが、ググると、nil と false 以外はすべて真と評価されるそうで、上の式の場合、論理演算の計算結果はあくまで数値なので、問答無用で真と評価されるらしいです。なんで・・・?
改行コードの期待値
最初、文字列に改行を挿入する際、"\r\n" を使っていました。しかし、これでテストをすると、エラーと判定されました。理由は、テストコードの期待値が "\n" だった為です。
期待値が "\n" なんだから、ソースコードも "\n" に直せばいいじゃないか。そう思って、とっとと修正したのですが、ふと疑問に思いました。改行を直接コードで指定すると、OSが変わったときに困らないのか、と。
たとえば、この点字文字列をテキストに出力してしまうと、改行コードは強制的に "\n" になります。そうなると、改行コードが "\r\n" の Windows ユーザーは、泣きながら1つ1つ "\r" を手挿入することになるのではないだろうか。これは大変だと思いました。
そんなわけで、ググって探しましたよ。実行環境に合わせて適切な改行コードを出力する何かを。そして、見つけられませんでした。ごめんよ Windows ユーザー。
エラーの意味が分からない!
何もしてないのに、エラーになった! 助けて! となりました。
syntax error, unexpected '\n', expecting '.' or &. or :: or '['
日本語に訳すと「構文エラー:予期しない'改行'です。'.',&.,::,'['のどれかと間違えてませんか?愚か者なんですか?」となるんですが、予期しない改行ってなに?となりました。コードを何度も見直した結果、下記のようなコードを書いた事が原因でした。
for a.each do |b|
# 何か処理
end # ここの行に対してエラーを指摘された
eachメソッドを使っているのに、forを入れてしまったんです。ミスの内容は理解できるし、エラーの内容を言葉通りに理解できるけど、その2つがどうしても結び付きませんでした。もちろん、エラーメッセージをググりましたが、原因にはたどり着けませんでした。
もし、エラーメッセージとして、for文の行で「for に in が足りてないんじゃないかな?」あるいは「その 足りない頭 では理解できないかもしれないけど、each に for は、いらないんだよ。」と表示してくれれば、私みたいな素人でも、簡単にトラブルの原因にたどり着けたに違いありません。
そもそもこんなミスする奴いないのかな・・・
#6. さいごに
最後まで読んでいただき、ありがとうございます。
今回、この企画を開催いただけたこと、大変感謝いたします。おかげさまで、Qiitaに初めて投稿するきっかけとする事ができました。
参加にあたって、「プロを目指すためのRuby入門」は、読まずに 進めました。理由は、11月中にコーディングを終えて、12月に記事を書こうとしたら、本の発売が12月だった事に気づいたのと、あとちょっとで第二版がでるのに、初版を買うのが、お小遣い的に許されなかった為です。
そんなわけで、ソースコードを提出した後「プロを目指す人のためのRuby入門 改定2版」を読みました。そして思い知りました。
「ああ、やっぱり読んでからやればよかった・・・」と。
私のソースコードを見ていただければ、「ググって見つけた適当なWebページをみて我流で勉強したプログラマ」が作ると、必ずしもRubyらしいソースコードとはならず、「ちゃんと」答えることはできなかったとわかります。
もちろん、公式ドキュメントを読み込めば分かるだろ、という内容も多いのですが、素人は早く情報を得ようとして、ちょっとわかりやすそうな解説記事に飛びついたりしてしまうので、体系的な理解を得るのは難しいのかなと感じました。
良書などから得る、洗練された形式知を吸収すれば、無駄な手戻り、愚かな思い込み、しょうもないミスなどで、時間をロスすることなく、Ruby らしいソースコードを書くことができるのかなと、改めて思いました。
つまり、みんな本を買って勉強しよう!ということですね。・・・若干強引ですかね。
以上です。