LoginSignup
8
3

More than 3 years have passed since last update.

「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題解いてみた。

Last updated at Posted at 2019-10-11

はじめに

この記事は「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題を解く過程から、先輩のレビューをいただき、振り返るところまでを纏めた記事です。
備忘録・振り返り的な要素が強い為、フランクな書き方をしておりますが、大目に見ていただければと思います。軽い読み物だと思って読んでください。
また解き方に稚拙な箇所もあるかと思いますがご容赦ください。指摘等は大歓迎です。励みになります。

登場人物

    • 社会人2年目PG。Rubyでコーディングし始めて1年とちょっと。うっかりポンコツ。最近Ruby Goldを取得した。
  • ラテ太郎(アイコン参照)
    • 私の心の中に住んでいる妖精。 白黒つけない いいやつ。ゆるい見掛けによらずしっかりしている。最後の砦。
  • タピオカ先輩
    • ラテ太郎の先輩妖精。コーディングが得意。

フェーズ1 「考える・自力で解く」

実際に出題された問題

# NameIndex

## 問題

- カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成せよ。
- 各要素は 50 音順にソートもすること。

## 例

- IN: `['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']`
- OUT: `[ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]`

## 提出時、以下全ての条件を満たしていること

- `RSpec` でエラーが発生していないこと
- `RuboCop` で警告が出ていないこと
- `Coverage` が `100%` であること

考えたこと

私「なるほどなるほど、 辞書みたい な感じでそれぞれの名前を纏めてあげればいいんだ。 out の形ってなんだか group_by した時の形に似てない?(※この時の私は空前の group_by ブーム) 同じ感じ でやれたらいいのにな〜。」

ラテ「こんなイメージで合ってるかな?」


class NameIndex
  def self.create_index(names)
    names.group_by { |name| name.chr }
  end
end

私「そうそう、そんな感じ。私は group_by のところを group_by(&:chr) って書くかな。(※書かないとRuboCopに怒られる。)」


class NameIndex
  def self.create_index(names)
    names.group_by(&:chr)
  end
end

names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']

NameIndex.create_index(names)
=>{"キ"=>["キシモト"], "イ"=>["イトウ"], "バ"=>["ババ"], "カ"=>["カネダ"], "ワ"=>["ワダ"], "ハ"=>["ハマダ"]}

私「これを to_a で配列にしたらイメージに近くない?なんかイメージに近い気がする!……でもこれって並び替えも必要だよね。うーん、 各要素はソートすること って書いてあったよなぁ。各要素をソートする……。」

※シンキングタイム

私「最初から配列の中身ソートしとけばええやんけ…………………………………………………………………………。」

ラテ「気付くのおっそ…………。」


class NameIndex
  def self.create_index(names)
    names.sort.group_by(&:chr).to_a
  end
end

names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']

NameIndex.create_index(names)
=>[["イ", ["イトウ"]], ["カ", ["カネダ"]], ["キ", ["キシモト"]], ["ハ", ["ハマダ"]], ["バ", ["ババ"]], ["ワ", ["ワダ"]]]

私「これです!!!!!!!!!!!!」
私「ちょっとどうしよう凄いしっくりきた。」
私「初めて一行で書けたのでは?」
私「やった〜〜〜〜〜〜〜〜〜〜〜嬉しい!」
私「RSpec書いて、時間を置いてから見直して、リファクタリングできるところあったらリファクタリングしよ!」








ラテ「お判りいただけただろうか…………。」

ラテ「こいつは凄い愚かなヤツです。どの辺が愚かかというと、 自分もやればできるじゃん という気持ちに慢心しているところが愚かなのです。見直しは重要だということは小学生でも知っているのです。勿論、こいつにもそういう気持ちはあったのだと思うのです。あるからこそ 時間を置いてから見直して なんて言ってるのです。けれど、時間を置きすぎてはならないのです。なぜって時間には限りがあるから…………。」












私「待って?」
私「これ、 とか で纏まってなくない?」
私「 辞書みたいなイメージ が先行してたけどこれ 問題と違う よね?」

私「(大混乱)」

私「落ち着け、落ち着け私。きっと ここまでの課程で考えてきたこと と今までの経験が私を助けてくれるはず。」

私「グループ化するっていう発送は悪くないはず。 キーがインデックス、valueがそのインデックスに含まれる文字、みたいなハッシュ を作ったらいいんじゃないか?マッピング的な……ちょっと違う気がするけど……。」

私「あ〜お、って 範囲オブジェクト でいける?数字とアルファベット以外でもいける?ええい、ままよ!食らえ!」


 ('ア'..'オ').to_a
 => ["ア", "ィ", "イ", "ゥ", "ウ", "ェ", "エ", "ォ", "オ"] 

私「すっっっっっっっっっっっっご」

私「まじか……いやまあそうよな……できるよな……できるんか……凄いなRuby……。」

