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:
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.
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.
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:
withdraw(15)
should returntrue
withdraw(18)
should returnfalse
, because $18 cannot be made up of $5 notes
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?
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:
withdraw(15)
should return3
, since three $5 notes makes $15withdraw(20)
should return4
, since four $5 notes makes $20withdraw(11)
should returnfalse
, because $11 cannot be made up of $5 notes
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:
In ruby, when you do division with two whole numbers (integers), the result is rounded down to the nearest whole number.
Tests for step 2:
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, Code so that it returns an array of notes (in this case, all $5’s).
Examples
withdraw(15)
should return an array,[5, 5, 5]
. (That’s basically a collection of three $5 notes)withdraw(11)
should returnfalse
, because $11 cannot be made up of $5 notes
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:
Tests for step 3:
4. $10 notes
Now imagine the ATM returns only $10 notes. Modify your function to accommodate this.
Examples
withdraw(20)
should return an array,[10, 10]
withdraw(15)
should returnfalse
, because $15 cannot be made up of $10 notes
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:
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:
5. $5 and $10 notes
Imagine your ATM now holds $5 and $10. People want as few notes as possible.
Examples
withdraw(25)
should return[10, 10, 5]
withdraw(11)
should returnfalse
, because $11 cannot be made up of $5 and $10 notes
Tests for step 5
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
withdraw(15)
should return[10, 5]
withdraw(25)
should return[20, 5]
withdraw(35)
should return[20, 10, 5]
withdraw(11)
should returnfalse
, because $11 cannot be made up of $5 and $10 notes
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
- How many lines did you have to change, going from step 5 to step 6?
- What if we made $100 notes available, as well? Could you do this in a single line?
- Refactor your code so that you could change to $100, $20 and $10 notes, by changing a single line.
- What is the most future-proof solution?
Tests for step 6:
7. Final Discussion Points
- Given a particular solution works, what makes it ‘good’ or ‘bad’ code?
- Can you think of any take-home best practices or principals? Is code structure important? Why?
- Did you have any ‘Aha!’ moments? What were they?
- Lets say you started a little ATM company, which quickly expanded to become a global success. How suitable is your code for dealing with all denominations of currency, in all nations of the world? Did it get more suitable with each step?
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 need 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).