Rubyを使って点字をbit変換して出力

# 概要

今回のやることは、入力された文字を点字で返すことです。
例えば`A HI RU`が入力されたとき、次のように返すように。

```math
\begin{matrix}
o & - \\
- & - \\
- & - \\
\end{matrix}
\qquad
\begin{matrix}
o & - \\
o & - \\
o & o \\
\end{matrix}
\qquad
\begin{matrix}
o & o \\
- & o \\
- & - \\
\end{matrix}
```

### 点字の規則性

点字には規則性があり、2×3の点のうち次のように上段と中段左の3点で母音を表現できます。


```math
A = \begin{matrix}
o & - \\
- & - \\
- & - \\
\end{matrix}
\qquad

I = \begin{matrix}
o & - \\
o & - \\
- & - \\
\end{matrix}
\qquad

U = \begin{matrix}
o & o \\
- & - \\
- & - \\
\end{matrix}
\qquad

E = \begin{matrix}
o & o \\
o & - \\
- & - \\
\end{matrix}
\qquad

O = \begin{matrix}
- & o \\
o & - \\
- & - \\
\end{matrix}
```

そして、残りの中段右と下段の3点で子音を表現できます。

```math
K = \begin{matrix}
- & - \\
- & - \\
- & o \\
\end{matrix}
\qquad

S = \begin{matrix}
- & - \\
- & o \\
- & o \\
\end{matrix}
\qquad

T = \begin{matrix}
- & - \\
- & o \\
o & - \\
\end{matrix}
\qquad \\
```

```math
N = \begin{matrix}
- & - \\
- & - \\
o & - \\
\end{matrix}
\qquad

H = \begin{matrix}
- & - \\
- & - \\
o & o \\
\end{matrix}
\qquad

M = \begin{matrix}
- & - \\
- & o \\
o & o \\
\end{matrix}
\qquad

R = \begin{matrix}
- & - \\
- & o \\
- & - \\
\end{matrix}
\qquad
```

例えば`HI`の時は子音と母音を組み合わせて次のようになります。

```math
HI = \begin{matrix}
o & - \\
o & - \\
o & o \\
\end{matrix}
\qquad \\
```

小さい文字(`ッ`, `ャ`, `ュ`, `ョ`)を含む文字や、`ヲ`など、規則に当てはまらないものもあるのですが、今回は考慮しなくていいのでありがたく無視します。
そして、`YA, YU, YO, WA, N`だけ考慮が必要なので例外として定義することとします。定義はロジックのほうで記載します。

# ロジックの解説

今回の条件では入力の1音1音が1つの点字に対応します。機械的に処理するために入力に対してそれぞれどこを塗ればいいのかというのを数値化しておきましょう。

点字の6つ点のエリアは塗るか塗らないかの2通りなので`0, 1`の2進数(bit)で表現できます。

ファイルのパーミッション設定の時に`rwxr-xr-x`にするのに`755`と指定したりしますよね。数値だけでどこに権限を持たせるかわかります。この仕組みを使って数値だけでどこに色を塗るかを判断するようにします。

数値は次のように定義することとします。

```math
\begin{matrix}
2^{0} & 2^{1} \\
2^{2} & 2^{3} \\
2^{4} & 2^{5} \\
\end{matrix}
\qquad

\begin{matrix}
1 & 2 \\
4 & 8 \\
16 & 32 \\
\end{matrix}
\qquad \\
```

例えば`HI`の時は`53`となります。

```math
HI \quad = \quad 
\begin{matrix}
o & - \\
o & - \\
o & o \\
\end{matrix} \quad 
= \quad 53
```

この`53`を2進数`110101`にすればなんとなく形が見えてきます。


流れはこんな感じ

- STEP1: 入力に対する数値を定義する
    - `A HI RU` => `[1, 53, 11]`
- STEP2: 数値を2進数にして点字配列を作る
    - `[1, 53, 11]` => `['000001', '110101', '001011']` => `['100000', '101011', '110100']`
- STEP3: 点字配列を点字の形で返す

## STEP1: 入力に対する数値を定義する

上に書いた数値をもとに、1音1音と数値との対応を定義しておきます。
母音のみの場合と子音と母音を組み合わせた場合と、例外の`ヤユヨワン`を定義します。

```ruby
BOIN = { 'A' => 1, 'I' => 5, 'U' => 3, 'E' => 7, 'O' => 6 }.freeze
SHIIN = { 'K' => 32, 'S' => 40, 'T' => 24, 'N' =>16, 'H' => 48, 'M' => 56, 'R' => 8 }.freeze
EXCEPTION = { 'YA' => 18, 'YU' => 50, 'YO' => 26, 'WA' => 16, 'N' => 56 }.freeze
COMBI = SHIIN.map { |k1, v1| BOIN.map { |k2, v2| [k1+k2, v1+v2] } }.flatten(1).to_h.freeze

# { 'A' => 1, 'I' => 5, ..., 'KA' => 33, 'KI' => 37, ..., 'N' => 56 }
WORD_NUM_DICT = BOIN.merge(COMBI, EXCEPTION).freeze

> puts num = WORD_NUM_DICT["HI"]
=> 53
```

