Ruby ATM

Created by Joshua Paling, @joshuapaling

In this exercise you will write a function to handle withdrawing money from an ATM. It is intended to challenge you! Be prepared to ask lots of questions, Google things, and take a step back from the computer. But you’ll learn a lot!

You should use pair, group, or remote programming so you have other programmers to bounce ideas off. You’ll use Test Driven Development. However, all the tests have been pre-written for you, so you can focus on the code itself.

We’ll start simple, but ramp the difficulty up pretty steeply toward the end!

Workflow

For each step, you will need to take the following actions:

1. Run the new tests: Delete tests from the previous step, and paste in the pre-written tests for the current step. (Tests for each step are show at the bottom of that step.) Run the tests to see which ones fail. In sublime text, you can hit Ctrl+B to run the tests. Or, you can open a terminal, cd into your working directory, and run ruby atm.rb.

2. Make the tests pass: Edit your code to meet the functionality requirements of the current step. You’ll know you’ve got it right when all your tests are green.

3. Refactor: See if there are any edits you can make to ensure your code is as clean and easy to understand as possible. Some steps have discussion points. Discuss them with your pair - they’ll help you moving forward.

You may recognise these steps as the red, green, refactor workflow of TDD.

1. $5 bills

Imagine an ATM that holds only $5 notes. Write a function that returns true if an amount can be returned, and false otherwise.

Examples:

Tips for getting tests green:

The modulus operator, % gets the remainder of a division. Eg, 9 % 4 results in 1 (nine divided by four has a remainder of 1).

Starting code:

Create a file called atm.rb. paste the following code into it. This contains the shell of your withdraw() function, along with tests.

def withdraw(amount)
  if amount <= 0 # this deals with some of the situations...
    return false
  end
  # ToDo: figure out this bit
end

Tests for step 1:

# import required testing libraries
require 'minitest/spec'
require 'minitest/autorun'

# BELOW ARE THE TESTS FOR AUTOMATICALLY CHECKING YOUR SOLUTION.
# THESE TESTS ARE FOR STEP 1.
# THESE NEED TO BE REPLACED AFTER EACH STEP.
describe 'atm' do
  [
    [-1, false],
    [0, false],
    [1, false],
    [43, false],
    [17, false],
    [5, true],
    [20, true],
    [35, true],
  ].each do |input, expected|
    it "should return #{expected} when $#{input} is withdrawn" do
      withdraw(input).must_equal expected
    end
  end
end

2. How many bills?

Now, modify your function so that if the amount can be withdrawn, it will return the appropriate number of notes, rather than simply true

Examples:

Tips for getting tests green:

The / operator performs a division. For example, if you wanted to get half your age, and store it in a variable, you’d do this:

my_age = 28
half_my_age = my_age / 2

In ruby, when you do division with two whole numbers (integers), the result is rounded down to the nearest whole number.

new_number = 25/10
# new_number contains 2, because 25/10 = 2.5, and ruby will round that down to 2.

Tests for step 2:

# Replace your existing tests with the ones below.
describe 'atm' do
  [
    [-1, false],
    [0, false],
    [1, false],
    [43, false],
    [7, false],
    [5, 1],
    [20, 4],
    [35, 7],
  ].each do |input, expected|
    it "should return #{expected} when $#{input} is withdrawn" do
      withdraw(input).must_equal expected
    end
  end
end

3. Array of notes

In programming, an array is basically a collection of things. It’s like a list.

Rather than returning the number of notes, modify the code so that it returns an array of notes (in this case, all $5’s).

Examples

Tips for getting tests green:

[] defines an empty array. [1, 2] defines an array with two elements (1 and 2).

The shovel operator (<<) adds an element to an array. Eg. [10, 20] << 30 will add 30 to the array, resulting in [10, 20, 30].

The times method executes a block of code several times - eg. 5.times { puts 'hello' } will print ‘hello’ 5 times.

Bringing it all together:

