Quantcast
Channel: 初心者タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 22058

「アウトプットのネタに困ったらこれ!?Ruby初心者向けのプログラミング問題」を自力で解いてリファクタリングした

$
0
0

プログラマ歴 == Ruby歴 == 6ヶ月 の若輩者です。

業務でのRubyコードの実装に周囲よりも多くの時間がかかっていることにもどかしさを感じています。そこで、演習を通じて実装スピードを上げたいと思い、前々から気になっていたアウトプットのネタに困ったらこれ!?Ruby初心者向けのプログラミング問題を集めてみた(全10問)の記事に掲載されている問題をを解いてリファクタリングしました。

結果的に、RubyのAPIへの習熟と簡易的ななアルゴリズムの勉強ができとても有益でしたので、学習のメモをまとめます。

はじめに自力で解いたコードと次にリファクタリング後のコードを載せ、リファクタリングのポイントを解説します。
(参考にできるコードが見当たらなかった場合は、リファクタリングせず自分のコードだけを載せています。)

「ここはもっとこうしたほうが良い」などのご指摘お待ちしております。

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.4
BuildVersion:   19E266
$ ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091)[x86_64-darwin19]

1.カレンダー作成問題

自分で解いたコード

# frozen_string_literal: truerequire'date'today=Date.todayyear=today.yearmonth=today.monthnext_month=today.month+1start_date=Date.new(year,month,1)end_date=Date.new(qyear,next_month,1)-1putstoday.strftime('%B %Y').center(20)puts'Su Mo Tu We Th Fr Sa'(start_date..end_date).each_with_object(Array.new(7,'  '))do|date,result|result[date.wday]=date.day.to_s.rjust(2,' ')ifdate.day==end_date.dayputsresult[0..date.wday].join(' ')elsifdate.wday==6putsresult.join(' ')endend

リファクタリング後のコード

# frozen_string_literal: truerequire'date'classCalendarDatadefinitialize(date)@date=dateenddefoutputheader+"\n"+bodyendprivatedefheadersun_to_sat='Su Mo Tu We Th Fr Sa'year_and_month=@date.strftime('%B %Y').center(sun_to_sat.size)sun_to_sat+"\n"+year_and_monthenddefbodyweeks_in_month.map{|week|week.join(' ')}.join("\n")enddefweeks_in_monthweek=0dates_in_month.each_with_object([])do|date,result|ifresult.empty?result<<Array.new(7,'  ')elsifdate.sunday?result<<[]week+=1endresult[week][date.wday]=date.day.to_s.rjust(2,' ')endenddefdates_in_month(start_date..end_date)enddefstart_dateDate.new(year,month,1)enddefend_datestart_date.next_month.prev_dayenddefyear@date.yearenddefmonth@date.monthendend

リファクタリングのポイント

Dateクラスのメソッドを使うようにした

DateDate#next_monthやDate#prev_day, Date#sunday?など多くのメソッドを知り、便利だったので導入しました。

クラスとメソッドを定義した

きみたちは今まで何のためにRailsでMVCパターンを勉強してきたのかの記事に

ロジック本体と画面出力をきちんと分離すべし

とあるように、ロジック本体と画面の出力を分離しました。
また、再利用性を考えてクラスとメソッドを定義しました。

参考

2.カラオケマシン問題

自分で解いたコード

# frozen_string_literal: trueclassKaraokeMachinedefinitialize(melody)@melody=melodyendMELODIES=%w[C C# D D# E F F# G G# A A# B].freezedeftranspose(number)@melody.split('').map{|code|code.match?(/\w/)?adjust_melody_line(code,number):code}.join('')enddefadjust_melody_line(code,number)MELODIES[new_index(code,number)]enddefnew_index(code,number)original_index=MELODIES.index(code)(original_index+number).modulo(MELODIES.size)endend

リファクタリング後のコード

# frozen_string_literal: trueclassKaraokeMachinedefinitialize(melody)@melody=melodyendMELODIES=%w[C C# D D# E F F# G G# A A# B].freezedeftranspose(number)@melody.gsub(/[A-G]#?/){|code|MELODIES[new_index(code,number)]}enddefnew_index(code,number)(MELODIES.index(code)+number)%MELODIES.sizeendend

リファクタリングのポイント

文字列の置換をgsubメソッドを使うようにした

String#gsubならば、文字列をそのまま置換できます。
わざわざRegexp#match?を使って配列にしたあとにjoinして文字列に整形し直すというのは、冗長でしたね。

剰余を%を使って求めるようにした

好みの問題かもしれませんが、剰余はNumeric#moduloよりもNumeric#%のほうが直感的にわかりやすいと思ったので修正しました。

参考

3.ビンゴカード作成問題

自分で解いたコード

