プログラミング経験者なら一度は通る FizzBuzz。
このお題に「除算(/)・剰余算(%)禁止」の制約を設けると、面白さが格段にアップします。
様々な解決アプローチを考えることで頭の体操になりますし、語彙を増やすきっかけにもなります。
以下、自分の考えたアプローチをいくつか紹介します(Ruby 2.0.0 以降で動作)。
他にも「こんなやり方あるよ!」というのがあれば是非教えてください。
1. 飛び飛びで値を格納していくアプローチ
1.1 Integer#step
の利用
空の Array
を定義し、3個飛ばしで 'Fizz'
を格納し、5個飛ばしで 'Buzz'
を格納or追記します。
隙間の要素は nil
で埋められるので、要素が nil
の場合は整数を出力します。
num = 100
list = []
3.step(num, 3) {|i| list[i] = 'Fizz' }
5.step(num, 5) {|i| list[i] = list[i].to_s + 'Buzz' }
# list の中身: [ nil, nil, nil, 'Fizz', nil, 'Buzz',
# 'Fizz', nil, nil, 'Fizz', 'Buzz', ... ]
(1..num).each {|i| puts list[i] || i }
1.2 Hash
を使った改良版
空の Hash
に 'Fizz'
, 'Buzz'
, 'FizzBuzz'
を出力する要素を格納していきます。
それ以外の要素は Hash#fetch
のデフォルト値として取得します。
Hash
では要素番号が飛び飛びになる場合でも隙間の要素が nil
で埋められることはないため、
Array
よりも要素数を少なくできます。
num = 100
list = {}
3.step(num, 3) {|i| list[i] = 'Fizz' }
5.step(num, 5) {|i| list[i] = list[i].to_s + 'Buzz' }
# list の中身: { 3 => 'Fizz', 6 => 'Fizz', 9 => 'Fizz', ... ,
# 5 => 'Buzz', 10 => 'Buzz', 15 => 'FizzBuzz', ... }
(1..num).each {|i| puts list.fetch(i, i) }
Hash#fetch
の代わりに Hash
のデフォルト値を使う方法もありますが、記述が冗長になるため割愛します。
2. 周期的構造を利用するアプローチ
2.1 15進数表現の利用
Integer#to_s
は、第一引数を指定することで2〜36進数の文字列表現を返します。
このメソッドを利用して、15進数表現の最下位桁で FizzBuzz 判定します。
class Integer
def to_fizzbuzz
case to_s(15)[-1]
when *%w(3 6 9 c)
'Fizz'
when *%w(5 a)
'Buzz'
when *%w(0)
'FizzBuzz'
else
self
end
end
end
# main
num = 100
(1..num).each {|i| puts i.to_fizzbuzz }
2.2 Enumerator
オブジェクトの利用
each
メソッドは、ブロックを省略すると Enumerator
オブジェクトを返します。
Enumerator
オブジェクトには #next
や #rewind
など、列挙状態を手動で1つ先に進めたり巻き戻したりするメソッドが定義されています。
# 以下のようにも書ける
# * fizz_enum = [nil, nil, 'Fizz'].to_enum
# * fizz_enum = Enumerator.new([nil, nil, 'Fizz'])
fizz_enum = [nil, nil, 'Fizz'].each
buzz_enum = [nil, nil, nil, nil, 'Buzz'].each
num = 100
(1..num).each do |i|
(fizz = fizz_enum.next) && fizz_enum.rewind
(buzz = buzz_enum.next) && buzz_enum.rewind
puts (fizz || buzz) ? "#{fizz}#{buzz}" : i
end
2.3 cycle
メソッドによる Enumerator
を使った改良版
each
でなく cycle
メソッドによる Enumerator
オブジェクトを利用すれば、
#rewind
の記述が不要になります。
# 以下のようにも書ける
# * fizz_cycle = [nil, nil, 'Fizz'].to_enum(:cycle)
# * fizz_cycle = Enumerator.new([nil, nil, 'Fizz'], :cycle)
fizz_cycle = [nil, nil, 'Fizz'].cycle
buzz_cycle = [nil, nil, nil, nil, 'Buzz'].cycle
num = 100
(1..num).each do |i|
fizz = fizz_cycle.next
buzz = buzz_cycle.next
puts (fizz || buzz) ? "#{fizz}#{buzz}" : i
end
X. ネタ的アプローチ
X.1 スリープソート的なやつ
1, 3, 5, 15秒ごとにそれぞれ 整数
, 'Fizz'
, 'Buzz'
, 'FizzBuzz'
を標準出力するスレッドを同時実行します。
- 100個の出力に100秒かかります
- うっかり
3
とか出力したあとにこっそりFizz
とかに書き換えます(\r
で)- タイミングの問題でたまに出力間違えますが許してあげてください
- 整数が9999超えると書き換え漏れしますが許して(ry
class FizzBuzzTimer
def initialize(num)
@num = num
end
def start
running_threads = [
integer_thread,
fizz_thread,
buzz_thread,
fizzbuzz_thread,
]
sleep(@num)
ensure
running_threads.each(&:kill)
end
private
def integer_thread
num = 0
Thread.new do
loop do
print num += 1
sleep(1)
print "\n"
end
end
end
def fizz_thread
Thread.new do
loop do
sleep(3)
print "\rFizz"
end
end
end
def buzz_thread
Thread.new do
loop do
sleep(5)
print "\rBuzz"
end
end
end
def fizzbuzz_thread
Thread.new do
loop do
sleep(15)
print "\rFizzBuzz"
end
end
end
end
# main
num = 100
FizzBuzzTimer.new(num).start