Help us understand the problem. What is going on with this article?

除算(/)・剰余算(%)を使わない FizzBuzz - Ruby 編

More than 3 years have passed since last update.

プログラミング経験者なら一度は通る FizzBuzz。
このお題に「除算(/)・剰余算(%)禁止」の制約を設けると、面白さが格段にアップします。
様々な解決アプローチを考えることで頭の体操になりますし、語彙を増やすきっかけにもなります。

以下、自分の考えたアプローチをいくつか紹介します(Ruby 2.0.0 以降で動作)。
他にも「こんなやり方あるよ!」というのがあれば是非教えてください。

1. 飛び飛びで値を格納していくアプローチ

1.1 Integer#step の利用

空の Array を定義し、3個飛ばしで 'Fizz' を格納し、5個飛ばしで 'Buzz' を格納or追記します。
隙間の要素は nil で埋められるので、要素が nil の場合は整数を出力します。

fizzbuzz_step_array.rb
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 よりも要素数を少なくできます。

fizzbuzz_step_hash.rb
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 判定します。

fizzbuzz_15-ary.rb
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つ先に進めたり巻き戻したりするメソッドが定義されています。

fizzbuzz_enumerator.rb
# 以下のようにも書ける
# * 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 の記述が不要になります。

fizzbuzz_cycle_enumerator.rb
# 以下のようにも書ける
# * 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
fizzbuzz_timer.rb
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

他にもあれば随時追記

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away