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 - it is harder than the previous excercises, so you should do them first! 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:

A: Delete tests from the previous step, and paste in the pre-written tests for the current step. Check which ones fail.

B: Code until all tests pass.

C: 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.

0.Set up your file

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

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

# BELOW ARE THE TESTS FOR AUTOMATICALLY CHECKING YOUR SOLUTION.
# 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

1. $5 bills

Run your tests. If you use Sublime Text on Linux, OSX Mavericks (or better) or Windows, you can run the tests by pressing Ctrl-B. Otherwise you can type the follow into your terminal:

ruby atm.rb

You need to get all of these tests to pass.

Code

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).

2. How many bills?

Tests

Replace your existing tests with the following code. Run the tests to see which ones fail.

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

Code

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

3. List of bills

Tests

Replace your existing tests with the following code. Run the tests to see which ones fail.

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

Code

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

Rather than returning the number of notes, 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. [1, 2] << 3 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]

4. $10 notes

Tests

Replace your existing tests with the following code. Run the tests to see which ones fail.

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

Code

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?

5. $5 and $10 notes

Tests

Replace your existing tests with the following code. Run the tests to see which ones fail.

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

Code

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

Examples

Tips for getting tests green:

The floor method rounds a number down to the nearest whole integer. For example:

my_number = 3.87.floor # my_number contains 3

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

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

Tests

Replace your existing tests with the following code. Run the tests to see which ones fail.

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

Code

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

Note: that 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]

If your function calls a sub-function, keep in mind that Ruby has no concept of ‘pass by value’. Variables are always a reference to an object.

Refactor

7. Final Discussion Points