はじめに
毎月先輩から出していただいた課題に取り組んでいます、 mi0です。
3月は、2月に作成した社内報を作成するクラスに、さらに機能を追加するという課題にチャレンジしました。
この記事は要件定義〜レビューをいただくまでの過程を纏めた備忘録です。
こうやったらもっとよくなる、などのご指摘があればコメント頂けると嬉しいです!
過去の記事はこちら!↓
- 「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題解いてみた。
- Ruby ボウリング問題解いてみた。
- Ruby 価値が大きくなる組み合わせ問題 解いてみた
- Ruby初心者向けのプログラミング問題のカラオケマシン問題にハモリ機能を追加してみた
- Ruby ボウリング問題解いてみた。をパワーアップしてみた。
- 【Ruby】社内報を作成してくれるクラスを作成してみた。
登場人物
- 私
- 気付いたら三年目になっていた(記事執筆時点)。気付いたら課題も10回目になっていた。気付いたら割とコードが書けるようになっていた。気付いたら文章を書くのも割と慣れてきていた。
- ラテ太郎(アイコン参照)
- 初心に返りたかったという理由で召集された、私の心の中に住んでいる妖精。最後の砦。
- タピオカ先輩
- ラテ太郎の先輩妖精(出演2回目)。プログラミングが得意。「なければ作ればいいじゃない」ムーブがかっこいい。
要件を定義する
前回までの仕様(詳細は前回の記事参照)
- 社内報の本文を作成する
- テンプレートに、各社員の原稿を嵌め込んでいき、社内報の本文を作成する
前回はシンプルに社内報を作成する機能だけを実装しました。
今回は更に必要そうな機能を盛り込んでいきます。
今回追加する機能の仕様について考える
今回追加する機能
タピオカ先輩との打ち合わせで、今回追加することになった機能は以下の2つです。
ひとりごと
の追加(既存機能の改修)ひとりごと
という、各社員に書いてもらった呟き
を匿名で載せるコーナーを社内報に設けているので、そのコーナーを本文に追加できるよう改修する
- 未提出者をリストアップする
トレース
機能の追加- 前回、未提出者については社内報本文内で、
未提出
という表記を使って表示していた。未提出に該当する人物についてはリストアップし、提出を依頼する必要があるため、トレース
機能を実装することでリストアップを自動化する
- 前回、未提出者については社内報本文内で、
これらについては、現在全て手作業
で行っている上、思っている以上に時間がかかっている作業です。
これらの作業を自動化できたらかなり作業時間を時短できるため、今回追加することになりました。
より具体的な仕様を考えて行く
ひとりごとの追加
私「とりあえずひとりごとを今どんな風に編集しているか纏めてみよっか」
ラテ「現状把握
は大事だね。現状
が分からないとどうやって改善するか
が考えられないから。」
私「でしょ〜何事も現状把握が大事!で、一旦まとめるとこんな感じかな。」
- 原稿はテキストファイル。名称は統一して
hitorigoto
で提出するよう依頼している。 - 全て
hitorigoto.txt
として手元に届くため、保存時はhitorigoto1
のように、ナンバリングして保存する。 - 提出されているひとりごとの数だけ本文には反映する(具体例は以下に纏める)
- 1件も提出がない場合は
ひとりごと
の見出しごと消す - 現在は以下のようなフォーマットで原稿本文末尾に挿入している。
ひとりごと
①ひとりごと本文
②ひとりごと本文
③ひとりごと本文
ひとりごと
①ひとりごと本文
②ひとりごと本文
私「こんな感じかな〜。大筋は今のままでいいと思うんだよね。文頭の数字の表示はちょっと考えるけど……。一旦シンプルに1
とかでもいい気がしなくはない、かな……うーん。」
ラテ「ふんふん、その辺はタピオカ先輩と相談で良いんじゃないかな?多分I18n
の仕組みが今は入ってないから、とりあえずは普通の数字で定義しておいて、ゆくゆく改良みたいな形になりそうな気がする」
私「そっか、確かに。じゃあ一旦これで纏めておこう。」
トレース機能の追加
私「次、トレース機能だね。これも結構大変なんだよ〜。」
ラテ「今はどんな感じでやってるの?」
私「んとね、こんな感じ。」
- 提出された原稿を1つのディレクトリに全て保存ずる
- 名簿を元に、提出している人をチェックしていく
- チェックがついていない人を未提出としてリストアップする
- 但し、産休中の方などは上記リストから除外する
- リストアップするときは
・[名前]さん
のような形式でリストアップする
私「このチェックのところが大変なんだよね〜。前の実装で作った未提出
として出力する処理がうまいこと使えたらいいな〜って思ってる。」
ラテ「ふんふん、なるほど……。とりあえず#trace
みたいなメソッドを追加するのが大前提だね。」
私「そうだね。メンバー一覧の項目にトレース対象かどうか
みたいなフラグ
を足した方がいいかも」
ラテ「なるほど、トレース対象だったらリストに追加するし、そうじゃなかったら含めない、みたいにするわけだね?」
私「そうそう、それが一番イメージに近いかなぁ。これなら、後からRails
アプリとして作り変えるってなった時もいい感じにできそうだし。」
ラテ「確かに。じゃあ、前のメンバーリストはヘッダーなしのCSVとして作ってたから、今回はヘッダーありにする方が分かり易そうだね。突然リストにtrue
・false
が書いてあると困惑するし。」
私「あ〜、じゃあそこは修正かな。一旦それで纏めてみよっか。」
完成した要件
社内報作成アプリ v2
問題
- 社内報作成機能に
ひとりごと
登録機能を追加する - 未提出者をリストアップする
トレース
機能を追加する
社内報の元データ
db/template.txt
・・・社内報のテンプレート。db/member_list.csv
・・・各メンバーの名前とメールアドレスが記載されている。db/manuscripts
・・・年月
毎にディレクトリを作成し、直下にその月の各メンバーの原稿を格納する。
要求仕様
ひとりごと登録機能(#generate の改修)
- ひとりごとは
hitorigoto1.txt
のように、1〜n までの数字でナンバリングされたテキストファイルである - ひとりごとは件数分表示される
- ひとりごとの並び順はテキストファイルの番号が若い順とする
- ナンバリングされていないテキストファイルは反映されない
- テンプレートの
[hitorigoto]
に以下のような形式でひとりごとを表示する- 見出しには
ひとりごと
と表示する - ひとりごとのフォーマットは
[n] 本文
とする - ひとりごとが 1 件も存在しない場合は
空行にする
- 見出しには
例) 202002を引数に指定していて、ひとりごとが2件提出されている場合
・ラテ太郎
お疲れ様です、ラテ太郎です。
サンプル
サンプル
サンプル
・ラテ子
お疲れ様です、ラテ子です。
サンプル
サンプル
サンプル
サンプル
・沖漬け先輩
サンプル
サンプル
サンプル
サンプル
サンプルサンプルサンプルサンプルサンプルサンプルサンプル
・ラテ二郎
未提出
ひとりごと
1 ひとりごと
2 ひとりごと
例) 202002を引数に指定していて、ひとりごとが1件も提出されていない場合
・ラテ太郎
お疲れ様です、ラテ太郎です。
サンプル
サンプル
サンプル
・ラテ子
お疲れ様です、ラテ子です。
サンプル
サンプル
サンプル
サンプル
・沖漬け先輩
サンプル
サンプル
サンプル
サンプル
サンプルサンプルサンプルサンプルサンプルサンプルサンプル
・ラテ二郎
未提出
トレース機能
- 未提出者をリストアップする
#trace
を実装する - フォーマットは
・名前さん
とする db/member_list.csv
のexcluded
欄がtrue
の場合はリストから除外する
require'csv'classInternalNewsletterMEMBER_LIST=CSV.read('db/member_list.csv')NEWSLETTER=File.read('db/template.txt')NOT_SUBMITTED='未提出'.freezedefinitialize(datestr)@datestr=datestr@dirpath="db/manuscripts/#{@datestr}"enddefgenerateraiseArgumentErrorunlessDir.exist?(@dirpath)MEMBER_LIST.inject(NEWSLETTER,&method(:newsletter))+"\n"enddeftrace''endprivatedefnewsletter(text,member)name,keyword=split_name_keyword(member)fullpath="#{@dirpath}/#{@datestr}#{keyword}.txt"body=File.exist?(fullpath)?File.read(fullpath).strip:NOT_SUBMITTEDtext.gsub(/\[#{keyword}\]/,<<~NEWSLETTER.chomp・#{name}#{body} NEWSLETTER)enddefsplit_name_keyword(member)name,email=member_,keyword=email.match(/\A(\w+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/).to_a[name,keyword]endend
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
社内報
[rate_taro]
[rate_ko]
[okiduke_senpai]
[rate_jiro]
[tarabagani_senpai]
[koppegani]
[hitorigoto]
- 202002hitorigoto1.txt
- 202002hitorigoto2.txt
- 202002okiduke_senpai.txt
- 202002rate_ko.txt
- 202002rate_taro.txt
要件を元に実際に作成していく
私「よし!じゃあ今度は出来た仕様を元にそれぞれの機能を作っていこう!」
私「その前に簡単に手直ししておこっか」
require 'csv'
class InternalNewsletter
- MEMBER_LIST = CSV.read('db/member_list.csv')
+ MEMBER_LIST = CSV.read('db/member_list.csv', headers: true)
# 略
end
私「これでとりあえずメンバーリストがいい感じで読み込めるようになった!次、ひとりごとの追加ができるようにしていこう!」
ひとりごとの追加
私「とりあえずいつもの通りイメージを文章で纏めてみよう」
require'csv'classInternalNewsletterMEMBER_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='未提出'.freezedefinitialize(datestr)@datestr=datestr@dirpath="db/manuscripts/#{@datestr}"end# 一部省略・必要箇所のみ抜粋defgenerateMEMBER_LIST.inject(NEWSLETTER,&method(:newsletter))+"\n"# ここで、原稿の本文を作成する+ひとりごとを追加してくれるようなメソッドを呼ぶ# newsletterメソッドの中でいい感じにできる?endprivatedefnewsletter(text,member_info)name,keyword=split_name_keyword(member_info)fullpath=file_fullpath(keyword)body=File.exist?(fullpath)?File.read(fullpath).strip:NOT_SUBMITTEDtext.gsub(/\[#{keyword}\]/,<<~NEWSLETTER.chomp・#{name}#{body} NEWSLETTER)# ここでひとりごとをよしなにする?enddefhitorigoto# ひとりごとの欄に表示したい内容を文字列で成形していく# フォーマットは `[インデックス] [本文]\n`(これは従来の実装と同様にヒアドキュメント使う)# ひとりごとの数だけ上記のフォーマットの文字列を作って、配列に格納する。# 上記配列が空じゃない(ひとりごとが存在する)場合は `ひとりごと`の見出しを配列の頭に挿入する# 上記配列が空じゃない(ひとりごとが存在する)場合# 上の配列を改行でjoinしたものをテンプレートの[hirotigoto]の部分に突っ込む# 上記配列が空(ひとりごとが存在しない)場合# テンプレートの[hitorigoto]を空行にするendend
私「う〜〜〜ん一旦こんな感じかな?」
私「本当にそれでできるん?みたいな部分はあるけど、一旦これで作ってみようか。」
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
で行って、あとはそれぞれのファイルを読み込んで成形する方式で今回はやってみました。
ラテ「(微妙な顔)」
私「え!?!?!!?!なにその顔」
ラテ「動かしてみたら?」
私「ヘァ〜〜〜〜〜???動かしてみよ」
社内報
[rate_taro]
[rate_ko]
[okiduke_senpai]
[rate_jiro]
[tarabagani_senpai]
[koppegani]
ひとりごと
1 とにかくこの世に生まれたからには、何か1つ足跡を残したい
# 以下略
私「????????????????」
私「あれ?ん?個人の原稿が置換されてない
、ね?」
私「(シンキングタイム)」
私「そういえばinject
使ってるじゃん………」
私「じゃあnewsletter
改良作戦は無理そうだな……なるほどね……盲点でした……。」
ラテ「置換するっていう方向性は良さそうだから、置換するタイミングを変えてみるのはどう?」
私「なるほど……従来の実装で本文の置換
は出来てるから、あとはひとりごと
のところを独自に置き換えればいい……から、本文の置換が終わった後にひとりごと
の欄を作る?」
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
私「こう、かな……」
私「うん、ちゃんと置換されるしよさみちゃん!ひとりごとは一旦これでいこう」
※コメントは後で消しました。
トレース機能の実装
私「次、トレース機能!」
私「これもとりあえずイメージを纏めておこっか」
require'csv'classInternalNewsletterMEMBER_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='未提出'.freezedefinitialize(datestr)@datestr=datestr@dirpath="db/manuscripts/#{@datestr}"end# 一部省略・必要箇所のみ抜粋deftrace''# ディレクトリが存在してなかったら例外# メンバーリストを一件ずつチェックしていく# 該当のファイルが存在していればスルー# 該当のファイルが存在していない場合# 除外フラグが立っていたらスル──# 除外フラグが立っていない場合はリストに追加する# リストを結合するendend
私「こっちは結構シンプルそうだね?」
ラテ「確かに、存在チェックとフラグのチェックだけだもんね」
私「よし、じゃあこれでパパッと作ってみよう」
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
の方とも共通で使えそうなメソッドも足してみたよ」
私「ここは本当に読み通りで実装できてよかった〜〜〜!!!」
私「というか、私は今まで数行コード書けば後は実行するだけで終わり
な作業を全部手作業でやってたってことよね……?時間もったいな……
」
私「きっとこういう気持ちが湧くから人は自動化できるツール
を作っていくんだろうな……」
ラテ「コードが書けない時は人力でやった方が早い
からツールを作ろうって気持ちが湧かないけど、今はコードが書けるようになってきたから、この程度のコードでいいなら作った方が早い
って意識に変わってるってことだね。」
私「これが極まっていくと、タピオカ先輩みたいにないなら作ればいいじゃない
的なマリーアントワネット方式になっていくんだろうなぁ。」
私「よしよし、後は共通メソッドが使える場所をリファクタして……完成!」
〜〜〜そうして出来上がったものがこちら〜〜〜
require'csv'classInternalNewsletterMEMBER_LIST=CSV.read('db/member_list.csv',headers: true)NEWSLETTER=File.read('db/template.txt')NOT_SUBMITTED='未提出'.freezedefinitialize(datestr)@datestr=datestr@dirpath="db/manuscripts/#{@datestr}"enddefgenerateraiseArgumentErrorunlessDir.exist?(@dirpath)convert_hitorigoto(MEMBER_LIST.inject(NEWSLETTER,&method(:newsletter)))enddeftraceraiseArgumentErrorunlessDir.exist?(@dirpath)MEMBER_LIST.each_with_object([])do|member_info,array|nextifFile.exist?(file_fullpath(member_info['email']))nextifmember_info['excluded']=='true'array<<trace_name_format(member_info['name'])end.joinendprivatedefnewsletter(text,member_info)name,keyword=split_name_keyword(member_info)fullpath=file_fullpath(keyword)body=File.exist?(fullpath)?File.read(fullpath).strip:NOT_SUBMITTEDtext.gsub(/\[#{keyword}\]/,<<~NEWSLETTER.chomp・#{name}#{body} NEWSLETTER)enddefsplit_name_keyword(member_info)_,keyword=member_info['email'].match(/\A(\w+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/).to_a[member_info['name'],keyword]enddeffile_fullpath(keyword)"#{@dirpath}/#{@datestr}#{keyword}.txt"enddefconvert_hitorigoto(text)body=hitorigoto_files.each_with_object([]).with_index(1)do|(file,array),index|array<<<<~NEWSLETTER.chomp#{index}#{File.read(file)} NEWSLETTERendbody.unshift("ひとりごと\n")unlessbody.empty?keyword='[hitorigoto]'body.empty??text.gsub("#{keyword}\n",''):text.gsub(keyword,body.join("\n"))enddefhitorigoto_filesDir.glob("db/manuscripts/#{@datestr}/*.txt").selectdo|file|file.match?(%r{\Adb/manuscripts/#{@datestr}/#{@datestr}hitorigoto[0-9]+\.txt\z})endenddeftrace_name_format(name)<<~NAME・#{name}さん
NAMEendend
レビューをいただく
タピ「じゃあレビューをしていくね」
私「よろしくお願いします!(今回は割と自信があるぞ!!)」
タピ「まず俺が用意したテストが落ちました
」
私「?」
私「えっ」
タピ「具体的に言うと、ひとりごとの並び順が番号の若い順になってなかった
ね。」
私「?????うそ」
私「私のローカルでは番号の若い順で並んでて、………あ〜〜〜〜〜〜!!!!システムの設定によって並び順は変わるから
私のローカルでの並び順が正常でも、他の環境で必ずその並び順になるとは限らない
!!!!!」
私「あ〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜(業務中に同じようなことをやったことがある記憶がある顔)」
私「明示的に並び替えないとダメですね……」
タピ「そうです」
私「」
タピ「後はね、ここ」
deftraceraiseArgumentErrorunlessDir.exist?(@dirpath)MEMBER_LIST.each_with_object([])do|member_info,array|nextifFile.exist?(file_fullpath(member_info['email']))# <= ここnextifmember_info['excluded']=='true'array<<trace_name_format(member_info['name'])end.joinend
タピ「ここで渡したいのってメールアドレスのドメイン部以外
だよね?」
タピ「メールアドレスそのまま渡してる」
私「」
私「ヒェ」
タピ「後はね、ここ」
defhitorigoto_filesDir.glob("db/manuscripts/#{@datestr}/*.txt").selectdo|file|file.match?(%r{\Adb/manuscripts/#{@datestr}/#{@datestr}hitorigoto[0-9]+\.txt\z})# <= ここendend
タピ「このdb/manuscripts/#{@datestr}
は@datestr
に入ってる」
私「」
私「(ぴえん)」
私「この辺は見直し不足ですね……」
タピ「そうだね」
タピ「後はCSVから値を取得する時、Boolean
はBoolean
として取得できたらよかったかな」
私「あ〜〜〜〜〜〜」
タピ「とりあえず俺のコード見ていこうか」
require'csv'classInternalNewsletterMEMBER_LIST=CSV.read('db/member_list.csv',headers: true)NEWSLETTER=File.read('db/template.txt')NOT_SUBMITTED='未提出'.freezeMANUSCRIPTS_PATH='db/manuscripts'.freezeHITORIGOTO_KEYWORD='hitorigoto'.freezedefinitialize(datestr)@datestr=datestr@dirpath="#{MANUSCRIPTS_PATH}/#{@datestr}"enddefgeneratevalid!manuscripts=MEMBER_LIST.inject(NEWSLETTER,&method(:newsletter))allocate_hitorigoto(manuscripts)enddeftracevalid!targets=unsubmitted_namesreturniftargets.empty?format_unsubmitted_target(targets)+"\n"endprivatedefvalid!returnifDir.exist?(@dirpath)raiseArgumentErrorenddefnewsletter(text,member)name,keyword=split_name_keyword(member)fullpath="#{@dirpath}/#{@datestr}#{keyword}.txt"body=File.exist?(fullpath)?File.read(fullpath).strip:NOT_SUBMITTEDtext.gsub(/\[#{keyword}\]/,<<~NEWSLETTER.chomp・#{name}#{body} NEWSLETTER)enddefsplit_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]enddefallocate_hitorigoto(manuscripts)files=Dir["#{@dirpath}/#{@datestr}#{HITORIGOTO_KEYWORD}*.txt"]iffiles.any?replace_hitorigoto_manuscripts(manuscripts,files)elseremove_hitorigoto_space(manuscripts)endenddefreplace_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)enddefhitorigoto(file,index)"#{index}#{File.read(file).strip}"+"\n"enddefremove_hitorigoto_space(manuscripts)manuscripts.gsub(/\n\[#{HITORIGOTO_KEYWORD}\]\n/,'')enddefunsubmitted_namesMEMBER_LIST.mapdo|member_list|nextifmember_list['excluded']name,keyword=split_name_keyword(member_list)nextifFile.exist?("#{@dirpath}/#{@datestr}#{keyword}.txt")nameend.compactenddefformat_unsubmitted_target(targets)targets.mapdo|target|I18n.t('trace.name_list_format',name: target)end.join("\n")endend
私「アヒョ……(動揺のあまり溢れ出るグーフ●)」
私「すみませんちょっと夢の国に」
私「あ〜〜〜#allocate_hitorigoto
めっちゃいいですね……」
私「ひとりごと
が存在しない時はファイルのソート処理
とか無駄だし、一旦該当ファイルを取得しておいて、必要に応じて後からソートする、とか」
私「#replace_hitorigoto_manuscripts
とかの感じもいいですね……シンプルで分かりやすい。」
私「(map
の時の&method
でいい感じに引数渡してメソッド実行するやつ使いこなせてない感覚あるから活用していきたいな〜)」
タピ「私ちゃんが言う通り、ひとりごと
のファイルがある時とない時でそれぞれ違うメソッドを呼ぶようにしてみた。これならぱっと見で何してるか分かりやすいでしょ?」
私「はい……割と後置if
を使いがちなんですけど、今回みたいな場合は普通にif
文を書いた方が見やすいですね……。」
タピ「そうだね、後は固定で使う文言は定数化するとかかな。」
私「(忘れてた……
)」
タピ「こんな感じで書くと可読性がよくていいかな」
私「なるほど……ありがとうございます!」
※RSpecについて、rspec-its
というGemを教えていただきましたが、これはまた別の記事にてまとめたいと思います。
まとめ
- ファイル名などは事前にソートする(
目に見えている並びが正しい
とは思わないこと) 後置if
が可読性がいい、は時と場合による- 見直す際、仕様の確認・既存のメソッドの確認などを行う
最後に
今回は社内報作成アプリの既存機能の改修と新機能の追加を行いました。
見直し不足だったりしてうまく実装できない部分はありましたが、自分の力で概ね実装できるようになったこと、自分のやりたかったことを実現できたことが私としては大きかったです。
今後はRails
アプリとして細かい部分を実装していけたらいいなと思っています。
その辺りも記事としてまとめて行きたいと思っているので、今後にご期待(?)ください。