## STEP2: 数値を2進数にして点字配列を作る

数値を2進数にするには`num.to_(2)`を使います。
しかし、このままでは`1`に対して`1`が出力されて不十分です。点字は6つの点でできているため`000001`となるよう`0`埋めする必要があります。`sprintf('%06d', num.to_s(2))`で6bits分使って表現できるようにしておきましょう。

そして、点字の並びを配列に見立てたとき、左上から右下に向かって桁が増える形に定義していました。

```math
\begin{matrix}
2^{0} & 2^{1} \\
2^{2} & 2^{3} \\
2^{4} & 2^{5} \\
\end{matrix}
\quad = [2^{0}, 2^{1}, 2^{2}, 2^{3}, 2^{4}, 2^{5}]
```

`000001`は左から右に向かって桁が上がっていくので、`100000`となるよう`reverse`で反転しておきましょう。

```ruby
> puts sprintf('%06d', 1.to_s(2)).reverse
=> '100000'
```

次にこの結果を点字文字列に変換します。
文字列を`chars`で一文字ずつの配列にして`0,1`を`-, o`に変換します。

```ruby
> puts '101011'.chars.map { _1.to_i.zero? ? '-' : 'o' }.join
=> 'o-o-oo'
```

ここまでの処理をループで組み合わせると`A HI RU`の入力に対して`['o-----', 'o-o-oo', 'oo-o--']`で出力されるところまでできるはずです。

## STEP3: 点字配列を点字の形で返す

あとは点字の形で返すだけですが、文字列で返すときは1行ずつとなるため、縦に`2×3`で並ぶ点字ブロックごとに表現するには、配列の各要素を2文字分(2bits分)ずつ出力して改行、というのを繰り返す必要があります。

`['o-----', 'o-o-oo', 'oo-o--']`は次のようになります。

```math
\begin{matrix}
o & - \\
- & - \\
- & - \\
\end{matrix}
\qquad

\begin{matrix}
o & - \\
o & - \\
o & o \\
\end{matrix}
\qquad

\begin{matrix}
o & o \\
- & o \\
- & - \\
\end{matrix}
```

まずは2文字ずつ分割。

```ruby
> puts ['o-----', 'o-o-oo', 'oo-o--'].map { _1.scan(/.{1,#{2}}/) }
[
  ['o-', '--', '--'],
  ['o-', 'o-', 'oo'],
  ['oo', '-o', '--']
]
```

次に、`transpose`を使って行と列を入れ替えます。以下のように変換されます。

```ruby
[                            [
  ['o-', '--', '--'],          ['o-', 'o-', 'oo'],
  ['o-', 'o-', 'oo'],   =>     ['--', 'o-', '-o'],
  ['oo', '-o', '--']           ['--', 'oo', '--']
].transpose                  ]
```

あとは、配列を文字列にしていい感じに成形して完成です。

```ruby 
> ['o-----', 'o-o-oo', 'oo-o--'].map { _1.scan(/.{1,#{2}}/) }.transpose.map { "#{_1.join(' ')}\n" }.join.chomp
=> o- o- oo
   -- o- -o
   -- oo --
```

## 完成

各処理をメソッドに切り出して全部まとめるとこんな感じ

```ruby
class TenjiMaker
  BOIN = { 'A' => 1, 'I' => 5, 'U' => 3, 'E' => 7, 'O' => 6 }.freeze
  SHIIN = { 'K' => 32, 'S' => 40, 'T' => 24, 'N' =>16, 'H' => 48, 'M' => 56, 'R' => 8 }.freeze
  EXCEPTION = { 'YA' => 18, 'YU' => 50, 'YO' => 26, 'WA' => 16, 'N' => 56 }.freeze
  COMBI = SHIIN.map { |k1, v1| BOIN.map { |k2, v2| [k1+k2, v1+v2] } }.flatten(1).to_h.freeze

  # { 'A' => 1, 'I' => 5, ..., 'KA' => 33, 'KI' => 37, ..., 'N' => 56 }
  WORD_NUM_DICT = BOIN.merge(COMBI, EXCEPTION).freeze

  def to_tenji(text)
    tenji_list = text.split(' ').map do |word|
      six_bits = convert_word_to_six_bits word
      convert_bits_to_tenji six_bits
    end
    convert_tenji_lsit_to_tenji_template tenji_list
  end

  private

  # ex. 'A' => '100000'
  def convert_word_to_six_bits word
    sprintf('%06d', WORD_NUM_DICT[word].to_s(2)).reverse
  end

  # ex. '100000' => 'o-----'
  def convert_bits_to_tenji bits_s
    bits_s.chars.map { _1.to_i.zero? ? '-' : 'o' }.join
  end

  def convert_tenji_lsit_to_tenji_template tenji_list
    tenji_list.map { _1.scan(/.{1,#{2}}/) }.transpose.map { "#{_1.join(' ')}\n" }.join.chomp
  end
end
```