私「待てよ、カタカナだったらあれよな、 とかあるよな。え、ヴァネッサとか出てくるかな電話帳。いやでもこれ苗字だけとか書いてないしな。友達にヴァネッサとかおるかもしれんし……ヴァネッサだけ行き場ないとか可哀想よな……。」

ラテ「そうして出来上がった定数がこれ。」


INDEX = {
  'ア': ('ア'..'オ').to_a << 'ヴ',
  'カ': ('カ'..'ゴ').to_a,
  'サ': ('サ'..'ゾ').to_a,
  'タ': ('タ'..'ド').to_a,
  'ナ': ('ナ'..'ノ').to_a,
  'ハ': ('ハ'..'ボ').to_a,
  'マ': ('マ'..'モ').to_a,
  'ヤ': ('ヤ'..'ヨ').to_a,
  'ラ': ('ラ'..'ロ').to_a,
  'ワ': ('ワ'..'ン').to_a
}.freeze

私「 っている???」
私「いや、ンダホさんとかおるかもしれんし……」

私「入れよう」

私「問題はここからよな」
私「 のグループ、ってグループ分けしたい。」
私「グループ分けしたいけどどうやってメンバー選出する……?」

※シンキングタイム

私「メンバー選出といえば select やんけ…………。」
私「さっき作った範囲の中に頭文字が当てはまる子(文字列)を選出してあげたらいいんじゃん? include? でそれはチェックできる……で、それをうまいこと配列にして……なんかやっと見えてきたぞ…………。」


class NameIndex
  INDEX = {
    'ア': ('ア'..'オ').to_a << 'ヴ',
    'カ': ('カ'..'ゴ').to_a,
    'サ': ('サ'..'ゾ').to_a,
    'タ': ('タ'..'ド').to_a,
    'ナ': ('ナ'..'ノ').to_a,
    'ハ': ('ハ'..'ボ').to_a,
    'マ': ('マ'..'モ').to_a,
    'ヤ': ('ヤ'..'ヨ').to_a,
    'ラ': ('ラ'..'ロ').to_a,
    'ワ': ('ワ'..'ン').to_a
  }.freeze

  def self.create_index(names)
    INDEX.map do |key, value|
      index_names = names.select { |name| value.include?(name.chr) }
    end
  end
end

私「そうそう、これで最初に想定してたみたいに 要素をソート して。 index_names が空じゃなかったらインデックスをつけてあげればいけるんじゃないか。」


class NameIndex
  INDEX = {
    'ア': ('ア'..'オ').to_a << 'ヴ',
    'カ': ('カ'..'ゴ').to_a,
    'サ': ('サ'..'ゾ').to_a,
    'タ': ('タ'..'ド').to_a,
    'ナ': ('ナ'..'ノ').to_a,
    'ハ': ('ハ'..'ボ').to_a,
    'マ': ('マ'..'モ').to_a,
    'ヤ': ('ヤ'..'ヨ').to_a,
    'ラ': ('ラ'..'ロ').to_a,
    'ワ': ('ワ'..'ン').to_a
  }.freeze

  def self.create_index(names)
    INDEX.map do |key, value|
      index_names = names.select { |name| value.include?(name.chr) }.sort
      [key.to_s, index_names] unless index_names.empty?
    end
  end
end

names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']

NameIndex.create_index(names)

=>[["ア", ["アマネ"]], ["カ", ["キシモト"]], nil, nil, nil, ["ハ", ["ハマダ", "ババ"]], nil, nil, nil, ["ワ", ["ワダ"]]]

私「盲点」

私「うっそやん………そりゃそうだわ…………なんで気付かんのじゃ……。」

私「 compact しよ………。」
私「ごちゃごちゃしてるし、 names が空だったら素直に早期リターンしよ」

ラテ「そして完成したのがこれ。」


class NameIndex
  INDEX = {
    'ア': ('ア'..'オ').to_a << 'ヴ',
    'カ': ('カ'..'ゴ').to_a,
    'サ': ('サ'..'ゾ').to_a,
    'タ': ('タ'..'ド').to_a,
    'ナ': ('ナ'..'ノ').to_a,
    'ハ': ('ハ'..'ボ').to_a,
    'マ': ('マ'..'モ').to_a,
    'ヤ': ('ヤ'..'ヨ').to_a,
    'ラ': ('ラ'..'ロ').to_a,
    'ワ': ('ワ'..'ン').to_a
  }.freeze

  def self.create_index(names)
    return [] if names.empty?

    INDEX.map do |key, value|
      index_names = names.select { |name| value.include?(name.chr) }.sort
      [key.to_s, index_names] unless index_names.empty?
    end.compact
  end
end

私「あっ、あ……RSpecも書き直しやん…………。先輩ごめんなさい………。(15分オーバー)」

