改訂版・チェリー本発売記念のRubyプログラミング問題にチャレンジしました

## はじめに
この記事は [Rubyプログラミング問題にチャレンジ！ －改訂版・チェリー本発売記念－ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ruby-challenge) の23日目の記事となります。

## プルリクエストのリンク
https://github.com/JunichiIto/tenji-maker-challenge/pull/29

## 方針
- [KISSの原則](https://ja.wikipedia.org/wiki/KISS%E3%81%AE%E5%8E%9F%E5%89%87)　に則り、問題のテストが通れば十分とする（変に拡張性を考えない）
- 点字の業務知識に沿ったコードを心がける

## 作戦
[問題のREADME](https://github.com/JunichiIto/tenji-maker-challenge/blob/main/README.md) を眺めてみます。
点字1字について

- 母音と子音の組み合わせで表現できる
- や行、わ、んは例外

が見えてきます。

- 入力にアルファベットを取り点字1字を表すクラス

を用意して、

- 上記クラスを複数持つデータ構造

を題意の点字文字列に変換できれば十分であると考えました。

## 入力にアルファベットを取り点字1字を表すクラス

こんなクラスを考えます。

```rb
class Tenji
  def initialize(str)
    @str = str
  end
end
```

母音と子音の組み合わせで表現できる、裏を返せば入力文字列を母音と子音に変換する必要があります。

- 「あ行」と「ん」は1文字
- それ以外は2文字
- あ行には子音がない
- [んは通常子音](https://ja.wikipedia.org/wiki/%E3%82%93)

であることから入力文字列を母音と子音に変換するメソッドを次のように定義しました。

```rb
class Tenji
  ...
  attr_reader :str

  # 母音
  def vowel
    if str.length == 2
      str[1]
    elsif str == "N"
      nil
    elsif str.length == 1
      str[0]
    end
  end

  # 子音
  def consonant
    if str.length == 2
      str[0]
    elsif str == "N"
      str[0]
    elsif str.length == 1
      nil
    end
  end

  ...
end
```

次に点字1字をどういうデータ構造で表すか考えます。

[全視情協:点字とは - 点字のしくみ](http://www.naiiv.net/braille/?tenji-sikumi) を見てみると、

- 母音は1,2,4
- 子音は3,5,6

で表すことがわかります。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f373436352f31633830643434622d616530662d313135332d623239642d6439326265363936353338302e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41778/9f99b617-1d0a-ce92-31e5-e50acf0fcae5.png)

データ構造として配列、ハッシュなどが考えられますがここではハッシュを採用します。

```rb
# あ
{
  1 => "o", 4 => "-",
  2 => "-", 5 => "-",
  3 => "-", 6 => "-",
}
```

のように書くことでコードの見た目と点字の見た目を一致させることができるからです。点字は1始まりなので0始まりである配列で表現するのは毎回0を考慮する必要があるとも考えました。ハッシュを採用したことで

```rb
def a
  {
    1 => "o", 4 => "-",
    2 => "-", 5 => "-",
    3 => "-", 6 => "-",
  }
end

def k
  {
    5 => "-",
    3 => "-", 6 => "o",
  }
end

```

と定義すると「か」が

```rb
a.merge(k)
# => => {1=>"o", 4=>"-", 2=>"-", 5=>"-", 3=>"-", 6=>"o"}
```

と表現できます。[#send](https://docs.ruby-lang.org/ja/latest/method/Object/i/send.html) を使えば先程の母音/子音の表現と合わせて

```rb
send(vowel.downcase).merge(send(consonant.downcase))
```

で表せそうです。なのでひたすら母音と子音の小文字のメソッドを定義していきます。

- や行、わ、んは例外

を考えるとそれらは `ya` `wa` といったメソッドで表します。

```rb
def ya
  {
    1 => "-", 4 => "o",
    2 => "-", 5 => "-",
    3 => "o", 6 => "-",
  }
end

def wa
  {
    1 => "-", 4 => "-",
    2 => "-", 5 => "-",
    3 => "o", 6 => "-",
  }
end

```

また「ん」は子音がな行と重複するため定数で表現します。

```rb
N =
  {
    1 => "-", 4 => "-",
    2 => "-", 5 => "o",
    3 => "o", 6 => "o",
  }
```


以上から入力文字列を点字のハッシュで表すのは下記のクラスでできそうです。

https://github.com/JunichiIto/tenji-maker-challenge/pull/29/files#diff-1085adbbda7ce4f24577514be1a664c0dd5a3097ec423cfefcf6ed7d8a1ce063

抜粋

```rb
class Tenji
  attr_reader: str
  
   def initialize(str)
    @str = str
  end

  def to_h
    if consonant.nil? # あ行
      send(vowel.downcase)
    elsif %w[K S T N H M R].include?(consonant) && vowel
      send(vowel.downcase).merge(send(consonant.downcase))
    elsif %w[Y W].include?(consonant)
      send(str.downcase)
    elsif %w[N].include?(consonant) && vowel.nil? # ん
      N
    end
  end
  ...
end
```

テストも書いておきました。
https://github.com/JunichiIto/tenji-maker-challenge/blob/a1fa0e32fb1dd8cb55ca52da4f268a730e6a20b6/test/tenji_test.rb

網羅しつつケースを減らすために [直交表](https://geechs-job.com/tips/details/34) という考えをベースに a, i, u, e, o, ka, si, tu, ne, ho...ya, yu, yo, wa, n のようなテストケースとなってます。

## 点字クラスを複数持つデータ構造

配列で良さそうです。下記のような形で表現することを考えます。

```rb
[Tenji.new("A"), Tenji.new("HI"), Tenji.new("RU")]
```

入力文字列を [#split](https://docs.ruby-lang.org/ja/latest/method/String/i/split.html) してやれば良さそうです。

```rb
tenjis = text.split(" ").map { |str| Tenji.new(str) }
```

求めたい文字列は `A HI RU` の場合下記です。

```rb
"o- o- oo\n-- o- -o\n-- oo --\n"
```

なのでTenjiを下記のような配列に変換します。

```rb
[["o-", "o-", "oo"],
 ["--", "o-", "-o"],
 ["--", "oo", "--"]]
```

一般化するとこうですね。

```rb
tenji_matrix = tenjis.map(&:to_h).map do |tenji_hash|
  [
    tenji_hash[1] + tenji_hash[4],
    tenji_hash[2] + tenji_hash[5],
    tenji_hash[3] + tenji_hash[6],
  ]
end
```

文字列操作しやすいように下記のように変換します。

```rb
[["o-", "--", "--"], 
 ["o-", "o-", "oo"], 
 ["oo", "-o", "--"]]
```

行列としてみると上の転置行列とみなせるので [#transpose](https://docs.ruby-lang.org/ja/latest/method/Array/i/transpose.html) が使えそうです。

```rb
tm = tenji_matrix.transpose
```

あとは各要素を `" "` で繋げて、改行すれば良いから

```rb
tm.map { |arr| arr.join(" ") }.join("\n")
```

でできそうです。

以上まとめると下記となります。

https://github.com/JunichiIto/tenji-maker-challenge/blob/a1fa0e32fb1dd8cb55ca52da4f268a730e6a20b6/lib/tenji_maker.rb

```rb
require "tenji"

class TenjiMaker
  def to_tenji(text)
    tenjis = text.split(" ").map { |str| Tenji.new(str) }

    tenji_matrix = tenjis.map(&:to_h).map do |tenji_hash|
      [
        tenji_hash[1] + tenji_hash[4],
        tenji_hash[2] + tenji_hash[5],
        tenji_hash[3] + tenji_hash[6],
      ]
    end
    tenji_matrix.transpose.map { |arr| arr.join(" ") }.join("\n")
  end
end

```

## 伊藤さんにメッセージ
うだつの上がらないエクセル設計書/エビデンス系SEだったわたしが、2018年に職業プログラマへ転職しご飯を食べれているのはチェリー本のおかげです。改訂版も楽しく読ませていただきます（と言いつつまだ買っていないです）。