99 bottles in Ruby


(Daniel Hollands) #1

Below is some code which produces the 99 bottles of beer :beers: song using Ruby.

This exists as my first practice session while following along the 99 bottles book by Sandi Metz.

This was all inspired by the Ruby Book Club, a podcast in which the hosts read an hour of a ruby book per week, then discuss their thoughts on what they read, and they just so happen to be reading 99 bottles.

Anyway, the code below is horrible, but all the tests pass, so I’m as happy as I can be for someone that’s not yet read the book. I’m hoping once I’ve read the book, I’ll be able to come back here and see just how horrible my implementation was, and maybe learn some object-oriented design as I go.

Anyway, here’s the code:

# lib/bottles.rb
class Bottles
  def verse(bottles)
    <<-EOF
#{how_many_bottles(bottles).capitalize} of beer on the wall, #{how_many_bottles(bottles)} of beer.
#{do_what(bottles)}, #{how_many_bottles(bottles - 1)} of beer on the wall.
    EOF
  end

  def verses(first, last)
    (last..first).to_a.reverse.map do |bottles|
      verse(bottles)
    end.join("\n")
  end

  def song
    verses(99, 0)
  end

  private

  def how_many_bottles(bottles)
    return '99 bottles' if bottles == -1
    return 'no more bottles' if bottles == 0
    return "#{bottles} bottle" if bottles == 1
    "#{bottles} bottles"
  end

  def what(bottles)
    return 'it' if bottles == 1
    'one'
  end

  def do_what(bottles)
    return 'Go to the store and buy some more' if bottles < 1
    "Take #{what(bottles)} down and pass it around"
  end
end

and the test class:

# test/bottles_test.rb
gem 'minitest', '~> 5.4'
require 'minitest/autorun'
require_relative '../lib/bottles'

class BottlesTest < Minitest::Test
  def test_the_first_verse
    expected = <<-VERSE
99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.
VERSE
    assert_equal expected, ::Bottles.new.verse(99)
  end

  def test_another_verse
    expected = <<-VERSE
89 bottles of beer on the wall, 89 bottles of beer.
Take one down and pass it around, 88 bottles of beer on the wall.
VERSE
    assert_equal expected, ::Bottles.new.verse(89)
  end

  def test_verse_2
    expected = <<-VERSE
2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.
    VERSE
    assert_equal expected, ::Bottles.new.verse(2)
  end

  def test_verse_1
    expected = <<-VERSE
1 bottle of beer on the wall, 1 bottle of beer.
Take it down and pass it around, no more bottles of beer on the wall.
    VERSE
    assert_equal expected, ::Bottles.new.verse(1)
  end

  def test_verse_0
    expected = <<-VERSE
No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.
    VERSE
    assert_equal expected, ::Bottles.new.verse(0)
  end

  def test_a_couple_verses
    expected = <<-VERSES
99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.

98 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.
VERSES
    assert_equal expected, ::Bottles.new.verses(99, 98)
  end

  def test_a_few_verses
    expected = <<-VERSES
2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.

1 bottle of beer on the wall, 1 bottle of beer.
Take it down and pass it around, no more bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.
VERSES
    assert_equal expected, ::Bottles.new.verses(2, 0)
  end

  def test_the_whole_song
    expected = <<-SONG
99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.

98 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

97 bottles of beer on the wall, 97 bottles of beer.
Take one down and pass it around, 96 bottles of beer on the wall.

96 bottles of beer on the wall, 96 bottles of beer.
Take one down and pass it around, 95 bottles of beer on the wall.

95 bottles of beer on the wall, 95 bottles of beer.
Take one down and pass it around, 94 bottles of beer on the wall.

94 bottles of beer on the wall, 94 bottles of beer.
Take one down and pass it around, 93 bottles of beer on the wall.

93 bottles of beer on the wall, 93 bottles of beer.
Take one down and pass it around, 92 bottles of beer on the wall.

92 bottles of beer on the wall, 92 bottles of beer.
Take one down and pass it around, 91 bottles of beer on the wall.

91 bottles of beer on the wall, 91 bottles of beer.
Take one down and pass it around, 90 bottles of beer on the wall.

90 bottles of beer on the wall, 90 bottles of beer.
Take one down and pass it around, 89 bottles of beer on the wall.

89 bottles of beer on the wall, 89 bottles of beer.
Take one down and pass it around, 88 bottles of beer on the wall.

