Python vs. Swift string interpolation - 99 Bottles of Milk

I’m pretty new to Swift, but I’m very familiar with Python, so when I got to this project I wanted to take it up a notch with the optional tasks at the end — specifically, “Assign the lyrics of the song to some String variables.”

I’d be interested in any feedback by more experienced Swift developers if there is a better to go about it than what I came up with. Below was my thought process.

Problem

What piqued my interest here was that you actually cannot just assign the lyrics to a variable at the beginning of your code and have them update in your loop with string interpolation.

For example, this will end up printing 99 for each iteration of the loop, even though the numBottles variable is updated:

var numBottles: Int = 99
var firstLine: String =  "\(numBottles) bottles of milk on the wall"

while numBottles > 0 {
    print(firstLine)
    numBottles -= 1
}

The reason why is because if your lyric variables are assigned before your loop, that is when the string interpolation takes place. Your string variable won’t automatically update even though the value of numBottles is changing.

Solution 1

Of course, you can work around it by including your lyrics inside your loop like this:

**Click here for code**
var numBottles: Int = 99

while numBottles > 0 {
    let firstLine: String = "\(numBottles) bottles of milk on the wall, \(numBottles) bottles of milk! \nYou take one down and pass it around..."

    let secondLine: String = "\(numBottles - 1) bottles of milk on the wall!\n"

    print(firstLine)
    print(secondLine)
    numBottles -= 1
} 

followed by print() statements for the end lines.

Mapping out a better solution

But…that’s a little too simplistic. What if I want to reuse the loop (“sing the song”) multiple times? I’m going to want a few things out of my code:

  • My code should be more concise — I shouldn’t rewrite the lyrics multiple times.
  • Everything I need should be contained in my loop. I should have no outside print() statements.
  • I should avoid changing the global variable of numBottles so I don’t have to reset it each time.

Python Solution

In Python, I could easily accomplish my above goals, thanks to how string formatting works in Python. With the .format() method I can define my string variables with curly braces ({}) as placeholders outside my loop and then apply the correct number right inside of my print statements. Wrap it all up in functions and I’m good to go. It might look something like this:

**Click here for code**
num_bottles = 99

first_line = '{num} bottles of milk on the wall, {num} bottles of milk! \
     \nYou take one down and pass it around...'

second_line = '{num} bottles of milk on the wall!\n'

buy_drinks = '{num} bottles of milk on the wall, {num} bottles of milk! \
     \nGo to the store and buy some more,'

def drink():
    bottles = num_bottles
    while bottles > 0:
        print(first_line.format(num=bottles))
        bottles -= 1 
        print(second_line.format(num=bottles))

def top_off():
    print(buy_more.format(num=0))
    print(second_line.format(num=num_bottles))

drink()
top_off()

Could it really be that easy in Swift too? As it turns out, not quite. From what I could find — and correct me if I’m wrong — there is no Swift equivalent of Python’s .format() method that would allow me to format the strings later than where I assigned them to variables. So, I had to get a little more creative.

Final(?) Solution

Assuming that I want to keep my drink function — where I loop through the lyric printing and decrement my bottle count — separate from where I generate my lyrics, and since I cannot simply define them in String variables outside of my function, the best I could come up with was to add another function that made each verse for me, and to call that function in my main drink loop. See below for an example.

**Click here for code**
let numBottles: Int = 99   // notice `let` bars us from changing our global

func makeVerse(_ count: Int) {
  if count > 0 { 
    print("\(count) bottles of milk on the wall, \(count) bottles of milk! \nYou take one down and pass it around...")
    print("\(count - 1) bottles of milk on the wall!\n")
  } else {
    print("\(count) bottles of milk on the wall, \(count) bottles of milk! \nGo to the store and buy some more,")
    print("\(numBottles) bottles of milk on the wall!")
  }
}

func drink(bottles: Int = numBottles) {
  var count = bottles
  while count > 0 {
    makeVerse(count)
    count -= 1
  }
}

func topOff() {
  makeVerse(0)
}
drink()
topOff()

Although it’s not perfect, it achieves what I was trying to accomplish by waiting until the makeVerse function is called to generate the strings with the proper integer plugged in. It also keeps my drink and topOff functions clean and simple.

If anyone with more Swift experience has some ideas on how I could have improved this even more, please leave a comment and let me know. Otherwise, I hope some of you found this interesting or helpful for your own coding projects!