my_array = [] # create an empty array and store it in my_array
my_array << 20 # now my array contains [20]
my_array << 30 # now my_array contains [20, 30]
remainder = 13 % 5 # remainder is 3
remainder.times { my_array << 5 } # now my_array contains [20, 30, 5, 5, 5]

Tests for step 3:

# Replace your existing tests with the ones below.
describe 'atm' do
  [
    [-1, false],
    [0, false],
    [1, false],
    [43, false],
    [20, [5, 5, 5, 5]],
    [35, [5, 5, 5, 5, 5, 5, 5]],
  ].each do |input, expected|
    it "should return #{expected} when $#{input} is withdrawn" do
      withdraw(input).must_equal expected
    end
  end
end

4. $10 notes

Now imagine the ATM returns only $10 notes. Modify your function to accommodate this.

Examples

Refactor

Once you have your tests passing, it’s time to refactor.

In programming, ‘Magic Numbers’ are a bad thing (don’t be fooled by the name!). They refer to a hard-coded value that just sort of appears out of thin air, with no clear explanation of what that particular number represents.

Consider how easy / hard it is to understand the following code snippets:

# BAD - magic number!
balance = balance * 4.45
# BAD - nondescript variable name is not much better!
value = 4.45
balance = balance * value
# GOOD - isn't this much easier to understand?
interest_rate = 4.45
balance = balance * interest_rate

Magic numbers are particularly troublesome when the same hard-coded value appears in multiple places.

Did you use magic numbers? Can you refactor your code to eliminate them?

Tests for step 4:

# Replace your existing tests with the ones below.
describe 'atm' do
  [
    [-1, false],
    [0, false],
    [7, false],
    [45, false],
    [20, [10, 10]],
    [40, [10, 10, 10, 10]],
  ].each do |input, expected|
    it "should return #{expected} when $#{input} is withdrawn" do
      withdraw(input).must_equal expected
    end
  end
end

5. $5 and $10 notes

Imagine your ATM now holds $5 and $10. People want as few notes as possible.

Examples

Tests for step 5

# Replace your existing tests with the ones below.
describe 'atm' do
  [
    [-1, false],
    [0, false],
    [7, false],
    [20, [10, 10]],
    [25, [10, 10, 5]],
    [35, [10, 10, 10, 5]],
  ].each do |input, expected|
    it "should return #{expected} when $#{input} is withdrawn" do
      withdraw(input).must_equal expected
    end
  end
end

6. $5, $10, and $20 notes

Your ATM now holds $5, $10, and $20 notes. Modify your function to accommodate this.

Note: at this point, each higher denomination can be evenly divided by each lower denomination - eg. $20 / $10 = 2. Things get much trickier when that’s not the case (eg, $50’s and $20’s). For this step, we’ll intentionally not deal with this case to make it easier.

Examples

Tips for getting tests green:

To tell if an array is empty: my_array.empty?

To tell if an array is not empty: !my_array.empty?

To remove the first element off an array: my_array.shift. Eg, [1, 2, 3].shift results in [2, 3]

Variables are always a reference to an object.

Refactor

Tests for step 6:

# Replace your existing tests with the ones below.
describe 'atm' do
  [
    [-1, false],
    [0, false],
    [7, false],
    [53, false],
    [35, [20, 10, 5]],
    [40, [20, 20]],
    [65, [20, 20, 20, 5]],
    [70, [20, 20, 20, 10]],
    [75, [20, 20, 20, 10, 5]],
  ].each do |input, expected|
    it "should return #{expected} when $#{input} is withdrawn" do
      withdraw(input).must_equal expected
    end
  end
end

7. Final Discussion Points

Challenge! $50 and $20 notes

Up til now, we’ve intentionally avoided the case where a smaller note cannot fit evenly into each larger one. For example, we’ve avoided the case of having only $50s and $20s (where 20 does not divide evenly into 50). Can you see why this case will be harder? If your current code were to include only $50s and $20s, what would happen when you try to withdraw $60, or $110? In your head, or on paper, can you think of what logic would be needed to be in place to handle these cases correctly? If you’re up for a challenge, try to handle this case in your code! (You’ll need to write the tests yourself for this step).