88 bottles of beer on the wall, 88 bottles of beer.
Take one down and pass it around, 87 bottles of beer on the wall.

87 bottles of beer on the wall, 87 bottles of beer.
Take one down and pass it around, 86 bottles of beer on the wall.

86 bottles of beer on the wall, 86 bottles of beer.
Take one down and pass it around, 85 bottles of beer on the wall.

85 bottles of beer on the wall, 85 bottles of beer.
Take one down and pass it around, 84 bottles of beer on the wall.

84 bottles of beer on the wall, 84 bottles of beer.
Take one down and pass it around, 83 bottles of beer on the wall.

83 bottles of beer on the wall, 83 bottles of beer.
Take one down and pass it around, 82 bottles of beer on the wall.

82 bottles of beer on the wall, 82 bottles of beer.
Take one down and pass it around, 81 bottles of beer on the wall.

81 bottles of beer on the wall, 81 bottles of beer.
Take one down and pass it around, 80 bottles of beer on the wall.

80 bottles of beer on the wall, 80 bottles of beer.
Take one down and pass it around, 79 bottles of beer on the wall.

79 bottles of beer on the wall, 79 bottles of beer.
Take one down and pass it around, 78 bottles of beer on the wall.

78 bottles of beer on the wall, 78 bottles of beer.
Take one down and pass it around, 77 bottles of beer on the wall.

77 bottles of beer on the wall, 77 bottles of beer.
Take one down and pass it around, 76 bottles of beer on the wall.

76 bottles of beer on the wall, 76 bottles of beer.
Take one down and pass it around, 75 bottles of beer on the wall.

75 bottles of beer on the wall, 75 bottles of beer.
Take one down and pass it around, 74 bottles of beer on the wall.

74 bottles of beer on the wall, 74 bottles of beer.
Take one down and pass it around, 73 bottles of beer on the wall.

73 bottles of beer on the wall, 73 bottles of beer.
Take one down and pass it around, 72 bottles of beer on the wall.

72 bottles of beer on the wall, 72 bottles of beer.
Take one down and pass it around, 71 bottles of beer on the wall.

71 bottles of beer on the wall, 71 bottles of beer.
Take one down and pass it around, 70 bottles of beer on the wall.

70 bottles of beer on the wall, 70 bottles of beer.
Take one down and pass it around, 69 bottles of beer on the wall.

69 bottles of beer on the wall, 69 bottles of beer.
Take one down and pass it around, 68 bottles of beer on the wall.

68 bottles of beer on the wall, 68 bottles of beer.
Take one down and pass it around, 67 bottles of beer on the wall.

67 bottles of beer on the wall, 67 bottles of beer.
Take one down and pass it around, 66 bottles of beer on the wall.

66 bottles of beer on the wall, 66 bottles of beer.
Take one down and pass it around, 65 bottles of beer on the wall.

65 bottles of beer on the wall, 65 bottles of beer.
Take one down and pass it around, 64 bottles of beer on the wall.

64 bottles of beer on the wall, 64 bottles of beer.
Take one down and pass it around, 63 bottles of beer on the wall.

63 bottles of beer on the wall, 63 bottles of beer.
Take one down and pass it around, 62 bottles of beer on the wall.

62 bottles of beer on the wall, 62 bottles of beer.
Take one down and pass it around, 61 bottles of beer on the wall.

61 bottles of beer on the wall, 61 bottles of beer.
Take one down and pass it around, 60 bottles of beer on the wall.

60 bottles of beer on the wall, 60 bottles of beer.
Take one down and pass it around, 59 bottles of beer on the wall.

59 bottles of beer on the wall, 59 bottles of beer.
Take one down and pass it around, 58 bottles of beer on the wall.

58 bottles of beer on the wall, 58 bottles of beer.
Take one down and pass it around, 57 bottles of beer on the wall.

57 bottles of beer on the wall, 57 bottles of beer.
Take one down and pass it around, 56 bottles of beer on the wall.

56 bottles of beer on the wall, 56 bottles of beer.
Take one down and pass it around, 55 bottles of beer on the wall.

55 bottles of beer on the wall, 55 bottles of beer.
Take one down and pass it around, 54 bottles of beer on the wall.

54 bottles of beer on the wall, 54 bottles of beer.
Take one down and pass it around, 53 bottles of beer on the wall.

