1
0

More than 1 year has passed since last update.

Rubyをつかってローマ字を点字に変換する

Last updated at Posted at 2021-12-23

この記事は 改訂版・チェリー本発売記念 Advent Calendar 2021 の24日目向けに書いたものです。

参加した動機

はじめまして、都内でエンジニアをしているむらかみです。

毎年行われているQiitaのアドヴェントカレンダー企画、ときどきQiitaの記事を読み漁る中で見かける赤ラベルのついたあの記事はなんだろう?とは思ってはいたものの特に気にしていなかったのですが、伊藤さん企画のアドヴェントカレンダーがあるということを知り、これに参加すれば自分も赤ラベルのついた記事を投稿できるのではないか?
というちょっとしたプレミア感を演出した記事を投稿できる喜びを感じたためカレンダー登録しました。

それに加えて、自身がRubyからプログラミングをはじめたことや、伊藤さんが執筆されているチェリー本で学習をしてきたので、自分がやってきたことのアウトプットをする良い機会だと思ったことも大きな動機の一つです。

どのようなプログラムを書いたのか

タイトルにもありますが、今回書くプログラムの仕様を簡単に説明すると下記のようにローマ字を受け付けてそれを点字の記号で返すプログラムです。

詳細はこちらで確認をお願いします。

Calendar for Rubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念- | Advent Calendar 2021 - Qiita

tenji_maker = TenjiMaker.new
puts tenji_maker.to_tenji('A HI RU')

# => o- o- oo
#    -- o- -o
#    -- oo --

自分が書いたコードと解説

自分が実装したコードのプルリクはこちらです。

コードを書きながらクラスを分割していった結果、下記のようなクラスの関係図となりました。
矢印の向きは参照の方向を示しており、「TenjiMakerクラスはTenjiクラス, Rendererクラスのことを知っていて、TenjiクラスはCharactorクラス, Romajiクラスのことを知っている」ということを表現しています。

image.png

【解説1】 処理の全体像

まず受け取った文字列をTenjiMakerクラスのto_tenjiメソッド内で一文字ずつ分解して配列を作成した後、その配列から一文字ずつ取り出してTenjiクラスのインスタンス化をしています。
Tenjiインスタンスに対して、変換(convert)してやると点字に変換され、それをRendererクラスへと渡して整形・出力をおこなうという全体の処理の流れになっています。

#
# 点字メーカー
#
class TenjiMaker
  #
  # 点字に変換/出力する
  #
  # @param [String] text
  #
  # @return [String]
  #
  def to_tenji(text)
    charactors = text.split(' ')
    tenjies = to_tenji_strings(charactors)
    Renderer.render(tenjies)
  end

  private

  #
  # 点字文字列への変換
  #
  # @param [Array] charactors
  #
  # @return [Array]
  #
  def to_tenji_strings(charactors)
    charactors.map do |charactor|
      tenji = Tenji.new(charactor)
      tenji.convert
    end
  end
end

【解説2】 どのように点字へ変換しているか

【解説1】の実装でいうtenji.convertしている処理のコアとなる部分です。
まずは点字1文字を母音と子音にわけて、下記画像のように分割して番号を割り振って考えることにしました。
すると母音は① ~ ③のいずれかに「o」の記号を割り振ることで表現でき、子音に関しては④ ~ ⑥のいずれかに「o」を割り振ることで表現できるので、その規則性をもとにアルファベットと点字の対応表を作成して点字に変換しました。

image.png

しかしこのやり方では [ヤ行, ワ行, ン] の音は上記の母音/子音の分け方では表現できなかったため、それらを不規則な変換として扱うことにして、規則的な変換不規則的な変換の二つに点字への変換処理を分けています。

また規則的な変換として母音(例えばAなど)を点字に変換する場合は子音である@romaji.consonantはnilであるためLOWER_HALF_TENJI_TABLE[@romaji.consonant]も必然的にnilになり、その場合は全て-を返すようにしています。

さらに不規則変換で子音がyのときにrotateしているのは、点字位置① ~ ③を%w[- o -]で固定したうえで、下半分の点字位置④ ~ ⑥には母音をrotateした場合に点字の下記要件にぴったり当てはったためそのような処理を行なっています。

ヤ行はちょっと変わっていますが、母音をそのまま一番下へ移動させる

【参考】
Ruby Array#rotate
点字のしくみ

