初めまして、hiraです。
弊社に入社してからもう7ヶ月も経っていますが、TechRachoの記事を書くのはこれが初めてです。
アドベントカレンダーということで、ないコンテンツを絞って書いてみました。
FizzBuzz
さて今回書く内容は、FizzBuzzです。
プログラマなら大抵ご存知と思いますが、簡単に内容に触れておくと、プログラマの技術力を測るための問題で、以下のような内容です。
「1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリント し、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。」 (「どうしてプログラマに・・・プログラムが書けないのか?」より引用)
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, ...
みたいな出力が得られればOKです。区切り文字は本質ではないのでなんでもよいです。私は改行で実装しています。
また、Fizzだけだとあっているかわかりにくいので、以降3: Fizz
のような形で出力するようにします。
普通に解く
前書きはこれくらいにして早速解いていきましょう。
私なら次のように書きます。
MAX = 30
def fizz_buzz(n)
ret = ''
ret = 'Fizz' if n % 3 == 0
ret += 'Buzz' if n % 5 == 0
ret.empty? ? n.to_s : "#{n}: #{ret}"
end
1.upto(MAX) { |i| puts fizz_buzz(i) }
実行すれば以下のようになるはずです。
1
2
3: Fizz
4
5: Buzz
6: Fizz
7
8
9: Fizz
10: Buzz
11
12: Fizz
13
14
15: FizzBuzz
16
17
18: Fizz
19
20: Buzz
21: Fizz
22
23
24: Fizz
25: Buzz
26
27: Fizz
28
29
30: FizzBuzz
次のように4パターンに場合分けする方も多くいらっしゃると思います。速度は文字列の連結分、上のコードより速いらしいです。ですが、以降の拡張性を考えて上のやり方で進めます。
MAX = 30
def fizz_buzz(n)
if n % 15 == 0
"#{n}: FizzBuzz"
elsif n % 3 == 0
"#{n}: Fizz"
elsif n % 5 == 0
"#{n}: Buzz"
else
n.to_s
end
end
オブジェクト指向的
さてこれらのコードだと、「オブジェクト指向でない!」という声がどこからか飛んできそうです。
ということで、Integerクラスに#to_fizz_buzz
を追加してみましょう。
MAX = 30
class Integer
def to_fizz_buzz
ret = ''
ret = 'Fizz' if self % 3 == 0
ret += 'Buzz' if self % 5 == 0
ret.empty? ? self.to_s : "#{self}: #{ret}"
end
end
1.upto(MAX) { |i| puts i.to_fizz_buzz }
どうせなら条件式もメソッドにしましょう。
MAX = 30
class Integer
def to_fizz_buzz
ret = ''
ret = 'Fizz' if self.multiple?(3)
ret += 'Buzz' if self.multiple?(5)
ret.empty? ? self.to_s : "#{self}: #{ret}"
end
def multiple?(n)
n.kind_of?(Integer) && self % n == 0
end
end
1.upto(MAX) { |i| puts i.to_fizz_buzz }
出力の部分は、
puts (1..MAX).map(&:to_fizz_buzz).join("\n")
でもよいのですが、これだとMAX = Float::INFINITY
にしたとき、mapで止まってしまうのが不満です。
一般化
このまま普通のFizzBuzzを続ける手もありますが、結構手垢のついたネタなので、これぐらいで切り上げて、一般化をしてみます。
3, 5とくれば普通に考えると7, 11, 13と素数列が続きそうです。
例えば、FizzBuzzHogeは3の倍数がFizz, 5の倍数がBuzz, 7の倍数がHogeということで、
1, 2, 3: Fizz, 4, 5: Buzz, 6: Fizz, 7: Hoge, ... , 21: FizzHoge, ..., 105: FizzBuzzHoge
となりそうです。
素数は自動的に決定できるので、0個以上の文字列をコンストラクタが受け取るようなGeneralFizzBuzzクラスを作ればよさそうです。(FizzBuzzHogeの場合'Fizz', 'Buzz', 'Hoge'
をコンストラクタに渡す)
とすると、GeneralFizzBuzzクラスは次のように書けます。
require 'prime'
class GeneralFizzBuzz
def initialize(*keywords)
@keywords = keywords
end
def execute(max)
1.upto(max) { |n| puts output(n) }
end
def output(n)
ret = ''
@keywords.zip(Prime.lazy.reject { |prime| prime == 2 }).each do |keyword, prime|
ret += keyword if n % prime == 0
end
ret.empty? ? n.to_s : "#{n}: #{ret}"
end
end
@keywords.zip(Prime.lazy.reject { |prime| prime == 2 })
は文字列の配列と3以上の素数列をzipしています。Primeは無限の長さを持つので、単にrejectしてしまうと処理が無限に継続してしまいます。そのため、Enumerator#lazy
を用いてそれを回避しています。
Enumerator#lazy
は自身をEnumerator::Lazy
オブジェクトに変えたものを返すメソッドで、Enumerator::Lazy
は関数型言語でよく用いられる遅延評価を可能にするオブジェクトです。
Prime.lazy.reject { |prime| prime == 2 }
の代わりにPrime.take(@keywords.size + 1)[1..-1]
としてもよいです。
動かしてみると以下のようになります。
irb > GeneralFizzBuzz.new('Fizz', 'Buzz', 'BPS').execute(30)
1
2
3: Fizz
4
5: Buzz
6: Fizz
7: BPS
8
9: Fizz
10: Buzz
11
12: Fizz
13
14: BPS
15: FizzBuzz
16
17
18: Fizz
19
20: Buzz
21: FizzBPS
22
23
24: Fizz
25: Buzz
26
27: Fizz
28: BPS
29
30: FizzBuzz
ちゃんと7の倍数でBPSが出力されるようになっています。
#method_missing
を使って一般化
最後に、かの#method_missing
を使ってみたかったので、それを使った実装を紹介して終わります。
require 'prime'
class Range
def method_missing(method_name)
keywords = method_name.to_s.split('_').map(&:capitalize)
GeneralFizzBuzz.new(self, *keywords).execute
end
end
class GeneralFizzBuzz
def initialize(range, *keywords)
@range = range
@keywords = keywords
end
def execute
@range.each { |n| puts output(n) }
end
def output(n)
ret = ''
@keywords.zip(Prime.lazy.reject { |prime| prime == 2 }).each do |keyword, prime|
ret += keyword if n % prime == 0
end
ret.empty? ? n.to_s : "#{n}: #{ret}"
end
end
Rangeの定義されていないメソッドが呼び出されたときに、メソッド名を_
で分割し、頭文字を大文字にした上で、GeneralFizzBuzzのコンストラクタに渡しています。 (1..MAX).fizz_buzz
で通常のFizzBuzzとなります。
irb > (1..30).fizz_buzz_bps_advent_calendar
1
2
3: Fizz
4
5: Buzz
6: Fizz
7: Bps
8
9: Fizz
10: Buzz
11: Advent
12: Fizz
13: Calendar
14: Bps
15: FizzBuzz
16
17
18: Fizz
19
20: Buzz
21: FizzBps
22: Advent
23
24: Fizz
25: Buzz
26: Calendar
27: Fizz
28: Bps
29
30: FizzBuzz
ちゃんと11の倍数でAdvent, 13の倍数でCalendarが出力できています。
めでたしめでたし。
まとめ
#method_missing
や#lazy
と実務ではなかなか使わないメソッドが使えて案外楽しかったです。
ただ、これを面接で聞かれて最後の方の実装を答えた場合にどんな顔をされるのかは少々気になるところです。
また、これ以外にも、ラムダ式やカリー化を使って色々できると思うので、皆さんも興味があればやってみてください。