RubyでFizzBuzz問題を解いて上司に対抗しよう!

ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。

RubyでFizzBuzz問題を解いて上司に対抗しよう! : melborne.github.com

                                                                                        • -

FizzBuzz問題は有名だから
少しプログラムをかじったことがあれば
名前くらいは知ってるよね
それを会社の10人のプログラマにテストしてみたら
あまりできがよくなかったという話があるよ


FizzBuzz問題を使って社内プログラミングコンテストを開催してみた - ITは芸術だ


確かにFizzBuzz問題は一見単純だから
誰でも簡単に解けると思われがちだけど
時間制限付きの抜き打ちテストというかたちでだされたら
頭が混乱して僕もどんな結果になるか心配だよ


だから上司の嫌がらせで恥をかかされないように
いまからしっかりと予習しておくよ:)

問題の分割

通常1つの問題は複数の小さな問題でできているんだ
だから与えられた問題を読んだとき最初にすべきことは
それを複数の小さな問題に切り分けることだよ


早速FizzBuzz問題を小さな問題に切り分けてみよう


僕はFizzBuzz問題は次のような
3つの小さな問題に切り分けられると思うんだ

  1. 1つの数を取ってFizzBuzzの結果を返す関数を作る問題
  2. 1からxまでの数をその関数に適用する関数を作る問題
  3. スクリプト引数xを2の関数に与えて結果をターミナルに出力する関数を作る問題

小さな問題1

じゃあこれらの問題を順に解いていくよ
まずは最初の小さな関数(fizzbuzzとする)を作ろう
これは1を取ったら1
2を取ったら2
3を取ったら'Fizz'
5を取ったら'Buzz'
を返すような関数を作ればいいから簡単だよね


あ いい忘れたけど僕はRubyしか書けないからRubyで書くよ
例えばこんなのどうかな?

def fizzbuzz(n)
  case
  when n%5==0 && n%3==0; 'FizzBuzz'
  when n%5==0; 'Buzz'
  when n%3==0; 'Fizz'
  else n
  end
end

fizzbuzz(1) # => 1
fizzbuzz(3) # => "Fizz"
fizzbuzz(4) # => 4
fizzbuzz(5) # => "Buzz"
fizzbuzz(10) # => "Buzz"
fizzbuzz(15) # => "FizzBuzz"


オーソドックスだけど個人的には
Fixnum#%が何回も出てくるのはイケてないと感じるよ
こうすればもう少し良くなるかな

def fizzbuzz(n)
  mod_zero = ->base{ n%base == 0 }
  case
  when mod_zero[3] && mod_zero[5]; 'FizzBuzz'
  when mod_zero[3]; 'Fizz'
  when mod_zero[5]; 'Buzz'
  else n
  end
end

fizzbuzz(1) # => 1
fizzbuzz(3) # => "Fizz"
fizzbuzz(4) # => 4
fizzbuzz(5) # => "Buzz"
fizzbuzz(10) # => "Buzz"
fizzbuzz(15) # => "FizzBuzz"


少し良くなったと思うけど
個人的にはwhenの順位を考慮しなきゃ
いけないってのが好きじゃないんだ
これはどうかな?

def fizzbuzz(n)
  x = ""
  x << "Fizz" if n%3 == 0
  x << "Buzz" if n%5 == 0
  x.empty? ? n : x
end

fizzbuzz(1) # => 1
fizzbuzz(3) # => "Fizz"
fizzbuzz(4) # => 4
fizzbuzz(5) # => "Buzz"
fizzbuzz(10) # => "Buzz"
fizzbuzz(15) # => "FizzBuzz"

まあこれは趣味の問題かもね..

小さな問題2

さて2つ目の小さな問題を解くよ
2つ目は
「1からxまでの数をその関数に適用する関数を作る」
だったね
RubyにはEnumeratorがあるから
これはばかみたいに簡単だよね
関数名をmap_uptoにしよう

def map_upto(max, f)
  (1..max).map { |n| f[n] }
end

map_upto(15, method(:fizzbuzz)) # => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

小さな問題3

次に3つ目の小さな問題を解くよ
3つ目は
スクリプト引数xを2の関数に与えて
結果をターミナルに出力する関数を作る」
だったね
Rubyスクリプトに与えられた引数は
ARGVという配列に格納されるよね
またターミナルへの改行出力はputsだよね
だから次のようになるよ
関数名をconsoleとするよ

def console(f)
  raise "need an argument of integer" if ARGV[0].nil?
  max = ARGV[0].to_i
  f[max].each { |e| puts e }
end

ここでは引数がない場合のチェックしかしてないけど
厳密なチェックが必要ならここで書くことになるよ


さあこれで完成だよ
コードをまとめて再掲するよ

def fizzbuzz(n)
  x = ""
  x << "Fizz" if n%3 == 0
  x << "Buzz" if n%5 == 0
  x.empty? ? n : x
end

def map_upto(max, f)
  (1..max).map { |n| f[n] }
end

def console(f)
  raise "need an argument of integer" if ARGV[0].nil?
  max = ARGV[0].to_i
  f[max].each { |e| puts e }
end

if __FILE__ == $0
  fizzbuzz_upto = ->max{ map_upto(max, method(:fizzbuzz)) }
  console fizzbuzz_upto
end

さあ実行してみよう

$ ruby fizzbuzz.rb 15
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

うまくいったね!


問題を切り分けて
一つづつ解いていけば簡単だね


経験あるプログラマ
これらを瞬時に頭の中でやってしまうから
ぼくらの気持ちがわからないんだね
次のようなコードをよく見るけど
個人的には問題の切り分けができてないから
良いコードとは思えないんだよ

max = ARGV[0].to_i
(1..max).each do |n|
  res =
    case 
    when n%3 == 0 && n%5 == 0; 'FizzBuzz'
    when n%3 == 0; 'Fizz'
    when n%5 == 0; 'Buzz'
    else n
    end
  puts res
end

テストしづらいし改変にも弱いからね


先のコードならテストしやすいし改変にも強そうだよね

require "test/unit"
require "stringio"
require "./fizzbuzz"

class TestFizzBuzz < Test::Unit::TestCase
  def setup
    @ans = [1,2,'Fizz',4,'Buzz','Fizz',7,8,'Fizz','Buzz',11,'Fizz',13,14,'FizzBuzz']
  end

  def test_fizzbuzz
    (1..15).each { |n| assert_equal(@ans[n-1], fizzbuzz(n)) }
  end

  def test_map_upto
    assert_equal(@ans, map_upto(15, method(:fizzbuzz)))
  end

  def test_console
    begin
      $stdout = op = StringIO.new("", 'w')
      fizzbuzz_upto = ->max{ map_upto(max, method(:fizzbuzz)) }
      console(fizzbuzz_upto)
      out = str2fizzbuzz_list(op.string)
      assert_equal(@ans, out)
    ensure
      $stdout = STDOUT
    end
  end

  def str2fizzbuzz_list(str)
    str.split.map { |n| n =~ /(Fi|Bu)zz/ ? n : n.to_i }
  end
end


(追記:2011-10-15)関数の機能分離が不十分だったので修正しました。


関連記事:
Yet Another Ruby FizzBuzz - hp12c
Yet Another Ruby FizzBuzz その2 - hp12c
Yet Another Ruby FizzBuzz その3 - hp12c
Yet Another Ruby FizzBuzz その4 - hp12c