53 bottles of beer on the wall, 53 bottles of beer.
Take one down and pass it around, 52 bottles of beer on the wall.

52 bottles of beer on the wall, 52 bottles of beer.
Take one down and pass it around, 51 bottles of beer on the wall.

51 bottles of beer on the wall, 51 bottles of beer.
Take one down and pass it around, 50 bottles of beer on the wall.

50 bottles of beer on the wall, 50 bottles of beer.
Take one down and pass it around, 49 bottles of beer on the wall.

49 bottles of beer on the wall, 49 bottles of beer.
Take one down and pass it around, 48 bottles of beer on the wall.

48 bottles of beer on the wall, 48 bottles of beer.
Take one down and pass it around, 47 bottles of beer on the wall.

47 bottles of beer on the wall, 47 bottles of beer.
Take one down and pass it around, 46 bottles of beer on the wall.

46 bottles of beer on the wall, 46 bottles of beer.
Take one down and pass it around, 45 bottles of beer on the wall.

45 bottles of beer on the wall, 45 bottles of beer.
Take one down and pass it around, 44 bottles of beer on the wall.

44 bottles of beer on the wall, 44 bottles of beer.
Take one down and pass it around, 43 bottles of beer on the wall.

43 bottles of beer on the wall, 43 bottles of beer.
Take one down and pass it around, 42 bottles of beer on the wall.

42 bottles of beer on the wall, 42 bottles of beer.
Take one down and pass it around, 41 bottles of beer on the wall.

41 bottles of beer on the wall, 41 bottles of beer.
Take one down and pass it around, 40 bottles of beer on the wall.

40 bottles of beer on the wall, 40 bottles of beer.
Take one down and pass it around, 39 bottles of beer on the wall.

39 bottles of beer on the wall, 39 bottles of beer.
Take one down and pass it around, 38 bottles of beer on the wall.

38 bottles of beer on the wall, 38 bottles of beer.
Take one down and pass it around, 37 bottles of beer on the wall.

37 bottles of beer on the wall, 37 bottles of beer.
Take one down and pass it around, 36 bottles of beer on the wall.

36 bottles of beer on the wall, 36 bottles of beer.
Take one down and pass it around, 35 bottles of beer on the wall.

35 bottles of beer on the wall, 35 bottles of beer.
Take one down and pass it around, 34 bottles of beer on the wall.

34 bottles of beer on the wall, 34 bottles of beer.
Take one down and pass it around, 33 bottles of beer on the wall.

33 bottles of beer on the wall, 33 bottles of beer.
Take one down and pass it around, 32 bottles of beer on the wall.

32 bottles of beer on the wall, 32 bottles of beer.
Take one down and pass it around, 31 bottles of beer on the wall.

31 bottles of beer on the wall, 31 bottles of beer.
Take one down and pass it around, 30 bottles of beer on the wall.

30 bottles of beer on the wall, 30 bottles of beer.
Take one down and pass it around, 29 bottles of beer on the wall.

29 bottles of beer on the wall, 29 bottles of beer.
Take one down and pass it around, 28 bottles of beer on the wall.

28 bottles of beer on the wall, 28 bottles of beer.
Take one down and pass it around, 27 bottles of beer on the wall.

27 bottles of beer on the wall, 27 bottles of beer.
Take one down and pass it around, 26 bottles of beer on the wall.

26 bottles of beer on the wall, 26 bottles of beer.
Take one down and pass it around, 25 bottles of beer on the wall.

25 bottles of beer on the wall, 25 bottles of beer.
Take one down and pass it around, 24 bottles of beer on the wall.

24 bottles of beer on the wall, 24 bottles of beer.
Take one down and pass it around, 23 bottles of beer on the wall.

23 bottles of beer on the wall, 23 bottles of beer.
Take one down and pass it around, 22 bottles of beer on the wall.

22 bottles of beer on the wall, 22 bottles of beer.
Take one down and pass it around, 21 bottles of beer on the wall.

21 bottles of beer on the wall, 21 bottles of beer.
Take one down and pass it around, 20 bottles of beer on the wall.

20 bottles of beer on the wall, 20 bottles of beer.
Take one down and pass it around, 19 bottles of beer on the wall.

19 bottles of beer on the wall, 19 bottles of beer.
Take one down and pass it around, 18 bottles of beer on the wall.

