【Ruby】社内報を作成してくれるクラスを作成してみた。を改良してみた。

## はじめに

毎月先輩から出していただいた課題に取り組んでいます、 mi0です。
3月は、2月に作成した社内報を作成するクラスに、さらに機能を追加するという課題にチャレンジしました。
この記事は要件定義〜レビューをいただくまでの過程を纏めた備忘録です。
こうやったらもっとよくなる、などのご指摘があればコメント頂けると嬉しいです！


過去の記事はこちら！↓

- [「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題解いてみた。](https://qiita.com/mi0/items/738b78fe5e7d10df34ae)
- [Ruby ボウリング問題解いてみた。](https://qiita.com/mi0/items/a8d4b90696ae4404e742)
- [Ruby 価値が大きくなる組み合わせ問題 解いてみた](https://qiita.com/mi0/items/194f3dcb2411b4978fec)
- [Ruby初心者向けのプログラミング問題のカラオケマシン問題にハモリ機能を追加してみた](https://qiita.com/mi0/items/ffffeca9ed5333bf984b)
- [Ruby ボウリング問題解いてみた。をパワーアップしてみた。](https://qiita.com/mi0/items/d531916b4a79c696ecdb)
- [【Ruby】社内報を作成してくれるクラスを作成してみた。](https://qiita.com/mi0/items/d22b539adc6be5565fc9)

## 登場人物
- 私
    - 気付いたら三年目になっていた（記事執筆時点）。気付いたら課題も10回目になっていた。気付いたら割とコードが書けるようになっていた。気付いたら文章を書くのも割と慣れてきていた。
- ラテ太郎（アイコン参照）
    - 初心に返りたかったという理由で召集された、私の心の中に住んでいる妖精。最後の砦。
- タピオカ先輩
    - ラテ太郎の先輩妖精（出演２回目）。プログラミングが得意。「なければ作ればいいじゃない」ムーブがかっこいい。 

## 要件を定義する
### 前回までの仕様（詳細は[前回の記事](https://qiita.com/mi0/items/d22b539adc6be5565fc9)参照）
- 社内報の本文を作成する
    - テンプレートに、各社員の原稿を嵌め込んでいき、社内報の本文を作成する

前回はシンプルに社内報を作成する機能だけを実装しました。
今回は更に必要そうな機能を盛り込んでいきます。

### 今回追加する機能の仕様について考える
#### 今回追加する機能
タピオカ先輩との打ち合わせで、今回追加することになった機能は以下の２つです。

- `ひとりごと`の追加（既存機能の改修）
    - `ひとりごと`という、各社員に書いてもらった`呟き`を匿名で載せるコーナーを社内報に設けているので、そのコーナーを本文に追加できるよう改修する
-  未提出者をリストアップする`トレース`機能の追加
    -  前回、未提出者については社内報本文内で、`未提出`という表記を使って表示していた。未提出に該当する人物についてはリストアップし、提出を依頼する必要があるため、`トレース`機能を実装することでリストアップを自動化する

これらについては、現在全て`手作業`で行っている上、思っている以上に時間がかかっている作業です。
これらの作業を自動化できたらかなり作業時間を時短できるため、今回追加することになりました。

#### より具体的な仕様を考えて行く 
##### ひとりごとの追加

私「とりあえずひとりごとを今どんな風に編集しているか纏めてみよっか」

ラテ「`現状把握`は大事だね。`現状`が分からないと`どうやって改善するか`が考えられないから。」

私「でしょ〜何事も現状把握が大事！で、一旦まとめるとこんな感じかな。」

- 原稿はテキストファイル。名称は統一して`hitorigoto`で提出するよう依頼している。
- 全て`hitorigoto.txt`として手元に届くため、保存時は`hitorigoto1`のように、ナンバリングして保存する。
- 提出されているひとりごとの数だけ本文には反映する（具体例は以下に纏める）
- １件も提出がない場合は`ひとりごと`の見出しごと消す
- 現在は以下のようなフォーマットで原稿本文末尾に挿入している。

```text:３件ひとりごとが提出されている場合（デフォルト）
ひとりごと

①ひとりごと本文

②ひとりごと本文

③ひとりごと本文
```

```text:２件ひとりごとが提出されている場合
ひとりごと

①ひとりごと本文

②ひとりごと本文
```

私「こんな感じかな〜。大筋は今のままでいいと思うんだよね。文頭の数字の表示はちょっと考えるけど……。一旦シンプルに`1`とかでもいい気がしなくはない、かな……うーん。」

ラテ「ふんふん、その辺はタピオカ先輩と相談で良いんじゃないかな？多分`I18n`の仕組みが今は入ってないから、とりあえずは普通の数字で定義しておいて、ゆくゆく改良みたいな形になりそうな気がする」

私「そっか、確かに。じゃあ一旦これで纏めておこう。」


##### トレース機能の追加

私「次、トレース機能だね。これも結構大変なんだよ〜。」

ラテ「今はどんな感じでやってるの？」

私「んとね、こんな感じ。」

- 提出された原稿を１つのディレクトリに全て保存ずる
- 名簿を元に、提出している人をチェックしていく
- チェックがついていない人を未提出としてリストアップする
    - 但し、産休中の方などは上記リストから除外する
- リストアップするときは`・[名前]さん`のような形式でリストアップする

私「このチェックのところが大変なんだよね〜。前の実装で作った`未提出`として出力する処理がうまいこと使えたらいいな〜って思ってる。」

ラテ「ふんふん、なるほど……。とりあえず`#trace`みたいなメソッドを追加するのが大前提だね。」

私「そうだね。メンバー一覧の項目に`トレース対象かどうか`みたいな`フラグ`を足した方がいいかも」

ラテ「なるほど、トレース対象だったらリストに追加するし、そうじゃなかったら含めない、みたいにするわけだね？」

私「そうそう、それが一番イメージに近いかなぁ。これなら、後から`Rails`アプリとして作り変えるってなった時もいい感じにできそうだし。」

ラテ「確かに。じゃあ、前のメンバーリストはヘッダーなしのCSVとして作ってたから、今回はヘッダーありにする方が分かり易そうだね。突然リストに`true`・`false`が書いてあると困惑するし。」

私「あ〜、じゃあそこは修正かな。一旦それで纏めてみよっか。」

### 完成した要件

#### 社内報作成アプリ v2

##### 問題

- 社内報作成機能に `ひとりごと` 登録機能を追加する
- 未提出者をリストアップする `トレース` 機能を追加する

##### 社内報の元データ

- `db/template.txt` ・・・社内報のテンプレート。
- `db/member_list.csv` ・・・各メンバーの名前とメールアドレスが記載されている。
- `db/manuscripts` ・・・ `年月` 毎にディレクトリを作成し、直下にその月の各メンバーの原稿を格納する。

##### 要求仕様

###### ひとりごと登録機能(#generate の改修)

- ひとりごとは `hitorigoto1.txt` のように、1〜n までの数字でナンバリングされたテキストファイルである
- ひとりごとは件数分表示される
- ひとりごとの並び順はテキストファイルの番号が若い順とする
  - ナンバリングされていないテキストファイルは反映されない
- テンプレートの `[hitorigoto]` に以下のような形式でひとりごとを表示する
  - 見出しには `ひとりごと` と表示する
  - ひとりごとのフォーマットは `[n] 本文` とする
  - ひとりごとが 1 件も存在しない場合は `空行にする`

```
例） 202002を引数に指定していて、ひとりごとが２件提出されている場合

・ラテ太郎
お疲れ様です、ラテ太郎です。
サンプル
サンプル
サンプル

・ラテ子
お疲れ様です、ラテ子です。
サンプル
サンプル
サンプル
サンプル

・沖漬け先輩
サンプル
サンプル
サンプル
サンプル
サンプルサンプルサンプルサンプルサンプルサンプルサンプル

・ラテ二郎
未提出

ひとりごと

1 ひとりごと

2 ひとりごと

例） 202002を引数に指定していて、ひとりごとが1件も提出されていない場合
・ラテ太郎
お疲れ様です、ラテ太郎です。
サンプル
サンプル
サンプル

・ラテ子
お疲れ様です、ラテ子です。
サンプル
サンプル
サンプル
サンプル

・沖漬け先輩
サンプル
サンプル
サンプル
サンプル
サンプルサンプルサンプルサンプルサンプルサンプルサンプル

・ラテ二郎
未提出

```

###### トレース機能

- 未提出者をリストアップする `#trace` を実装する
- フォーマットは `・名前さん` とする
- `db/member_list.csv` の `excluded` 欄が `true` の場合はリストから除外する


```ruby:app/internal_newsletter.rb
require 'csv'

class InternalNewsletter
  MEMBER_LIST = CSV.read('db/member_list.csv')
  NEWSLETTER = File.read('db/template.txt')
  NOT_SUBMITTED = '未提出'.freeze

  def initialize(datestr)
    @datestr = datestr
    @dirpath = "db/manuscripts/#{@datestr}"
  end

  def generate
    raise ArgumentError unless Dir.exist?(@dirpath)

    MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter)) + "\n"
  end

  def trace
    ''
  end

  private

  def newsletter(text, member)
    name, keyword = split_name_keyword(member)
    fullpath = "#{@dirpath}/#{@datestr}#{keyword}.txt"
    body = File.exist?(fullpath) ? File.read(fullpath).strip : NOT_SUBMITTED

    text.gsub(
      /\[#{keyword}\]/,
      <<~NEWSLETTER.chomp
        ・#{name}
        #{body}
      NEWSLETTER
    )
  end

  def split_name_keyword(member)
    name, email = member
    _, keyword = email.match(/\A(\w+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/).to_a
    [name, keyword]
  end
end
```

```db/member_list.csv
name,email,excluded
ラテ太郎,rate_taro@sample.com,true
ラテ子,rate_ko@sample.com,false
沖漬け先輩,okiduke_senpai@sample.com,true
ラテ二郎,rate_jiro@sample.com,false
タラバガニ先輩,tarabagani_senpai@sample.con,true
こっぺがに,koppegani@sample.com,false
```

```db/template.txt
社内報

[rate_taro]

[rate_ko]

[okiduke_senpai]

[rate_jiro]

[tarabagani_senpai]

[koppegani]

[hitorigoto]
```

```text:db/manuscripts/202002内のファイル構成
- 202002hitorigoto1.txt
- 202002hitorigoto2.txt
- 202002okiduke_senpai.txt
- 202002rate_ko.txt
- 202002rate_taro.txt
```

## 要件を元に実際に作成していく

私「よし！じゃあ今度は出来た仕様を元にそれぞれの機能を作っていこう！」

私「その前に簡単に手直ししておこっか」

```diff:app/internal_newsletter.rb
require 'csv'
 
 class InternalNewsletter
-  MEMBER_LIST = CSV.read('db/member_list.csv')
+  MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)

   # 略
 end
```

私「これでとりあえずメンバーリストがいい感じで読み込めるようになった！次、ひとりごとの追加ができるようにしていこう！」

### ひとりごとの追加

私「とりあえずいつもの通りイメージを文章で纏めてみよう」

```ruby:app/internal_newsletter.rb
require 'csv'
 
class InternalNewsletter
  MEMBER_LIST = CSV.read('db/member_list.csv')
  MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
  NEWSLETTER = File.read('db/template.txt')
  NOT_SUBMITTED = '未提出'.freeze

  def initialize(datestr)
    @datestr = datestr
    @dirpath = "db/manuscripts/#{@datestr}"
  end

  # 一部省略・必要箇所のみ抜粋

  def generate
    MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter)) + "\n"
    # ここで、原稿の本文を作成する＋ひとりごとを追加してくれるようなメソッドを呼ぶ
    # newsletterメソッドの中でいい感じにできる？
  end

  private

  def newsletter(text, member_info)
    name, keyword = split_name_keyword(member_info)
    fullpath = file_fullpath(keyword)
    body = File.exist?(fullpath) ? File.read(fullpath).strip : NOT_SUBMITTED

    text.gsub(
      /\[#{keyword}\]/,
      <<~NEWSLETTER.chomp
        ・#{name}
        #{body}
      NEWSLETTER
    )

   # ここでひとりごとをよしなにする？
  end

  def hitorigoto
    # ひとりごとの欄に表示したい内容を文字列で成形していく
    # フォーマットは `[インデックス] [本文]\n`（これは従来の実装と同様にヒアドキュメント使う）
    # ひとりごとの数だけ上記のフォーマットの文字列を作って、配列に格納する。
    # 上記配列が空じゃない（ひとりごとが存在する）場合は `ひとりごと`の見出しを配列の頭に挿入する
    # 上記配列が空じゃない（ひとりごとが存在する）場合
      # 上の配列を改行でjoinしたものをテンプレートの[hirotigoto]の部分に突っ込む
    # 上記配列が空（ひとりごとが存在しない）場合
      # テンプレートの[hitorigoto]を空行にする
  end
end
```

私「う〜〜〜ん一旦こんな感じかな？」

私「本当にそれでできるん？みたいな部分はあるけど、一旦これで作ってみようか。」

```diff:app/internal_newsletter.rb
 require 'csv'
  
 class InternalNewsletter
   MEMBER_LIST = CSV.read('db/member_list.csv')
   MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
   NEWSLETTER = File.read('db/template.txt')
   NOT_SUBMITTED = '未提出'.freeze
 
   def initialize(datestr)
     @datestr = datestr
     @dirpath = "db/manuscripts/#{@datestr}"
   end
 
   # 一部省略・必要箇所のみ抜粋
 
   def generate
     MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter)) + "\n"
     # ここで、原稿の本文を作成する＋ひとりごとを追加してくれるようなメソッドを呼ぶ
     # newsletterメソッドの中でいい感じにできる？
   end
 
   private
 
   def newsletter(text, member_info)
     name, keyword = split_name_keyword(member_info)
     fullpath = file_fullpath(keyword)
     body = File.exist?(fullpath) ? File.read(fullpath).strip : NOT_SUBMITTED
 
     text.gsub(
       /\[#{keyword}\]/,
       <<~NEWSLETTER.chomp
         ・#{name}
         #{body}
       NEWSLETTER
     )
-    # ここでひとりごとをよしなにする？
+    text.gsub('[hitorigoto]', hitorigoto)
   end
 
   def hitorigoto
     # ひとりごとの欄に表示したい内容を文字列で成形していく
     # フォーマットは `[インデックス] [本文]\n`（これは従来の実装と同様にヒアドキュメント使う）
     # ひとりごとの数だけ上記のフォーマットの文字列を作って、配列に格納する。
+    body = hitorigoto_files.each_with_object([]).with_index(1) do |(file, array), index|
+      array << <<~NEWSLETTER.chomp
+        #{index} #{File.read(file)}
+
+      NEWSLETTER
+    end
     # 上記配列が空じゃない（ひとりごとが存在する）場合は `ひとりごと`の見出しを配列の頭に挿入する
+    body.unshift("ひとりごと\n") unless body.empty?
     # 上記配列が空じゃない（ひとりごとが存在する）場合
       # 上の配列を改行でjoinしたものをテンプレートの[hirotigoto]の部分に突っ込む
     # 上記配列が空（ひとりごとが存在しない）場合
       # テンプレートの[hitorigoto]を空行にする
+    body.empty? ? "\n" : body.join("\n")
   end

+  def hitorigoto_files
+    Dir.glob("db/manuscripts/#{@datestr}/*.txt").select do |file|
+      file.match?(%r{\Adb/manuscripts/#{@datestr}/#{@datestr}hitorigoto[0-9]+\.txt\z})
+    end
+  end
 end
```

私「ど、どうかね？（`ひとりごと`のファイルを厳選する処理に苦労したって顔）」

※送付されてきた`ひとりごと`の中から、正常なファイル名のみ抜粋する、という処理を`hitorigoto_files`で行って、あとはそれぞれのファイルを読み込んで成形する方式で今回はやってみました。

ラテ「（微妙な顔）」

私「え！？！？！！？！なにその顔」

ラテ「動かしてみたら？」

私「ヘァ〜〜〜〜〜？？？動かしてみよ」

```text:実行結果
社内報

[rate_taro]

[rate_ko]

[okiduke_senpai]

[rate_jiro]

[tarabagani_senpai]

[koppegani]

ひとりごと

1 とにかくこの世に生まれたからには、何か1つ足跡を残したい

# 以下略
```

私「？？？？？？？？？？？？？？？？」

私「あれ？ん？`個人の原稿が置換されてない`、ね？」

私「（シンキングタイム）」

私「そういえば`inject`使ってるじゃん………」

私「じゃあ`newsletter`改良作戦は無理そうだな……なるほどね……盲点でした……。」

ラテ「置換するっていう方向性は良さそうだから、置換するタイミングを変えてみるのはどう？」

私「なるほど……従来の実装で`本文の置換`は出来てるから、あとは`ひとりごと`のところを独自に置き換えればいい……から、本文の置換が終わった後に`ひとりごと`の欄を作る？」

```diff:app/internal_newsletter.rb
 require 'csv'
  
 class InternalNewsletter
   MEMBER_LIST = CSV.read('db/member_list.csv')
   MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
   NEWSLETTER = File.read('db/template.txt')
   NOT_SUBMITTED = '未提出'.freeze
 
   def initialize(datestr)
     @datestr = datestr
     @dirpath = "db/manuscripts/#{@datestr}"
   end
 
   # 一部省略・必要箇所のみ抜粋
 
   def generate
-    MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter)) + "\n"
     # ここで、原稿の本文を作成する＋ひとりごとを追加してくれるようなメソッドを呼ぶ
     # newsletterメソッドの中でいい感じにできる？
+    convert_hitorigoto(MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter)))
   end
 
   private
 
   def newsletter(text, member_info)
     name, keyword = split_name_keyword(member_info)
     fullpath = file_fullpath(keyword)
     body = File.exist?(fullpath) ? File.read(fullpath).strip : NOT_SUBMITTED
 
     text.gsub(
       /\[#{keyword}\]/,
       <<~NEWSLETTER.chomp
         ・#{name}
         #{body}
       NEWSLETTER
     )
-    # ここでひとりごとをよしなにする？
-    text.gsub('[hitorigoto]', hitorigoto)
   end
 
-  def hitorigoto
+  def convert_hitorigoto
     # ひとりごとの欄に表示したい内容を文字列で成形していく
     # フォーマットは `[インデックス] [本文]\n`（これは従来の実装と同様にヒアドキュメント使う）
     # ひとりごとの数だけ上記のフォーマットの文字列を作って、配列に格納する。
     body = hitorigoto_files.each_with_object([]).with_index(1) do |(file, array), index|
       array << <<~NEWSLETTER.chomp
         #{index} #{File.read(file)}
 
       NEWSLETTER
     end
     # 上記配列が空じゃない（ひとりごとが存在する）場合は `ひとりごと`の見出しを配列の頭に挿入する
     body.unshift("ひとりごと\n") unless body.empty?
     # 上記配列が空じゃない（ひとりごとが存在する）場合
       # 上の配列を改行でjoinしたものをテンプレートの[hirotigoto]の部分に突っ込む
     # 上記配列が空（ひとりごとが存在しない）場合
       # テンプレートの[hitorigoto]を空行にする
-    body.empty? ? "\n" : body.join("\n")
+    keyword = '[hitorigoto]'
+    body.empty? ? text.gsub("#{keyword}\n", '') : text.gsub(keyword, body.join("\n"))
   end

   def hitorigoto_files
     Dir.glob("db/manuscripts/#{@datestr}/*.txt").select do |file|
       file.match?(%r{\Adb/manuscripts/#{@datestr}/#{@datestr}hitorigoto[0-9]+\.txt\z})
     end
   end
 end
```

私「こう、かな……」

私「うん、ちゃんと置換されるしよさみちゃん！ひとりごとは一旦これでいこう」

※コメントは後で消しました。

### トレース機能の実装

私「次、トレース機能！」

私「これもとりあえずイメージを纏めておこっか」


```ruby:app/internal_newsletter.rb
 require 'csv'
  
 class InternalNewsletter
   MEMBER_LIST = CSV.read('db/member_list.csv')
   MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
   NEWSLETTER = File.read('db/template.txt')
   NOT_SUBMITTED = '未提出'.freeze
 
   def initialize(datestr)
     @datestr = datestr
     @dirpath = "db/manuscripts/#{@datestr}"
   end

   # 一部省略・必要箇所のみ抜粋

   def trace
     ''
     # ディレクトリが存在してなかったら例外
     # メンバーリストを一件ずつチェックしていく
     # 該当のファイルが存在していればスルー
     # 該当のファイルが存在していない場合
       # 除外フラグが立っていたらスル──
       # 除外フラグが立っていない場合はリストに追加する
     # リストを結合する
   end
 end
```

私「こっちは結構シンプルそうだね？」

ラテ「確かに、存在チェックとフラグのチェックだけだもんね」

私「よし、じゃあこれでパパッと作ってみよう」

```diff:app/internal_newsletter.rb
  require 'csv'
   
  class InternalNewsletter
    MEMBER_LIST = CSV.read('db/member_list.csv')
    MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
    NEWSLETTER = File.read('db/template.txt')
    NOT_SUBMITTED = '未提出'.freeze
  
    def initialize(datestr)
      @datestr = datestr
      @dirpath = "db/manuscripts/#{@datestr}"
    end
 
    # 一部省略・必要箇所のみ抜粋
 
    def trace
      ''
      # ディレクトリが存在してなかったら例外
+     raise ArgumentError unless Dir.exist?(@dirpath)
      # メンバーリストを一件ずつチェックしていく
+     MEMBER_LIST.each_with_object([]) do |member_info, array|
      # 該当のファイルが存在していればスルー
+       next if File.exist?(file_fullpath(member_info['email']))
      # 該当のファイルが存在していない場合
        # 除外フラグが立っていたらスル──
+       next if member_info['excluded'] == 'true'
        # 除外フラグが立っていない場合はリストに追加する
+       array << trace_name_format(member_info['name'])
      # リストを結合する
+     end.join
    end
+
+  def file_fullpath(keyword)
+    "#{@dirpath}/#{@datestr}#{keyword}.txt"
+  end
+
+   def trace_name_format(name)
+     <<~NAME
+       ・#{name}さん
+     NAME
+   end
  end
```

私「こんな感じかね？」

私「`#generate`の方とも共通で使えそうなメソッドも足してみたよ」

私「ここは本当に読み通りで実装できてよかった〜〜〜！！！」

私「というか、私は今まで`数行コード書けば後は実行するだけで終わり`な作業を全部手作業でやってたってことよね……？`時間もったいな……`」

私「きっとこういう気持ちが湧くから`人は自動化できるツール`を作っていくんだろうな……」

ラテ「コードが書けない時は`人力でやった方が早い`からツールを作ろうって気持ちが湧かないけど、今はコードが書けるようになってきたから、`この程度のコードでいいなら作った方が早い`って意識に変わってるってことだね。」

私「これが極まっていくと、タピオカ先輩みたいに`ないなら作ればいいじゃない`的なマリーアントワネット方式になっていくんだろうなぁ。」

私「よしよし、後は共通メソッドが使える場所をリファクタして……完成！」


〜〜〜そうして出来上がったものがこちら〜〜〜

```ruby:app/internal_newsletter.rb
require 'csv'

class InternalNewsletter
  MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
  NEWSLETTER = File.read('db/template.txt')
  NOT_SUBMITTED = '未提出'.freeze

  def initialize(datestr)
    @datestr = datestr
    @dirpath = "db/manuscripts/#{@datestr}"
  end

  def generate
    raise ArgumentError unless Dir.exist?(@dirpath)

    convert_hitorigoto(MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter)))
  end

  def trace
    raise ArgumentError unless Dir.exist?(@dirpath)

    MEMBER_LIST.each_with_object([]) do |member_info, array|
      next if File.exist?(file_fullpath(member_info['email']))
      next if member_info['excluded'] == 'true'

      array << trace_name_format(member_info['name'])
    end.join
  end

  private

  def newsletter(text, member_info)
    name, keyword = split_name_keyword(member_info)
    fullpath = file_fullpath(keyword)
    body = File.exist?(fullpath) ? File.read(fullpath).strip : NOT_SUBMITTED

    text.gsub(
      /\[#{keyword}\]/,
      <<~NEWSLETTER.chomp
        ・#{name}
        #{body}
      NEWSLETTER
    )
  end

  def split_name_keyword(member_info)
    _, keyword = member_info['email'].match(/\A(\w+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/).to_a
    [member_info['name'], keyword]
  end

  def file_fullpath(keyword)
    "#{@dirpath}/#{@datestr}#{keyword}.txt"
  end

  def convert_hitorigoto(text)
    body = hitorigoto_files.each_with_object([]).with_index(1) do |(file, array), index|
      array << <<~NEWSLETTER.chomp
        #{index} #{File.read(file)}

      NEWSLETTER
    end

    body.unshift("ひとりごと\n") unless body.empty?

    keyword = '[hitorigoto]'
    body.empty? ? text.gsub("#{keyword}\n", '') : text.gsub(keyword, body.join("\n"))
  end

  def hitorigoto_files
    Dir.glob("db/manuscripts/#{@datestr}/*.txt").select do |file|
      file.match?(%r{\Adb/manuscripts/#{@datestr}/#{@datestr}hitorigoto[0-9]+\.txt\z})
    end
  end

  def trace_name_format(name)
    <<~NAME
      ・#{name}さん
    NAME
  end
end
```

## レビューをいただく

タピ「じゃあレビューをしていくね」

私「よろしくお願いします！（今回は割と自信があるぞ！！）」

タピ「まず`俺が用意したテストが落ちました`」

私「？」

私「えっ」

タピ「具体的に言うと、`ひとりごとの並び順が番号の若い順になってなかった`ね。」

私「？？？？？うそ」

私「私のローカルでは番号の若い順で並んでて、………あ〜〜〜〜〜〜！！！！`システムの設定によって並び順は変わるから`私のローカルでの並び順が正常でも、`他の環境で必ずその並び順になるとは限らない`！！！！！」

私「あ〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜（業務中に同じようなことをやったことがある記憶がある顔）」

私「明示的に並び替えないとダメですね……」

タピ「そうです:white_flower:」

私「:tired_face:」

タピ「後はね、ここ」

```ruby:app/internal_newsletter.rb
   def trace
     raise ArgumentError unless Dir.exist?(@dirpath)

     MEMBER_LIST.each_with_object([]) do |member_info, array|
       next if File.exist?(file_fullpath(member_info['email'])) # <= ここ
       next if member_info['excluded'] == 'true'

       array << trace_name_format(member_info['name'])
     end.join
   end
```

タピ「ここで渡したいのって`メールアドレスのドメイン部以外`だよね？」

タピ「メールアドレスそのまま渡してる」

私「:tired_face:」

私「ヒェ」

タピ「後はね、ここ」

```ruby:app/internal_newsletter.rb
  def hitorigoto_files
    Dir.glob("db/manuscripts/#{@datestr}/*.txt").select do |file|
      file.match?(%r{\Adb/manuscripts/#{@datestr}/#{@datestr}hitorigoto[0-9]+\.txt\z}) # <= ここ
    end
  end
```

タピ「この`db/manuscripts/#{@datestr}`は`@datestr`に入ってる」

私「:tired_face::tired_face::tired_face:」

私「（ぴえん）」

私「この辺は見直し不足ですね……」

タピ「そうだね」

タピ「後はCSVから値を取得する時、`Boolean`は`Boolean`として取得できたらよかったかな」

私「あ〜〜〜〜〜〜」

タピ「とりあえず俺のコード見ていこうか」

```ruby:app/internal_newsletter.rb
require 'csv'

class InternalNewsletter
  MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
  NEWSLETTER = File.read('db/template.txt')
  NOT_SUBMITTED = '未提出'.freeze
  MANUSCRIPTS_PATH = 'db/manuscripts'.freeze
  HITORIGOTO_KEYWORD = 'hitorigoto'.freeze

  def initialize(datestr)
    @datestr = datestr
    @dirpath = "#{MANUSCRIPTS_PATH}/#{@datestr}"
  end

  def generate
    valid!

    manuscripts = MEMBER_LIST.inject(NEWSLETTER, &method(:newsletter))

    allocate_hitorigoto(manuscripts)
  end

  def trace
    valid!

    targets = unsubmitted_names
    return if targets.empty?

    format_unsubmitted_target(targets) + "\n"
  end

  private

  def valid!
    return if Dir.exist?(@dirpath)

    raise ArgumentError
  end

  def newsletter(text, member)
    name, keyword = split_name_keyword(member)
    fullpath = "#{@dirpath}/#{@datestr}#{keyword}.txt"
    body = File.exist?(fullpath) ? File.read(fullpath).strip : NOT_SUBMITTED

    text.gsub(
      /\[#{keyword}\]/,
      <<~NEWSLETTER.chomp
        ・#{name}
        #{body}
      NEWSLETTER
    )
  end

  def split_name_keyword(member)
    name, email = member.values_at('name', 'email')
    _, keyword = email.match(/\A(\w+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/).to_a
    [name, keyword]
  end

  def allocate_hitorigoto(manuscripts)
    files = Dir["#{@dirpath}/#{@datestr}#{HITORIGOTO_KEYWORD}*.txt"]

    if files.any?
      replace_hitorigoto_manuscripts(manuscripts, files)
    else
      remove_hitorigoto_space(manuscripts)
    end
  end

  def replace_hitorigoto_manuscripts(manuscripts, files)
    bodies = files.sort.map.with_index(1, &method(:hitorigoto)).join("\n")

    manuscripts.sub(
      /\[#{HITORIGOTO_KEYWORD}\]\n/,
      <<~HITORIGOTO.chomp
        #{I18n.t(:hitorigoto)}

        #{bodies}
      HITORIGOTO
    )
  end

  def hitorigoto(file, index)
    "#{index} #{File.read(file).strip}" + "\n"
  end

  def remove_hitorigoto_space(manuscripts)
    manuscripts.gsub(/\n\[#{HITORIGOTO_KEYWORD}\]\n/, '')
  end

  def unsubmitted_names
    MEMBER_LIST.map do |member_list|
      next if member_list['excluded']

      name, keyword = split_name_keyword(member_list)
      next if File.exist?("#{@dirpath}/#{@datestr}#{keyword}.txt")

      name
    end.compact
  end

  def format_unsubmitted_target(targets)
    targets.map do |target|
      I18n.t('trace.name_list_format', name: target)
    end.join("\n")
  end
end
```

私「アヒョ……（動揺のあまり溢れ出るグーフ●）」

私「すみませんちょっと夢の国に」

私「あ〜〜〜`#allocate_hitorigoto`めっちゃいいですね……」

私「`ひとりごと`が存在しない時は`ファイルのソート処理`とか無駄だし、一旦該当ファイルを取得しておいて、必要に応じて後からソートする、とか」

私「`#replace_hitorigoto_manuscripts`とかの感じもいいですね……シンプルで分かりやすい。」

私「（`map`の時の`&method`でいい感じに引数渡してメソッド実行するやつ使いこなせてない感覚あるから活用していきたいな〜）」

タピ「私ちゃんが言う通り、`ひとりごと`のファイルがある時とない時でそれぞれ違うメソッドを呼ぶようにしてみた。これならぱっと見で何してるか分かりやすいでしょ？」

私「はい……割と`後置if`を使いがちなんですけど、今回みたいな場合は普通に`if`文を書いた方が見やすいですね……。」

タピ「そうだね、後は固定で使う文言は定数化するとかかな。」

私「（`忘れてた……`）」

タピ「こんな感じで書くと可読性がよくていいかな」

私「なるほど……ありがとうございます！」

※RSpecについて、`rspec-its`というGemを教えていただきましたが、これはまた別の記事にてまとめたいと思います。

### まとめ
- ファイル名などは事前にソートする（`目に見えている並びが正しい`とは思わないこと）
- `後置if`が可読性がいい、は時と場合による
- 見直す際、仕様の確認・既存のメソッドの確認などを行う

## 最後に
今回は社内報作成アプリの既存機能の改修と新機能の追加を行いました。
見直し不足だったりしてうまく実装できない部分はありましたが、自分の力で概ね実装できるようになったこと、自分のやりたかったことを実現できたことが私としては大きかったです。
今後は`Rails`アプリとして細かい部分を実装していけたらいいなと思っています。
その辺りも記事としてまとめて行きたいと思っているので、今後にご期待（？）ください。