#
# 不規則な点字の変換(子音が w y と 単体 n だけの場合)
#
# @return [String]
#
def convert_irregularly
  return '---ooo' if @romaji.consonant_only_n?
  return '----o-' if @romaji.consonant_w?

  (%w[- o -] + UPPER_HALF_TENJI_TABLE[@romaji.vowel].rotate).join if @romaji.consonant_y?
end

#
# 規則的な点字の変換
#
# @return [String]
#
def convert_regularly
  vowel_tenjies = UPPER_HALF_TENJI_TABLE[@romaji.vowel]
  consonant_tenjies = LOWER_HALF_TENJI_TABLE[@romaji.consonant] || %w[- - -]
  (vowel_tenjies + consonant_tenjies).join
end

【解説3】 出力処理

最後に点字に変換されたものを期待する出力に変換する箇所の解説になります。
【解説2】で説明した点字への変換処理により、点字文字列の配列が出来上がります。

下記ではbinding.irbを使用してtext = 'A HI RU'のときの処理を止めて、点字変換後のものを出力してみると点字に変換されたものが配列で取得できていることがわかります。

    15:   def to_tenji(text)
    16:     charactors = text.split(' ')
    17:     tenjies = to_tenji_strings(charactors)
 => 18:     binding.irb
    19:     Renderer.render(tenjies)
    20:   end
    21: 
    22:   private
    23: 

irb(#<TenjiMaker:0x00007fa8930173c0>):001:0> tenjies
=> ["o-o--o", "oo-oo-", "ooo-o-"]

この点字の配列をRendererという出力/整形の責任を持つクラスに渡して、一つ一つの要素をそれぞれ下記のように並び替え、組み合わせた上で出力しています。
image.png

Rendererクラス内ではさらにRowというクラスを内包して定義しており1行目(first), 2行目(second), 3行目(third)というインスタンス変数を持たせています。そして行を全てを組み合わせたものを1つのテンプレートとしてRendererのインスタンスメソッドに定義して点字を出力しています。

リファクタリングした結果下記のようになりました。
点字は3行で表現されるため3回ループを回して、出力するための各行をstrings.map { |string| parts(string, n) }.join(' ')で作成し、この各行を改行文字\nで結合することで期待通りの出力結果になるようにしています。

そのうちのpartsメソッドでは点字1文字を構成する1段分を点字文字列と何段目かということを引数に渡すことで表現できるようにしています。

class Renderer
  class << self
    #
    # 出力する
    #
    # @param [Array] strings
    #
    # @return [String]
    #
    def render(strings)
      3.times.map do |n|
        strings.map { |string| parts(string, n) }.join(' ')
      end.join("\n")
    end

    private

    def parts(string, number)
      return string.slice(0, 2) if number == 0
      return string.slice(2, 2) if number == 1

      string.slice(4, 2) if number == 2
    end
  end
end

がんばったところ・ふりかえり

実装する上で「他人が読みやすいコード」を書くことを心掛けました。
そのためにある程度意味のある単位でクラスに分割したり、命名にこだわったり、yardコメントを記載したりしました。
期待通りの出力ができる実装が出来上がってからのリファクタリングの方が時間がかかったなという気がします。

あとはより良い規則性を捉えて実装できればさらにスッキリしたものがかけそうだな〜という感覚もあり他の方が書かれたコードを見て勉強させていただこうと思っています。

メッセージ

改訂版発売おめでとうございます。
伊藤さんが執筆された記事やチェリー本の内容は非常にわかりやすく、Rubyを学習している自分にとってはありがたいものばかりだなと感じています。これからRubyを学習する方にはぜひおすすめしたい書籍です。

まだまだ自分自身も学習中の身なため、これからもRubyを楽しみながら頑張っていきたいと思います。
記事を読んでいただきありがとうございました!
Merry Christmas!🎅

コードレビュー後の修正

伊藤さんからコードレビューをいただいたため、ふりかえりを兼ねてコードを一部修正しました。

修正前と修正後の差分

修正点としてはざっくり下記の事項となります。

  • 点字と英字の対応表であるTABLEは英字が母音の対応表と英字が子音の対応表に分けたほうが意図がわかりやすいため分割
  • irregularメソッドはRomajiクラスの責任と考える方が自然なため、Romajiクラスのインスタンスメソッドとして定義
  • クラスの状態(インスタンス変数)はなるべく減らしたほうがコードが複雑にならずメリットが大きいため、Charactorクラスでメソッドとして定義できるインスタンス変数はメソッドにした
  • Rendererの1行目, 2行目, 3行目というインスタンスメソッドをループで回すことによりコード量を減らした

レビューありがとうございました!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0