classBingodefoutputheader+"\n"+bodyenddefheader' B |  I |  N |  G |  O'enddefbodylines.map{|line|line.join(' | ')}.join("\n")enddeflines[(1..15),(16..30),(31..45),(46..60),(61..75)].map{|numbers|column=numbers.map{|number|number.to_s.rjust(2,' ')}.sample(5)column[2]='  'ifnumbers==(31..45)column}.transposeendend

リファクタリング後のコード

classBingoBING_NUMBERS=(1..75)defoutputheader+"\n"+bodyenddefheader'BINGO'.chars.join(' | ')enddefbodylines.map{|line|line.map{|number|number.to_s.rjust(2)}.join(' | ')}.join("\n")enddeflinesBING_NUMBERS.each_slice(15).map{|numbers|numbers.sample(5)}.tap{|table|table[2][2]=''}.transposeendend

リファクタリングのポイント

15ずつの値の取得をeach_sliceを使うようにした

1から75までの15区切りのオブジェクトをEnumerable#each_sliceを使って実装しました。

二次元配列の値の書き換えをtapを使うようにした

Object#tapでオブジェクトの値を書き換えるのってエレガントですよね。業務のコードでも見かけるのですが、私は書いたことがなかったですので、良い機会だと思いtapを使いました。

参考

4.ボーナスドリンク問題

自分で解いたコード

classBonusDrinkdefoutput(number)number+bonus(number)enddefbonus(number)div,mod=number.divmod(3)return0ifdiv.zero?div+bonus(div+mod)endend# puts BonusDrink.new.output(100)# -> 149

ポイント

  • 再帰関数を使った

単語だけ聞いた頃あるレベルの再帰関数を使って解答できそうだとの直感を得たので、再帰関数について調べて実装してみました。シンプルなコードをかけたのでなかなか嬉しいです。

また、数学的な考えを使って解かれているている方がもいらっしゃるようでした。

(ボーナスドリンク問題の解答)

参考

5.電話帳作成問題

自分で解いたコード

classNameIndexdefinitialize(names)@names=namesendINDEXES={'ア'=>['ア','イ','ウ','エ','オ'],'カ'=>['カ','キ','ク','ケ','コ'],'ハ'=>['ハ','ヒ','フ','ヘ','ホ','バ','ビ','ブ','ベ','ボ'],'ワ'=>['ワ','オ','ン'],}defoutputreturn[]if@names.empty?@names.each_with_object({}){|name,result|INDEXES.eachdo|index,list|iflist.include?(name[0])result[index]=[]unlessresult.key?(index)result[index]<<nameresult[index].sort!breakendend}.to_a.map.sortendend

リファクタリング後のコード

classNameIndexdefinitialize(names)@names=namesendINDEXES={'ア'=>['ア','イ','ウ','エ','オ'],'カ'=>['カ','キ','ク','ケ','コ'],'ハ'=>['ハ','ヒ','フ','ヘ','ホ','バ','ビ','ブ','ベ','ボ'],'ワ'=>['ワ','オ','ン'],}defoutputreturn[]if@names.empty?@names.sort.each_with_object({}){|name,result|index,_value=INDEXES.find{|index,value|value.include?(name[0])}result[index]=[]unlessresult.key?(index)result[index]<<name}.to_aendend

リファクタリングのポイント

ソートの処理をひとまとめにした

自分で解いたときは、each_with_objectのブロック内とブロック外で1回ずつソートの処理をしてしましたが、each_with_objectの前にソート処理を挿入することでソート処理を1回で済むように修正しました。

インデックスの取得

ハッシュのキーに一致するバリューを取得するのにINDEXESの繰り返しを処理をしていましたがこれでは無駄な処理が発生してしまっています。

Enumerable#detectを使って一致するキーを探してindexを取得できるようにしました。

参考

6.国民の祝日.csv パースプログラム

自分で解いたコード

require'csv'classHolidayParserdefself.parsecsv=CSV.read('holiday.csv')csv.each_with_object({2016=>{},2017=>{},2018=>{}}).with_index(1){|(row,result),index|data=row.uniqnextunless(3..18).cover?(index)result[2016][data[1]]=data[0]result[2017][data[2]]=data[0]result[2018][data[3]]=data[0]}endend

リファクタリング後のコード

require'csv'classHolidayParserCSV_PATH=File.expand_path('../holiday.csv',__FILE__)HOLIDAY_INDEX=0YEARS_INDES={'2016'=>1,'2017'=>2,'2018'=>3}HOLIDAY_ROW_RANGE=(3..18)defself.parse(csv_path=CSV_PATH)self.new.parse(csv_path)enddefparse(csv_path)generate_csv(csv_path).each_with_object({2016=>{},2017=>{},2018=>{}}).with_index(1){|(row,result),index|data=row.uniqnextunlessHOLIDAY_ROW_RANGE.cover?(index)result[2016][data[YEARS_INDES['2016']]]=data[HOLIDAY_INDEX]result[2017][data[YEARS_INDES['2017']]]=data[HOLIDAY_INDEX]result[2018][data[YEARS_INDES['2018']]]=data[HOLIDAY_INDEX]}endprivatedefgenerate_csv(csv_path)CSV.read(csv_path)endend

