点字メーカープログラムに挑戦！ | 行列でシンプルに実装

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


## 1. ルールとか 🔰
ルールや、他の方の回答はこちらにあります。

https://qiita.com/advent-calendar/2021/ruby-challenge


## 2. 成果物 🍒
https://github.com/JunichiIto/tenji-maker-challenge/pull/27

<details><summary>ソースコード 📄 ( クリックで開きます )</summary><div>

```ruby:lib/tenji_maker.rb
require 'matrix'

# 180度回転
def rotate180(matrix)
  Matrix.rows(matrix.to_a.reverse.map(&:reverse))
end

# 上下反転
def reverse(matrix)
  Matrix.rows(matrix.to_a.reverse)
end

# 一番下に下げる
def down(tenji_to_a)
  return Matrix.rows(tenji_to_a) unless tenji_to_a[2].sum.zero?
  down([
    [0, 0],
    tenji_to_a[0],
    tenji_to_a[1]
  ])
end

class TenjiMaker
  class << self
    attr_accessor :boin, :siin
  end

  @boin = {
    'A' => Matrix[[1, 0], [0, 0], [0, 0]],
    'I' => Matrix[[1, 0], [1, 0], [0, 0]],
    'U' => Matrix[[1, 1], [0, 0], [0, 0]],
    'E' => Matrix[[1, 1], [1, 0], [0, 0]],
    'O' => Matrix[[0, 1], [1, 0], [0, 0]]
  }
  @boin['N'] = rotate180(@boin['E'])
  @siin = {
    'K' => rotate180(@boin['A']),
    'S' => rotate180(@boin['I']),
    'T' => rotate180(@boin['O']),
    'N' => reverse(@boin['A']),
    'H' => rotate180(@boin['U']),
    'M' => rotate180(@boin['E']),
    # 'Y'
    'R' => Matrix[[0, 0], [0, 1], [0, 0]]
    # 'W'
  }

  def exceptional(bo, si)
    if si == 'Y'
      down(self.class.boin[bo].to_a) + Matrix[[0,1], [0,0], [0,0]]
    elsif si == 'W'
      down(self.class.boin[bo].to_a)
    else
      nil
    end
  end

  def to_tenji(text)
    ary = []
    text.split.each do |romaji|
      bo = romaji.split('')[-1]
      si = romaji.split('')[-2]
      # 例外的な場合 (今回は、やゆよわを)
      ex = exceptional(bo, si)
      if ex
        ary << ex
        next
      end
      # 通常 (母音 + 子音 or 子音のみ)
      if si
        ary << self.class.boin[bo] + self.class.siin[si]
      else
        ary << self.class.boin[bo]
      end
    end

    # 表示
    output = ''
    text_num = text.split.length
    3.times do |y|
      text_num.times do |x|
        output << (ary[x].element(y, 0).zero? ? '-' : 'o')
        output << (ary[x].element(y, 1).zero? ? '-' : 'o')
        output << ' '
      end
      output = output.chop + "\n"
    end
    output.chomp
  end
end
```

</div></details>


## 3. 実装アイデア 💡

下の図を見てもらうと分かる通り
点字は、母音の位置の点と、子音の位置の点の足し算になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/788779/a3632ae4-f372-15db-8eb8-f220b26e9415.png)

最初に思いつく方法として配列での実装がありましたが、今回は行列を扱うことが出来る `Matrix` を使用します。( `Matrix` では行列の足し算が簡単に出来るという点のみで採用したので、配列で実装しても、行列の足し算の実装があるくらいで、ほとんど変わりません。)


## 4. 解説 🧬
では、
今回の実装のメインの部分を解説していきましょう！
まず、こちらがソースコードです。( 下にGitHub のリンクもあるので、見やすい方で見て下さい )

```ruby:lib/tenji_maker.rb
def to_tenji(text)
    ary = []
    text.split.each do |romaji|
      bo = romaji.split('')[-1]
      si = romaji.split('')[-2]
      # 例外的な場合 (今回は、やゆよわを)
      ex = exceptional(bo, si)
      if ex
        ary << ex
        next
      end
      # 通常 (母音 + 子音 or 子音のみ)
      if si
        ary << self.class.boin[bo] + self.class.siin[si]
      else
        ary << self.class.boin[bo]
      end
    end

    # 表示
    output = ''
    text_num = text.split.length
    3.times do |y|
      text_num.times do |x|
        output << (ary[x].element(y, 0).zero? ? '-' : 'o')
        output << (ary[x].element(y, 1).zero? ? '-' : 'o')
        output << ' '
      end
      output = output.chop + "\n"
    end
    output.chomp
  end
```
> https://github.com/stonesaw/tenji-maker-challenge/blob/main/lib/tenji_maker.rb#L82


### 4.1. ローマ字ごとに区切る