18 bottles of beer on the wall, 18 bottles of beer.
Take one down and pass it around, 17 bottles of beer on the wall.

17 bottles of beer on the wall, 17 bottles of beer.
Take one down and pass it around, 16 bottles of beer on the wall.

16 bottles of beer on the wall, 16 bottles of beer.
Take one down and pass it around, 15 bottles of beer on the wall.

15 bottles of beer on the wall, 15 bottles of beer.
Take one down and pass it around, 14 bottles of beer on the wall.

14 bottles of beer on the wall, 14 bottles of beer.
Take one down and pass it around, 13 bottles of beer on the wall.

13 bottles of beer on the wall, 13 bottles of beer.
Take one down and pass it around, 12 bottles of beer on the wall.

12 bottles of beer on the wall, 12 bottles of beer.
Take one down and pass it around, 11 bottles of beer on the wall.

11 bottles of beer on the wall, 11 bottles of beer.
Take one down and pass it around, 10 bottles of beer on the wall.

10 bottles of beer on the wall, 10 bottles of beer.
Take one down and pass it around, 9 bottles of beer on the wall.

9 bottles of beer on the wall, 9 bottles of beer.
Take one down and pass it around, 8 bottles of beer on the wall.

8 bottles of beer on the wall, 8 bottles of beer.
Take one down and pass it around, 7 bottles of beer on the wall.

7 bottles of beer on the wall, 7 bottles of beer.
Take one down and pass it around, 6 bottles of beer on the wall.

6 bottles of beer on the wall, 6 bottles of beer.
Take one down and pass it around, 5 bottles of beer on the wall.

5 bottles of beer on the wall, 5 bottles of beer.
Take one down and pass it around, 4 bottles of beer on the wall.

4 bottles of beer on the wall, 4 bottles of beer.
Take one down and pass it around, 3 bottles of beer on the wall.

3 bottles of beer on the wall, 3 bottles of beer.
Take one down and pass it around, 2 bottles of beer on the wall.

2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.

1 bottle of beer on the wall, 1 bottle of beer.
Take it down and pass it around, no more bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.
    SONG
    assert_equal expected, ::Bottles.new.song
  end
end

(Jon) #2

Go to the offie, surely? :beer:


(Marc Cooper) #3

So, you put up some code from which you plan to do some learning. I feel comment is allowed :slight_smile:

I enjoy the way Sandi communicates about code and coding. Her book, Practical OO Design in Ruby, is the only one I own in dead tree format, and I try to read it every year to refresh the basics. It’s also a book I frequently recommend to devs of all levels (you don’t need to know ruby to read it).

I also share Sandi’s enjoyment of looking at why we code in certain ways and analysing codebases. Reading, Your Code is a Crime Scene, and running Code Maat is my idea of fun!

To the code

So, I’ve joined Limeblast Inc. and been asked to audit the codebase:

First observations:

  • It’s not spaghetti code \o/
  • (last..first).to_a.reverse.map do |bottles|Not sure what this is doing at first glance. And converting a Range to an Array is odd. Also, Integer has some handy methods like upto() and downto. Can simplify this.
  • how_many_bottles(bottles)is a case-statement in disguise.
  • -1 bottles is an “interesting” idea
  • "#{bottles} bottle"can safely be "1 bottle". I think that’s clearer once the transformation to a case-statement has been made.
  • Use of both single and double quotes.
  • Some method names don’t really signal intent. e.g. what() and do_what()
  • what()and do_what()are ternaries. Once that’s done, then what() can go. So perhaps do_what() is clearer as an if.

After, that I ended up with:

class Bottles
  def song
    verses(99, 0)
  end

  def verses(first, last)
    first.downto(last).map { |bottles| verse(bottles)}.join("\n")
  end

  def verse(bottles)
    <<-EOF
#{how_many_bottles(bottles).capitalize} of beer on the wall, #{how_many_bottles(bottles)} of beer.
#{do_what(bottles)}, #{how_many_bottles(bottles - 1)} of beer on the wall.
    EOF
  end

  def how_many_bottles(bottles)
    case bottles
    when -1
      "99 bottles"
    when 0
      "no more bottles"
    when 1
      "1 bottle"
    else
      "#{bottles} bottles"
    end
  end

  def do_what(bottles)
    if bottles < 1
      "Go to the store and buy some more"
    else
      "Take #{bottles == 1 ? "it" : "one"} down and pass it around"
    end
  end