リファクタリングのポイント

マジックナンバーを定数化した

csvファイルのパスをCSV_PATHとして、祝日が入っている列数をHOLIDAY_INDEXとして、マジックナンバーとして利用されていた値を定数に置き換えました。

マジックナンバーはコードをスムーズに読むことを阻害する要因になるので使わないようにしたほうが良いですよね。
とはいえ、2016~2018の数値リテラルが使用されているため、csvファイルの年度がかわると動かなくなってしまうプログラムです。

まだ改良の余地がありますね。

参考

7.「Rubyで英語記事に含まれてる英単語を数えて出現数順にソートする」問題

自分で解いたコード

自分では歯が立たず写経しました。
単語を抜き出すだけなら出来そうでしたが、熟語を抜き出すというのが難しかったです。

classWordExtracterTEXT_PATH=File.expand_path('../input.txt',__FILE__)defoutputtext=File.read(TEXT_PATH)words=count_words(text)compound_words,single_words=words.partition{|word,_|word.include?(' ')}output_result(single_words,compound_words)endprivatedefcount_words(text)word_char='[\w’\/-]'compound_words=/[A-Z]#{word_char}*(?: of| [A-Z]#{word_char}*)+/words=/#{word_char}+/regex=Regexp.union(compound_words,words)text.scan(regex).each_with_object(Hash.new(0)){|word,count_table|count_table[word]+=1}enddefoutput_result(single_words,compound_words)word_count=single_words.inject(0){|sum,(_,count)|sum+count}puts"単語数(熟語以外):#{word_count}"output_words(compound_words,'英熟語?')output_words(single_words,'英単語')enddefextractinput_text.joinenddefoutput_words(count_table,header)puts"#{header}------------------------------------------------------------------"sorted_table=count_table.sort_by{|word,count|[-count,word.downcase]}sorted_table.eachdo|word,count|puts'%d %s'%[count,word]endendend

参考

8.行単位、列単位で合計値を求

自分で解いたコード

classQueueCalculatorINPUT=[[9,85,92,20],[68,25,80,55],[43,96,71,73],[43,19,20,87],[95,66,73,62]]defself.outputself.new.calculateenddefcalculateformat(calculation)endprivatedefformat(matrix)matrix.map{|row|row.join('| ')}.join("\n")enddefcalculationINPUT.map{|numbers|numbers<<numbers.sum}.transpose.map{|numbers|numbers<<numbers.sum}.transposeendend

ポイント

行列の入れ換え

Array#transposeを使って行列を入れ換えてうまく計算できました。

参考

9.ガラケー文字入力問題

自分で解いたコード

classKeitaiMessageCHARACTERS={'1'=>['.',',','!','?',' '],'2'=>['a','b','c'],'3'=>['d','e','f'],'4'=>['g','h','i'],'5'=>['j','k','l'],'6'=>['m','n','o'],'7'=>['p','q','r','s'],'8'=>['t','u','v'],'9'=>['w','x','y','z'],}definitialize(number)@number=numberenddefoutput@number.scan(/[1-9]+/).map{|numbers|CHARACTERS[numbers[0]].rotate(numbers.size-1).first}.joinendend

ポイント

正規表現とArray#rotateを使ってシンプルに実装できました。

参考

10.値札分割問題

自分で解いたコード

classPricedefinitialize(price)@price=priceenddefsplit_pricereturn['','']unless@pricenumber=@price.scan(/[0-90-9+|.|,| |\-|価格未定|]/).join[number,@price.delete(number).delete('-')]endend

リファクタリング後のコード

classPricedefinitialize(price)@price=priceenddefsplit_price@price.match(/([^万円]*)(.*)/)[1..2]endend

リファクタリングのポイント

正規表現を使う

自分で解いたコードは、特殊なケースごとに個別に対応するような冗長な実装になってしまっていたので、参考記事をほぼ写経しました。
キャプチャ()やメタ文字*をもっと使いこなせねばならないですね。

参考

まとめ

全く歯が立たない問題は1問(「Rubyで英語記事に含まれてる英単語を数えて出現数順にソートする」問題)だけでしたが、よりスマートな書き方を学ぶ良い機会になりました。
1問目から順に解き進めていましたが、後半に進むにつれ回答するまでのスピードが早くなり、コードは徐々に綺麗になってきているなと感じています。

また、私は正規表現が苦手だということがわかり新たな課題を見つけたので、正規表現を克服したいなと感じています。

よい練習問題をご提供いただいた@jnchitoさん、感謝いたします。


Viewing all articles
Browse latest Browse all 22058

Trending Articles