概要
本記事はRubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念- Advent Calendar 2021に、18日目として参加したものです。
ローマ字で与えられる入力値を点字に変換し出力するプログラムを作成しました。問題の詳細はこちらをご確認ください
PR
私のPullRequestはこちらになります
コードの解説
ざっくりと書いたコードについて解説していきます。
まずもって、今回実装対象のto_tenjiメソッドに関わる実装は以下のとおりです。
class TenjiMaker
# 点字配列のindex順は、ドメインとしての点字のナンバリングと同じ順で扱う
TENJI_INITIAL = ['-', '-', '-', '-', '-', '-']
# key: ローマ字の入力値 value: 点字のマーク箇所のindex
TENJI_EXCEPTIONAL = {'YA' => [2,3], 'YU' => [2,3,5], 'YO' => [2,3,4], 'WA' => [2], 'N' => [2,4,5]}
TENJI_NORMAL = {'A' => [0], 'I' => [0,1], 'U' => [0,3], 'E' => [0,1,3], 'O' => [1,3],
'K' => [5], 'S' => [4,5], 'T' => [2,4], 'N' => [2], 'H' => [2,5],
'M' => [2,4,5], 'R' => [4]}
def to_tenji(text)
split_text = text.split(' ')
tenji_letters = set_tenji_initial(split_text.length)
split_text.each_with_index do |letters, index|
if TENJI_EXCEPTIONAL.keys.include?(letters)
mark_tenji_exceptional(letters, tenji_letters[index])
else
mark_tenji_normal(letters, tenji_letters[index])
end
end
format_tenji_for_output(tenji_letters)
end
~ (他メソッドを省略) ~
end
to_tenjiメソッドについて、一つずつ見ていきましょう。
実例として、入力値が"KI RI N"だった場合の出力についても都度見ていきます。
split_text = text.split(' ')
tenji_letters = set_tenji_initial(split_text.length)
入力値であるtextはローマ字で渡されます。また、かな文字1文字につき半角スペースで渡されます。(例: 'KI RI N')
今回最終的に出力する点字は、かな文字1文字につき1つの2x3のブロックが対応している表記になります。
上記2つの理由から、入力値を半角スペースで区切った時に生まれる要素数 = かな文字数 = 出力する点字ブロックの数であることがわかるため、半角スペースで区切った上で、要素数を利用して必要な数の点字ブロックを配列として渡しています。
set_tenji_initialメソッドは以下のとおりです。
# 点字配列のindex順は、ドメインとしての点字のナンバリングと同じ順で扱う
TENJI_INITIAL = ['-', '-', '-', '-', '-', '-']
def set_tenji_initial(num)
tenji_letters = []
num.times do
tenji_letters << TENJI_INITIAL.dup
end
tenji_letters
end
# 例
# 例えばtextが"KI RI N"(かな3文字の時)
# tenji_letters => [["-","-","-","-","-","-"], ["-","-","-","-","-","-"], ["-","-","-","-","-","-"]]
点字は、2x3のブロックで1文字を表現しますが、入力値は常に「-」か「●(本問題の表記上はo)」です。
対象のカナ文字の子音部分と母音部分それぞれに対応するマスを「●」でマークしていくことで、点字を作成することが出来ます。
set_tenji_initialメソッドは初期状態(これからマークを行なっていく状態)なので、全ての要素が'-'で構成されている、要素数が6(2x3)の配列を、与えられたカナ文字分セットしています。
to_tenjiメソッドの続きを見ていきます
split_text.each_with_index do |letters, index|
if TENJI_EXCEPTIONAL.keys.include?(letters)
mark_tenji_exceptional(letters, tenji_letters[index])
else
mark_tenji_normal(letters, tenji_letters[index])
end
end
# 例
# "KI RI N"の例だと、3文字目のNはmark_tenji_execptional、他はmark_tenji_normalに処理がわたる
かな文字単位で分離されたsplit_text一つ一つに対して、先ほどセットした点字用の配列にマークを行なっていく処理になります。
具体的には二つのmark_tenjiメソッドを基に説明します。
# key: ローマ字の入力値 value: 点字のマーク箇所のindex
TENJI_EXCEPTIONAL = {'YA' => [2,3], 'YU' => [2,3,5], 'YO' => [2,3,4], 'WA' => [2], 'N' => [2,4,5]}
TENJI_NORMAL = {'A' => [0], 'I' => [0,1], 'U' => [0,3], 'E' => [0,1,3], 'O' => [1,3],
'K' => [5], 'S' => [4,5], 'T' => [2,4], 'N' => [2], 'H' => [2,5],
'M' => [2,4,5], 'R' => [4]}
def mark_tenji_exceptional(letters, tenji_letter)
TENJI_EXCEPTIONAL[letters].map{ |num| tenji_letter[num] = 'o'}
end
# 例
# "N"の時、tenji_letter = ["-","-","-","-","-","-"]
# TENJI_EXCEPTIONALで'N'のvalueとして指定されている[2,4,5]がindexの箇所にマークされる
# 返り値としては["-", "-", "o", "-", "o", "o"]となる
def mark_tenji_normal(letters, tenji_letter)
letters.each_char do |letter|
TENJI_NORMAL[letter].map{ |num| tenji_letter[num] = 'o'}
end
# 例
# "KI"の時、TENJI_NORMALのkeyが"K"と"I"のものについてマークが行われる
# 返り値としては、["o", "o", "-", "-", "-", "o"]
end
今回の入力値の内、特殊な入力規則を持つものは「'YA', 'YU', 'YO', 'WA', 'N'(ん としてのN)」ですので、それらに該当する場合はmark_tenji_execptinonalを、それ以外はmark_tenji_normalを呼び出します。
それぞれ定数としてハッシュを定義しており、keyには該当の文字を、valueとして、それぞれの文字がマークすべき箇所のindexを配列として持たせています。
それぞれのメソッドの中で、渡されたローマ字をkeyとしてvalueを取得し、mapでマーク処理を書けば適切な場所にマークがなされる様になっています。
例外の場合は 入力値を決め打ちで処理していますが、通常の場合はアルファベット2文字の場合、1文字の場合がそれぞれ存在するため、each_char
で1文字単位に分割し処理を行っています
ここまでくると、tenji_lettersにはマークが終わった点字の情報が保持されているので、最後に出力するために見た目を整える処理を行っています。
# to_tenjiの処理
format_tenji_for_output(tenji_letters)
def format_tenji_for_output(tenji_letters)
tenji_letters_for_output = []
tenji_letters.map{ |letter| tenji_letters_for_output << [letter[0] + letter[3], letter[1] + letter[4], letter[2]+ letter[5]]}
tenji_letters_for_output.transpose.map { |a| a.join(" ")}.join("\n")
end
format_tenji_for_outputメソッド内の処理を順に見ていきます。
tenji_letters_for_output = []
tenji_letters.map{ |letter| tenji_letters_for_output << [letter[0] + letter[3], letter[1] + letter[4], letter[2]+ letter[5]]}
# 例
# "KI RI N"の場合、tenji_letters = [["o", "o", "-", "-", "-", "o"], ["o", "o", "-", "-", "o", "-"], ["-", "-", "o", "-", "o", "o"]]
# この時点での返り値は、[["o-", "o-", "-o"], ["o-", "oo", "--"], ["--", "-o", "oo"]]
tenji_lettersには点字の数え方順(縦方向)で配列に要素が入っています。↓
(出典)全視情協HP
点字のドメイン側のナンバリングが上記画像の様に定義されているため、配列もそれに沿った順で情報を持たせる様にしました。
出力時には横方向のものをまとめて出力したいため、まずこの部分で横並びの要素を結合した配列を作成しています。
この時点で、配列内の各配列については、それぞれの文字に対応する要素がまだあるにすぎません。
しかし今回は出力の関係で、かな文字をまたがって、同じ行に存在する要素はまとめて1行で出力する必要があります。配列に要素としてみた時に、各配列の内容を同じ行に出力する」のではなく、各配列で同じindexを持つ要素を同じ行に出力することが求められていることです。
index毎にまとめて出力するということは、2次元配列である入力値を行列とみた時に、列単位で出力することが求められています。
これを叶えるために、transposeメソッドを利用し、行列の転置を行います。
tenji_letters_for_output.transpose.map { |a| a.join(" ") }.join("\n")
# 例("KI RI N")
# tenji_letters = [["o-", "o-", "-o"], ["o-", "oo", "--"], ["--", "-o", "oo"]]
# tenji_letters.transpose => [["o-", "o-", "--"], ["o-", "oo", "-o"], ["-o", "--", "oo"]]
これにより、列が行に転換されているため、素直に各配列について出力することで、求められる要件での出力gが可能になります。
「同一行、かな文字間は半角スペースで区切る」や「行毎に改行する」といった要件については、joinメソッドで要素の結合方法を指定することで実現しています。
工夫した所
コードを書く前に、ざっくり手順を設計した
手を動かす前に、何が手順として必要そうかはメモしてから着手しました。
点字へのそもそもの理解も余りないですし、入力値についてもありがたいことに制約が色々あったので、そうした前提を理解した上で何をすれば実現できそうかの作戦立てをしてから書き始める様にしました。
手順を明確にすることは後々リファクタリングを考える時に役だったと思います。
まず書き上げて、そこからリファクタリング
とりあえずtestをパスするコードを通した上で、リファクタリングをいくつか行いました。
やったこととしてはざっくり以下でしょうか。
1.作る過程でもやっとしてた変数名について考え直す
2.手順毎に切り出せる(別から呼び出される可能性のある)ものについてはメソッド化して適切な命名をする
3.点字の配列のindex順を、点字のしくみページ記載の数え順(縦型)に定義した上でメソッドを修正
4.既存の実装で簡潔にかける箇所は書き直す
5.定数の意味だけはコメントアウトで記載(パッとみわかりづらかったため)
一度testが通る形でコードを書き上げているので、修正は行いやすかったです(testのfailで気付けるため)。
修正の観点としては「コード単体としてみやすいか」、「点字のドメインとしてのルールを踏まえたものになっているか」を意識していました。
特に点字の数え順にそったデータを作る所と、求められる出力に成形する所を切り分けるということは意識しました。
今回求められている出力の形を実現するためには、「一つの配列に複数のかな文字の要素が存在する」状態を作り出す必要がありましたが、あくまでデータとしては「一つの点字に関する要素をまとめた配列が、点字の数分存在する」状態が好ましいと思ったためです。
出力時の見え方に関してはあくまで今回求められているだけの話なので、format_tenji_for_outputメソッドを定義してそこで変更する形にしました。
マークのロジックについては極力定数への記述に寄せた
これは、点字のマークというドメイン特有のロジックについて、「定数箇所に定義を集約させる」ことで修正等をしやすくしたかった、という意図になります。 基本的にはここに書き足していく形で、メソッド側を修正せず他の文字もマークできる様になっていくことを意図しています。
工夫しきれなかった所
発想が及ばなかった所は以下です。
そもそも例外ケースの定義がこれでいいのか?問題
例えばヤ行やワ行について、今回は例外値として定義していますが、こちらは「母音部分のマークが他の行のルールと異なる」だけであり、規則性は持っています。
となると、マークのロジック自体を母音の時と子音の時で分け、特定の子音の時は母音側のマーク箇所が変わる様なロジックを書く、という形が点字の規則性にのっとった時に書くべきロジックだと思うのですが、イマイチ方法が思いつかず・・・
ここは思いついたら根本的に書き換えたい所ですね。
この記事を公開するまで他の人の記事を見ない様にしてるので、はやく見て勉強したいなと思っています笑
伊藤さんへメッセージ
私はRailsエンジニアになって丁度1年くらいが経つ者です。([FYI]しくみ製作所で働いておりまして、yoshitsuguメンターの元で日々教えを受けています!)
チェリー本は、Ruby初学者にとっては必読書だなと思っています。チェリー本で基礎的な部分について学んだことで、Rubyのリファレンスを読める様になった実感があり、学びはじめの頃出会えて本当にありがたかったな、と感じています。(特に正規表現については、ほぼ全て伊藤さんの記事やチェリー本の内容のみで相当に理解が深まりました)
このたびに改訂版を出されたということで、おめでとうございます!
年末年始に購入し、復習も兼ねて再度学習させていただきますね!
いつもありがとうございます!