まず、`to_tenji` メソッドの引数 `text` には
`'A HI RU'` や `'HI YO KO'` など、スペース区切りのローマ字が渡されます。
なので、

```ruby
text.split.each do |romaji|
  # ...
end
```

とすることで、変数 `romaji` に、`'A'`, `'HI'`, `'RU'` などの文字列が入って、ローマ字ごとに操作ができるようになります。

### 4.2. 母音と子音で区切る

次は、こちらのコードです。

```ruby
bo = romaji.split('')[-1]
si = romaji.split('')[-2]
```

`.split('')` は、文字列を1文字ずつに分解して、配列を返します。
例えば、`'KA'` だと... `['K', 'A']`、 `'U'` だと... `['U']` といった具合になります。

そして、ちょっとしたテクニックなんですが
`split`で分割してできた配列の **1番後ろ**を取ると、母音を
`split`で分割してできた配列の **後ろから2番目**を取ると、子音を
取ることができます。

配列の後ろから (`[-1]`, `[-2]`) 取ってあげることで、
母音だけの場合 (`AIUEO`)でも、正しく取得することが出来ます。


### 4.3. 行列の足し算

そして次は、
通常 (母音 + 子音 or 子音のみ) の場合か、
特殊な場合 (今回は、やゆよわを) に分かれます。


#### 4.3-a. 普通の場合 (母音 + 子音 or 子音のみ)

実際のコードと順番が前後しますが、簡単なので普通の場合から解説していきます。
まず、`self.class.boin` や `self.class.siin` に母音や子音の行列データを保存しておきます。

```ruby:sekf.class.boin
@boin = {
  'A' => Matrix[[1, 0], [0, 0], [0, 0]],
  'I' => Matrix[[1, 0], [1, 0], [0, 0]],
  ...
}
```

与えられたローマ字に、子音があるときには、子音 + 母音 
母音のみのときには、母音そのまま
を 配列 `ary` に保存しておきます。

```ruby
if si
  ary << self.class.boin[bo] + self.class.siin[si]
else
  ary << self.class.boin[bo]
end
```

#### 4.3-b. 特殊な場合 (今回は、やゆよわを)
今回は特別な場合が、`やゆよわを` くらいだったので、決め打ちで行列として変数を持たせておいても良かったかもしれませんが、👇下の画像を愚直に実装してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/788779/0212579b-7827-61fb-6225-3464b7e33657.png)

まず、メソッド `down` は点字を1番下へ移動させるためのメソッドで、再帰を使って実装しています。

```ruby
def down(tenji_to_a)
  return Matrix.rows(tenji_to_a) unless tenji_to_a[2].sum.zero?
  down([
    [0, 0],
    tenji_to_a[0],
    tenji_to_a[1]
  ])
end
```

また、拡張しやすいように、特別な場合を扱うメソッド `exceptioanl` を作成しました。

```ruby
def exceptional(bo, si)
  if si == 'Y'
    down(self.class.boin[bo].to_a) + Matrix[[0,1], [0,0], [0,0]]
  elsif si == 'W'
    down(self.class.boin[bo].to_a)
  else
    nil
  end
end
```

メソッド `exceptional` は、特別な場合だったら `Matrix` を返して、特別な場合でなかったら `nil` を返すようにしています。

```ruby
ex = exceptional(bo, si)
if ex
  ary << ex
  next
end
```

### 4.4. 表示する
最後は、いい感じに表示させれば完成です。

```ruby
# ary = [
#   Matrix[...],
#   Matrix[...],
#   ...
# ]

output = ''
text_num = text.split.length
3.times do |y|
  text_num.times do |x|
    output << (ary[x].element(y, 0).zero? ? '-' : 'o')
    output << (ary[x].element(y, 1).zero? ? '-' : 'o')
    output << ' '
  end
  output = output.chop + "\n"
end
output.chomp # メソッドの最後 ( return output.chomp と同じ )
```

### 5. 感想 📈
自分は、まともに記事を書くのは初めてで、書き方・まとめ方にとても苦労しました。
また、最近はJavaScript系を触っていたので、いろいろ調べながら書いていたら、以外と時間がかかってしまいました... 
@jnchito さん とても良い企画をありがとうございました！ 記事を書く機会にもなりました！
自分も、Matz江市民として、Rubyist として、Rubyを盛り上げていきたいです！

---

少し宣伝みたいになってしまうのですが、
今自分が参加している [Smalruby](https://github.com/smalruby/smalruby3-gui) というプロジェクトもぜひよろしくお願いします！小中学生を対象にした、Rubyも書ける ブロック プログラミング アプリです！
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/788779/d723e532-9e18-aa7e-6f9a-5d5f23e1e632.png" /> <img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/788779/8e78a571-2592-6873-a909-10fc9c2e05c1.png" />