シンプルな実装で挑む点字メーカーチャレンジ

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

## 記事の概要
自身がRubyを利用して約半年がたち、どの程度Rubyに慣れてきたのか確認するため参加させていただきました！
記事内では以下の項目について記述します。

1. ロジックを考えている時の動き
1. ロジックの解説
1. コードのアピールポイント

## 1. ロジックを考えている時の動き
自分が普段プログラムを組み立てる際にどのような考え方で進めたのか、仕様確認の観点の整理も含めロジックを考えている時の動きを記載します。

### 1-1. 仕様を確認する
プログラムは**「入力 -> 変換 -> 出力」**を行う仕組みという認識なので、与えられる入力と期待される出力の確認は最初に行います。

以下の順で仕様を整理します。

1. 入力の確認
2. 出力の確認
3. 例外となるパターンの有無、対応要否の確認

#### 1-1-1. 入力の確認
[今回のお題「点字メーカープログラム」](https://github.com/JunichiIto/tenji-maker-challenge#%E4%BB%8A%E5%9B%9E%E3%81%AE%E3%81%8A%E9%A1%8C%E7%82%B9%E5%AD%97%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0)
> このプログラムでは入力されたローマ字に対応する点字をテキストで出力します。

[入力テキストについて](https://github.com/JunichiIto/tenji-maker-challenge#%E5%85%A5%E5%8A%9B%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
> 入力テキストは半角大文字のローマ字とします。文字と文字の間は半角スペースで区切ってください。ローマ字表記の形式は訓令式とします。

[変換対象となるローマ字・ならないローマ字](https://github.com/JunichiIto/tenji-maker-challenge#%E5%A4%89%E6%8F%9B%E5%AF%BE%E8%B1%A1%E3%81%A8%E3%81%AA%E3%82%8B%E3%83%AD%E3%83%BC%E3%83%9E%E5%AD%97%E3%81%AA%E3%82%89%E3%81%AA%E3%81%84%E3%83%AD%E3%83%BC%E3%83%9E%E5%AD%97)
> 変換対象となるローマ字は以下のとおりです。
> ・あいうえお、かきくけこ、（省略）、やゆよ、らりるれろ、わ、ん

##### 内容の整理
* 入力は半角大文字のローマ字
    * 複数文字の場合は文字と文字の間に半角スペースが入る
    * 表記の形式は訓令式
    * 変換の対象となるローマ字は**「を」を除外した**50音

#### 1-1-2. 出力の確認
[今回のお題「点字メーカープログラム」](https://github.com/JunichiIto/tenji-maker-challenge#%E4%BB%8A%E5%9B%9E%E3%81%AE%E3%81%8A%E9%A1%8C%E7%82%B9%E5%AD%97%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0)
> このプログラムでは入力されたローマ字に対応する点字をテキストで出力します。
> ・ローマ字（に対応するひらがな）から点字への変換ルールは以下のwebページに説明されている内容に従うものとします。
> http://www.naiiv.net/braille/?tenji-sikumi
> ・点字の点が出力される部分にはoの文字を、出力されない部分には-の文字をそれぞれ出力してください。
> ・点字と点字の間は半角スペースで区切ってください。

Webページで点字の描画について確認し、以下のルールがあることを確認しました。

* 点字は2列x3行の点から構成されている
* 点字は、母音の点の位置、母音以外の点の位置を組み合わせて作る
    * 例：「ア」の点の位置と、「カ行」の点の位置を組み合わせると「カ」の点字になる
* 「ヤ行」「ワ行」は、例外的に母音の点の位置が変わる
* 「ン」のみ独自の点の位置が設定されている

##### 内容の整理
* 出力は2列x3行で`o`or`-`が配置された文字列
    * 複数文字の場合は点字と点字の間に半角スペースが入る
    * 変換の対象となるローマ字は**「を」を除外した**50音
* 点字のルール
    * 母音の点の位置、子音の点の位置を組み合わせて作る
    * 「ヤ行」「ワ行」は、例外的に母音の点の位置が変わる
    * 「ン」のみ独自の点の位置が設定されている

#### 1-1-3. 例外パターンの有無、対応要否の確認
[その他：異常系の考慮は不要](https://github.com/JunichiIto/tenji-maker-challenge#%E3%81%9D%E3%81%AE%E4%BB%96%E7%95%B0%E5%B8%B8%E7%B3%BB%E3%81%AE%E8%80%83%E6%85%AE%E3%81%AF%E4%B8%8D%E8%A6%81)
> 仕様を簡単にするため、以下のような異常系の入力を考慮する必要はありません。
> ・対応不要のローマ字（例 YA GI、BU TA）
> ・ヘボン式で入力されたローマ字（例 SHI MA U MA、KI TSU NE）
> ・ありえないローマ字や半角大文字以外の文字（例 XYZ、ne ko、羊）
> ・半角スペースで区切られていない（例 HIYOKO）
> ・半角スペースが2文字以上入っている（例 TO      RA）
> ・空文字列や文字列以外のオブジェクト（数値やnil）

##### 内容の整理
* 異常系の対応は不要

#### 1-1-4. 利用する技術を確認する
[実行環境について](https://github.com/JunichiIto/tenji-maker-challenge#%E5%AE%9F%E8%A1%8C%E7%92%B0%E5%A2%83%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
> ・Ruby 3.0で動作すること
> ・追加でインストールが必要なgemを使用しないこと
> ・Bundlerは使用しないこと

[自分が書いたコードの配置について](https://github.com/JunichiIto/tenji-maker-challenge#%E8%87%AA%E5%88%86%E3%81%8C%E6%9B%B8%E3%81%84%E3%81%9F%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E9%85%8D%E7%BD%AE%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
> ・実行用プログラムはlibディレクトリに置くこと。必要に応じてファイルを追加しても良い。
> ・テストコードはtestディレクトリに置くこと。必要に応じてファイルやテストパターンを追加しても良い。
> ・libディレクトリとtestディレクトリ以外のコードは触らないこと。
> ・雛形リポジトリに最初から配置しているテストコードも変更不可。

[提出の必須条件](https://github.com/JunichiIto/tenji-maker-challenge#%E6%8F%90%E5%87%BA%E3%81%AE%E5%BF%85%E9%A0%88%E6%9D%A1%E4%BB%B6)
> このリポジトリにあるテストコードがすべてパスすること。テストはrakeコマンドで全件実行することができる。

##### 内容の整理
* 実行環境
    * Ruby 3.0 のみ利用可能
* コード配置
    * `lib`, `test` ディレクトリにのみ自身のコードが配置可能
* 提出条件
    * `rake`コマンドで全テストが通過する

### 1-2. ロジックの検討
仕様検討でまとめた内容をもとに、必要となる機能を検討していきます。

#### 1-2-1. 全体の大まかな流れを考える
* 入力部分に必要な機能
    * 文字列をスペース区切りで読み取る機能
    * 母音・「ン」とそれ以外の文字の判断を行う機能
* 点字変換に必要な機能
    * 下記2.の項目参照
* 出力部分に必要な機能
    * 点字の配列を3行で構成された文字列に変換する機能

#### 1-2-2. メイン処理となる点字変換処理の方針を考える
「1-1-2.出力の確認」より、点字には以下のような性質が見て取れます。

1. 出力は**2列x3行**で`o`or`-`が配置された文字列
2. 母音の点の位置、子音の点の位置を組み合わせて文字を作る

「2個の要素を3回配置する」、「構造が決まったデータを組み合わせて、同じ構造のデータを作成する」という性質が見て取れるため、配列で点字を作成しようと考えました。

配列で上記の機能を実現するため、以下の機能を実装することとしました。

* 母音・「ン」の点字配列を作成する機能
* 子音の点字配列を作成する機能
* 母音、子音の点字配列を組み合わせる機能

## ロジックの解説
ロジックの根幹として、点字は**母音の点の位置、子音の点の位置を組み合わせて文字を作る**という性質を採用しました。

```
ローマ字
A  + K  = KA

点字
o-   --   o-
-- + -- = --
--   -o   -o
```
~~「ヤ」「ユ」「ヨ」「ワ」は性質が異なりますが、汎用的な処理を作成せず例外的に扱って対応することとしました。~~
（例外対象が4文字と少ないため、無理に共通処理を行うよりも別途処理を定義してコードの見通しを良くした方が良いと判断しました）
**(2021-12-19追記)例外の対象となる文字に「ン」を追加し、例外パターンをひとまとめにして実装しなおしました**

また、母音と子音の各点の位置をそれぞれ合成するため、インデックスで各要素のデータにアクセスできる配列を利用することとしました。

```
点字配列(2要素x3行)
A:  [['o', '-'], ['-', '-'], ['-', '-']]
K:  [['-', '-'], ['-', '-'], ['-', 'o']]
KA: [['o', '-'], ['-', '-'], ['-', 'o']]
       ↑                            ↑
     [0][0]                       [2][1]

-> 「A」と「K」の配列を確認し、'o'の箇所を合成した配列を作れば「KA」の配列になる！
```

点字に変換した後の出力は文字列で行う必要があるため、各文字の点字配列を1->2->3行の順で文字列に変換します。

```
点字配列の配列
[
 [['o', '-'], ['-', '-'], ['-', 'o']],  # 1文字目
 [['o', '-'], ['-', 'o'], ['-', '-']],  # 2文字目
 [['o', 'o'], ['-', 'o'], ['-', 'o']]   # 3文字目
]
      ↑           ↑            ↑
    1行目　 ,    2行目   ,    3行目

表示される点字
o- o- oo      ← 1行目の配列を各要素ごとに半角スペースを入れて文字列結合
-- -o -o      ← 2行目の配列を                〃
-o -- -o      ← 3行目の配列を                〃
```
また、改行を含める都合上、文字列への変換は点字配列ごとではなく**各点字配列の行ごと**に改行を追加する必要があります。

上記ロジックで引数のローマ字を点字に変換、表示することができました！

### 

## コードのアピールポイント
### 頑張ったところ
#### 1. テスト駆動開発
1. テストケースを描く
1. 機能の実装 -> テストを繰り返す
1. 問題なくなった段階でCommit

上記のフローを繰り返していて感じたことは、とにかくテストを書くのが難しい。。。！
テストを書くのが難しいという事は、自分が行いたい処理が明確になっていない証拠でもあるので、自分がどれだけ関数の設計をせずに実装にとりかかっていたのか痛感しました。。。

#### 2. yardによるドキュメンテーション
このツールを別の開発者が引き継ぐことを想定し、yardによるドキュメンテーションを行いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146452/928bf316-5196-5c07-6057-278d39d466e8.png)

### 苦労したところ
#### 点字配列の文字列への変換
複数の点字配列を表示するにあたり、1文字では考慮していなかった以下の考慮が必要となり、非常に苦しめられました。

* 1文字の点字と、複数文字の点字では配列の入れ子数が異なる
    * 1文字の点字の場合は2重配列、複数文字の点字の場合は3重配列
* 点字と点字の間に半角スペースを入れる
* 改行は最後の点字配列の時のみ行う

ロジックの解説でも記載している通り、各点字配列を行ごとに処理をすれば良いのですが、その考えに辿り着くまでものすごく時間がかかりました。。。

↓ 紆余曲折あって出来上がった点字表示の関数 ↓

```ruby
  def show_tenji(tenji_array)
    # 1文字だけの点字かどうか判断
    is_single_tenji = tenji_array[0][0].kind_of?(String)
    # 点字は3行で構成されているので、各行でループする
    tenji_rows = (0...TENJI_ROW_NUM).map { |row_idx|
      # 点字1文字の場合は配列をそのまま結合した文字列を取得
      next tenji_array[row_idx].join('') if is_single_tenji
      tenji_array.inject(''){ |tenji_row_str, tenji_row| 
        # それぞれの配列を文字として取得し、区切り文字として半角スペースを入れる
        tenji_row_str += tenji_row[row_idx].join('') + ' '
      }
    }
    # 点字が2文字以上ある場合、文字列の最後に半角スペースが入るため削除処理後に改行を含めて表示する
    tenji_rows.map{ |tenji_row| tenji_row.strip }.join("\n")
  end
```

### 工夫したところ
#### 点字の設定パターンを削減
50音すべてを定義するのではなく、母音・子音でそれぞれ点字の配列を作成し、それらの配列を組み合わせることで点字を作成しました。
~~また、母音・子音のパターンについてもすべてを定義するのではなく、既に定義されているパターンがあれば流用しています。~~
**(2021-12-19追記)この考え方は点字作成者側の意図を汲み取っていない実装のため、レビュー後に実装を変更しております。**


```
例：「い」の点字の「う」の点字を組み合わせると「え」の点字になる

い + う = え
↓    ↓    ↓
o-   oo   oo
o- + -- = o-
--   --   --
```


#### for, each構文を利用しない
ソースの記述量を減らすため、forやeachを利用せずに処理を実装しました。
以下のルールでメソッドを使い分けて対応しています。
* 配列から新たな配列を作成する -> `map`
* 各配列で比較を行い、比較結果として新たな配列を作成する -> `inject`

#### 機能を最小単位でクラス化
機能を最小単位でクラス化し、共通で利用する部分はモジュール化しました。
1クラス1処理で設計しているため、クラスの利用時にどの関数を利用するか迷わず利用可能です。

### 自慢したいところ
#### コードの可読性
分かりにくい箇所にはコメントを入れ、関数利用方法など次の開発者に向けて情報を残しました。
まクラス構造も可能な限り簡潔にしたので、クラス間の連携や利用も迷うことなくできると考えています！

## 伊藤さんへメッセージ
RubyやRailsでハマって検索した際、伊藤さんがハマりどころについて記事にまとめられていることが多く、本当に助けていただいています。
記載いただいている内容も細かく理由付けがされており、読者側の気持ちや不安を的確に解消する記事が書ける力、尊敬しております。

今回、ドキドキしながら参加させていただきましたが、改めて自分の開発手法について考える良い機会になりました。
素敵なイベントを企画してくださり、ありがとうございます。

自分も伊藤さんを目指して、少しでも困った人を助けられる記事を書けるよう精進します！

## (2021-12-19追記)コードレビューを受けて
丁寧なレビューをいただき、ありがとうございました！
自身の実装だけでなく、何のためにプログラムを作っているのかという面も含め、めちゃくちゃ色々なことに気付かさせていただきました。
レビューいただける喜びと、この学びを忘れないため、自身のコードの改修を行いました！

https://github.com/watame/tenji-maker-challenge/pull/1

コードレビューを受け、自身のコードには大きく3つの課題点があると感じました。

#### 点字の背景や意図を汲み取っていなかった
最大の課題だと思います。
コードレビューや他の方の実装を見て気付いたのですが、自分のコードには点字に対する考えがすっぽりと抜け落ちていました。
伊藤さんから提供された仕様を満たす「だけ」で実装を進めてしまっており、パズル的にパターンを当てはめる実装をしている箇所が多くなっていました。
そのため点字のルールとして明示的に1-6の点が定義されているにもかかわらず、プログラムでは2x3の2重配列を前提として管理し点字の点が全く出てきません。
そのため、仮に点字を利用している人と仕様を詰めることになった際、相手の示す番号とプログラムの管理する番号に差異が発生し、認識のすり合わせに障壁が生じることになってしまいます。
最低でも前提となる仕様で利用している数値はそのままプログラムに反映させるべきでした。
##### 改修内容
* 'o'を打つ位置を点字の番号で表現する
  * 番号は固定値となるため、定数を利用する

#### 設計が悪い
自分はわかりやすいかと思い「1文字で成立する文字」「2文字で成立する文字」「ローマ字を点字に変換する」クラスを定義していましたが、それぞれのクラスを単独で利用するユースケースはなく、クラスを分ける必要はありませんでした。
そのため、点字の生成を行うクラス、点字を出力するクラスの2クラスで責務を分けて再設計しました。
また、例外パターンは別途処理すると記載しているにもかかわらず「n」を例外パターンとして定義できていなかったのも良くなかったです。

##### 改修内容
* クラス構造の変更
  * 点字生成用のクラス
     * 母音、子音、ローマ字、母音子音の合体
  * 点字出力用のクラス
     * 点字表示
* 例外処理の見直し

#### 純粋にrubyの知識が足りない
bit演算を行うというアプローチをそもそも考慮できていませんでした。
bit演算のOR演算で条件分岐をなくせるのは目から鱗でした。。。

##### 改修内容
* bit演算を用いたマージ処理の実装

### 改修後の感想
今回の改修を通じて、自身のプログラムの甘いところ、プログラムを通じて価値を提供したい方を意識してコードに落とし込む意味を学ぶことが出来ました。
貴重な経験をさせていただき、本当にありがとうございました！