ATM
Build the brain of an ATM. A customer inserts a card, types a PIN, and then checks a balance or withdraws cash. What a button does depends on where in that story the customer is, which is the State pattern, and the cash is paid out from limited stacks of notes, which is a job for a chain of handlers or a small loop that can backtrack. The tests drive one machine from the outside only, so design the classes however keeps these two ideas separate.
Implement the class Atm:
Atm(accounts, stock):accountsis a list of[id, pin, balance](a card is identified by its accountid;pinis a string;balanceis whole dollars).stockis the notes in the machine as[count100, count50, count20, count10]. The machine starts in state"IDLE", with no card.insertCard(id): allowed only inIDLE. Returns"OK"and moves toCARD_IN; or"UNKNOWN_CARD"if no such account; or"LOCKED"if that account is locked. In the last two cases the machine staysIDLE. In any other state it returns"INVALID_STATE".enterPin(pin): allowed only inCARD_IN. A right PIN returns"OK", moves toAUTHENTICATEDand resets the account's failed-PIN count to 0. A wrong PIN returns"WRONG_PIN"and stays inCARD_IN, unless it is the account's third wrong PIN in a row: then the account becomes locked for good, the machine keeps the card, goes back toIDLE, and returns"LOCKED". The failed-PIN count belongs to the account, so it survivesejectand a new session. Any other state returns"INVALID_STATE".balance(): allowed only inAUTHENTICATED. Returns the balance, or-1in any other state.withdraw(amount): allowed only inAUTHENTICATED. On success returns the notes paid out as a list of[note, count]pairs, largest note first, leaving out notes with a count of 0, and staysAUTHENTICATED. On failure it returns one of the strings below and changes nothing. Checks run in this order and the first that fails decides the answer:"INVALID_STATE"if notAUTHENTICATED."INVALID_AMOUNT"unlessamountis a positive multiple of 10."NO_EXACT_NOTES"if the notes in the machine cannot add up to exactlyamount."INSUFFICIENT_FUNDS"if the balance is lower thanamount."JAM"if the cash unit is jammed (seejam). The balance and the stock are left as they were.
eject(): allowed inCARD_INandAUTHENTICATED. Returns"OK"and goes toIDLE. InIDLEit returns"INVALID_STATE".state(): returns"IDLE","CARD_IN"or"AUTHENTICATED".cash(): the notes left in the machine as[[100, c], [50, c], [20, c], [10, c]], always all four.jam(flag):truemakes the cash unit fail on every dispense untiljam(false). Returnstrue.
Choosing the notes. The note values are 100, 50, 20 and 10. When several combinations make the amount, use the one with the most 100s, then among those the most 50s, then the most 20s, then the 10s. Be careful: taking the biggest notes first can leave a remainder you cannot pay even though another combination works. With one 50 and three 20s, an amount of 60 must be paid as three 20s.
Notes
- Wrong or locked cards, wrong states and invalid amounts are ordinary return values. Nothing in this class throws.
- Locking is permanent for the length of the test: there is no way to unlock.
- A new machine knows nothing about earlier tests; each test builds its own.
Examples
Example 1
Input: ["Atm","insertCard","enterPin","balance","withdraw","balance","eject","state"]
[[[["ann","1234",500]],[5,5,5,5]],["ann"],["1234"],[],[130],[],[],[]]
Output: [null,"OK","OK",500,[[100,1],[20,1],[10,1]],370,"OK","IDLE"]130 is paid as one 100, one 20 and one 10, the largest notes first. The balance drops from 500 to 370.
Example 2
Input: ["Atm","balance","withdraw","enterPin","eject","insertCard","state"]
[[[["ann","1234",500]],[5,5,5,5]],[],[10],["1234"],[],["nobody"],[]]
Output: [null,-1,"INVALID_STATE","INVALID_STATE","INVALID_STATE","UNKNOWN_CARD","IDLE"]In IDLE only insertCard is allowed. Everything else returns its sentinel (-1 for balance), and an unknown card leaves the machine IDLE.
Example 3
Input: ["Atm","insertCard","enterPin","state","enterPin","state","insertCard"]
[[[["ann","1234",500]],[5,5,5,5]],["ann"],["0000"],[],["1234"],[],["ann"]]
Output: [null,"OK","WRONG_PIN","CARD_IN","OK","AUTHENTICATED","INVALID_STATE"]A wrong PIN keeps the machine in CARD_IN. The right one authenticates. A second insertCard is not allowed during a session.
Each test creates the class with the first operation, then calls the methods in order. Output lists what each call returns; null marks the constructor and methods that return nothing, and their return values aren't checked.
class Atm {
/** @param {Array<[string, string, number]>} accounts [id, pin, balance] @param {number[]} stock [n100, n50, n20, n10] */
constructor(accounts, stock) {
}
/** @return {string} "OK" | "UNKNOWN_CARD" | "LOCKED" | "INVALID_STATE" */
insertCard(id) {
}
/** @return {string} "OK" | "WRONG_PIN" | "LOCKED" | "INVALID_STATE" */
enterPin(pin) {
}
/** @return {number} the balance, or -1 unless authenticated */
balance() {
}
/** @return {Array<[number, number]>|string} [note, count] pairs largest first, or "INVALID_STATE" | "INVALID_AMOUNT" | "NO_EXACT_NOTES" | "INSUFFICIENT_FUNDS" | "JAM" */
withdraw(amount) {
}
/** @return {string} "OK" or "INVALID_STATE" */
eject() {
}
/** @return {string} "IDLE" | "CARD_IN" | "AUTHENTICATED" */
state() {
}
/** @return {Array<[number, number]>} [[100, c], [50, c], [20, c], [10, c]] */
cash() {
}
/** Make the cash unit fail (true) or work (false). @return {boolean} true */
jam(flag) {
}
}
Ctrl/⌘ + ' runs the examples, Ctrl/⌘ + Enter submits. Use console.log freely: output shows under Console. In the editor, press Esc then Tab to move focus out.