end

I find this mostly easy to reason with it — it’s a song with verses from i to j — but I have to go to work to understand what’s happening with each verse. So, in cases like this, I ask myself whether there’s a useful abstraction for verse and, if so, what’s the best/right one? Doing this requires putting the current code to one side.

My reasoning is that there are only three or four types of verse (see the case-statement) and the likelihood of them changing — in the sense of requiring additional behaviour — is very low. and each verse type is very specific. Also, there’s no generic song or verse here. So there’s unlikely to be any use for objects of those types. It kinda feels like everything that’s here should remain here, despite there being obvious “nouns” that could be turned into objects. Best to be guided by behaviour.

At that point, I’d look more closely at the specific verse types. i.e. merge everything and then pull out anything obvious.

However, there didn’t seem to be anything to pull out! I ended up with

class Bottles
  FIRST = 99
  LAST = 0

  def song
    verses(FIRST, LAST)
  end

  def verses(first, last)
    first.downto(last).map { |bottles| verse(bottles)}.join("\n")
  end

  def verse(bottles)
    case bottles
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, #{FIRST} bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"
    else
      "#{bottles} bottles of beer on the wall, #{bottles} bottles of beer.\nTake one down and pass it around, #{bottles - 1} bottles of beer on the wall.\n"
    end
  end
end

And this is a neat example of why refactoring (and tests, obviously) is so important. This version is not only simple to reason with, it is less complex (no branching), and there’s less code. One of the things that always surprises me is how much a codebase shrinks when you remove wrong abstractions.

I’m not sure of the psychology behind this. I think it’s a sense that there’s duplication that can be removed. Perhaps a confusion between syntactic and semantic duplication.

It’s easy for this to go wayward, though. i.e. pull out stuff just because you can. I know folk who wouldn’t be able to stop themselves from doing this /o\ :

  def verse(bottles)
    case bottles
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, #{FIRST} bottles of beer on the wall.\n"
    when 1, 2
      "#{bottles} bottle#{bottles == 2 ? "s" : ""} of beer on the wall, #{bottles} bottle#{bottles == 2 ? "s" : ""} of beer.\n" +
      "Take #{bottles == 2 ? "one" : "it"} down and pass it around, #{bottles == 2 ? "1 bottle" : "no more bottles"} of beer on the wall.\n"
    else
      "#{bottles} bottles of beer on the wall, #{bottles} bottles of beer.\nTake one down and pass it around, #{bottles - 1} bottles of beer on the wall.\n"
    end
  end

Anyway, it’s always interesting to examine ways that code can be improved. There’s no one way, and the only way to improve is to study the art. imo, far too few do, sadly.


(Daniel Hollands) #4

Of course.

TBH, I’m still trying to unlearn a lot of the nonsense I learned in PHP. Not that anything like this is even possible in PHP, exactly, but it’s a PHP-brain’s solution to the problem of getting an array of numbers in reverse order. Not even realising that something as simple (and ruby-ish) as .downto exists… But I now know.

I’d argue that it was an abstracted case-statement, but I guess the outcome is the same.

I use single quotes wherever possible, switching to double quotes if I need interpolation. This is something that’s been discussed before.

They’re terrible names, and I know they’re terrible, but I was having trouble thinking of anything better, and I only had half hour to get as much done as I could, so opted for a working solution over a tidy one :grimacing: .


I do want to read and respond to the rest of your post, but my brain isn’t fully functioning right now, so I’ll return to this later in the week.


(Marc Cooper) #5

This popped into my head while practising my Spanish! :flag_es: Stepping away from the keyboard also an essential technique for generating ideas :stuck_out_tongue:

I’m quite fond of mapping stuff

class Bottles
  FIRST = 99
  LAST = 0

  VERSES = {
    0 => "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, #{FIRST} bottles of beer on the wall.\n",
    1 => "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n",
    2 => "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n",
  }

  def song
    verses(FIRST, LAST)
  end

  def verses(first, last)
    first.downto(last).map { |bottles| verse(bottles)}.join("\n")
  end

  def verse(bottles)
    VERSES.fetch(bottles) do |bottles|
      "#{bottles} bottles of beer on the wall, #{bottles} bottles of beer.\nTake one down and pass it around, #{bottles - 1} bottles of beer on the wall.\n"
    end
  end
end