フェーズ1で学んだこと

  • カタカナも範囲オブジェクトにできる。
  • select を使えばグループ化は簡単
  • 慢心しない、簡単にできたと思った時ほど見直しはしっかり。
  • 時間に余裕を持って作業する。
  • 問題は定期的に読み直す。自分の認識が間違っていないか確認する。
  • 方向性を見失わないようにテストファーストで作業をする。

フェーズ2 タピオカ先輩からレビューをもらう

この課題を解いた後、見守っていたタピオカ先輩からレビューをもらった。

タピ「レビューしたんだけどさ。」
私「はい。」
タピ「今回悪いところなかった。」
私「???」
私「またまたぁ」
タピ「いや本当。今回の評価ポイント(要件を満たせているか・可読性・テストコードの充実度)は満たせてる。」
私「(動揺)(困惑)」
タピ「とりあえずレビューしていこうか」

ポイント1 injectで無駄なく回す

タピ「私さん、 map compact してたじゃん。」
私「はい。mapだとnilが混在しちゃうので」
タピ「じゃあ、nilが混ざらないようにループ回したらよかったんじゃない?」
私「アッ」

  • map compact or select mapで作れる配列は inject で作れ
  • injectで作れるなら each_with_objectにしなさい(RuboCopの好み)

タピ「この記事が参考になる。」
タピ「今回の問題だと、nilになるものののチェックで無駄に処理が走るよね?例えば、電話帳にはア行しか登録されてないのに、他の行についての処理も走る。ループでいうなら2回ループが走ってる。必要なものだけの配列を作りたいなら inject を使えば1回で済む。」
私「そっか……そっか……そうですね……(知識としては持っていたのに未だ使いこなせていないことに対する悔しさに襲われる)」

ポイント2 冗長にならないようなコーディングをする

タピ「あとさ、定数のハッシュ。キーと配列の0番目の値が一緒なの、なんか冗長じゃない?」
私「確かに……。」
タピ「しかもハッシュのキー、to_sしてるじゃん、to_sするくらいならロケット記法で最初からキーを文字列にしておけばよかったんじゃない?」
私「確かに……。」
タピ「しかもこのキーって配列作るときに使ってるだけじゃん?」
私「そうですね。そのためだけに用意しちゃいました。そのキーでまとめなきゃ!!って気持ちが強くて、キーを。」
タピ「つまりこう定義したら万事解決だったってこと。」

class NameIndex
  SYLLABARIES = [
    ('ア'..'オ').to_a << 'ヴ',
    ('カ'..'ゴ').to_a,
    ('サ'..'ゾ').to_a,
    ('タ'..'ド').to_a,
    ('ナ'..'ノ').to_a,
    ('ハ'..'ボ').to_a,
    ('マ'..'モ').to_a,
    ('ヤ'..'ヨ').to_a,
    ('ラ'..'ロ').to_a,
    ('ワ'..'ン').to_a
  ].freeze

  class << self
    def create_index(names)
      names.sort.group_by(&method(:initial)).to_a
    end

    private

    def initial(name)
      SYLLABARIES.find { |values| values.include?(name[0]) }.first
    end
  end
end

私「これです」

私「こういうの書きたかったんです」

私「group_by使われてるし、メインのメソッドは一行で書かれてるし、超カッケー(配列か〜〜〜〜〜!!マッピングしなきゃって気持ちが強すぎた)」

〜タピオカ先輩の解説タイム〜

タピ「例えば、ア〜オ+ヴの中に、名字の1文字目が含まれていたらア行に纏められるわけでしょ。group_byの中でアカサタナ〜が返れば今回の想定した配列が作れるじゃん。つまり、

『名字の1文字目がSYLLABARIESの配列のどれかに含まれていたら、その配列の1文字目が返ればいい』

ってことね。それがinitialメソッド。」

タピ「そのメソッドをgroup_byと合わせて実行すればいいだけ。今回は &methodを使って書いてみた。」

私「(group_byとはこうやって使うのか)」

タピ「どう?納得?」

私「納得しかないです……すげ……」

フェーズ2で学んだこと

  • group_byはブロック内の返り値でグルーピングされる=>自分の思う返り値が返るようなメソッドを作って呼んであげればグルーピングはできる(今回はgroup_byが使えた!)
  • &methodを使うと短く書ける(参考: &演算子と、procと、Object#method について理解しなおす
  • 冗長だと感じるところは徹底的に排除する(同じ文字がなんども出てくる、等。今回は定数の中身が冗長だった。)

最後に

やり方、考え方の方向性は合っていましたが、テクニカルな部分がまだまだだなと実感しました。しかしこれで group_byはマスターです。&methodはあまり使ったことがなかったので、使える場面では使っていきたいと思います。
また、自分の書くコードは冗長になりがちなので、「これって冗長じゃない?」という気持ちをもってコーディングすることを意識する必要があるなと感じました。
次の回ではもっといいコードを書けるように腕を磨いておきます。

8
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3