Vending Machine
Build the brain of a vending machine. What a button press means depends on the phase the machine is in: selecting with no money is a different thing from selecting with money, and a machine that is out of service answers everything with a refusal. This is the State pattern: put each phase's behaviour in one place, so that an illegal event is a defined answer rather than a bug waiting behind a pile of flags.
Implement the class VendingMachine. The constructor takes stock, an object from slot code to { "price": cents, "qty": count }, for example { "A1": { "price": 150, "qty": 2 } }.
Money is integer cents. The accepted coins are 5, 10, 25 and 100; anything else is not a coin.
States: "IDLE" (no credit, the start state), "HAS_MONEY" (credit above 0) and "OUT_OF_SERVICE".
Operations
insert(cents): returns"OK"or"REJECTED". A coin that is not accepted, or any coin while out of service, is"REJECTED"and the machine keeps nothing.select(slot): returns one of"DISPENSED","INSUFFICIENT","SOLD_OUT","INVALID_SLOT","NO_MONEY"or"OUT_OF_SERVICE". On"DISPENSED"the slot's quantity drops by 1, the price is taken from the credit, the rest of the credit is paid out as change, and the machine returns to"IDLE"with a balance of 0.cancel(): gives back all credit and returns the refunded cents (0 when there was none). The machine ends in"IDLE", unless it is out of service, where it stays.service(on):service(true)takes the machine out of service andservice(false)puts it back into"IDLE". Any credit is refunded first, and the call returns the refunded cents (0 when there was none, and always 0 forservice(false)).restock(slot, qty): addsqtyitems to an existing slot and returns the new quantity. It returns-1, changing nothing, for an unknown slot or aqtybelow 1.
Queries (they never change anything): state(), balance() (credit in cents), change() (the change paid out by the most recent "DISPENSED", 0 before the first sale and unaffected by any other call) and stock(slot) (items left, or -1 for an unknown slot).
State by event table. Every cell has exactly one outcome:
| Event | IDLE | HAS_MONEY | OUT_OF_SERVICE |
|---|---|---|---|
insert (accepted coin) | add to credit, "OK", go to HAS_MONEY | add to credit, "OK" | "REJECTED" |
insert (any other value) | "REJECTED" | "REJECTED" | "REJECTED" |
select | "NO_MONEY" (for any slot, valid or not) | see the order below | "OUT_OF_SERVICE" |
cancel | returns 0 | refund the credit, go to IDLE | returns 0 |
service(true) | returns 0, go to OUT_OF_SERVICE | refund the credit, go to OUT_OF_SERVICE | returns 0, stay |
service(false) | returns 0, stay | returns 0, stay (credit kept) | returns 0, go to IDLE |
restock | applies | applies | applies |
In HAS_MONEY, select checks in this order, and the first rule that applies wins:
- The slot does not exist:
"INVALID_SLOT", state and credit unchanged. - The slot has no items left:
"SOLD_OUT", state and credit unchanged. This wins over"INSUFFICIENT". - The credit is below the price:
"INSUFFICIENT", state and credit unchanged, so the customer can add more coins. - Otherwise
"DISPENSED", as described above.
Notes
- Money is never lost or created: for any sequence of calls, the cents inserted equal the prices paid plus the change and the refunds returned, plus the credit still held.
restockchanges only quantities, never the state, and works in every state.- A sold-out slot is a fact about that slot, not about the machine: other slots still sell.
- There are no exceptions: every call returns one of the values above.
Examples
Example 1
Input: ["VendingMachine","insert","state","balance","cancel","state","balance"]
[[{"A1":{"price":150,"qty":2}}],[100],[],[],[],[],[]]
Output: [null,"OK","HAS_MONEY",100,100,"IDLE",0]One accepted coin moves the machine from IDLE to HAS_MONEY. Cancel returns the 100 cents and goes back to IDLE.
Example 2
Input: ["VendingMachine","insert","select","insert","select","change","state","balance","stock"]
[[{"A1":{"price":150,"qty":2}}],[100],["A1"],[100],["A1"],[],[],[],["A1"]]
Output: [null,"OK","INSUFFICIENT","OK","DISPENSED",50,"IDLE",0,1]100 cents is not enough for a 150 item (INSUFFICIENT, still HAS_MONEY, credit kept). After a second coin the balance is 200, the item is dispensed, 50 comes back as change, the machine is IDLE with no balance and one item fewer.
Example 3
Input: ["VendingMachine","select","select","insert","insert","insert","state","cancel"]
[[{"A1":{"price":150,"qty":2}}],["A1"],["ZZ"],[3],[50],[0],[],[]]
Output: [null,"NO_MONEY","NO_MONEY","REJECTED","REJECTED","REJECTED","IDLE",0]Selecting while IDLE gives NO_MONEY, even for a slot that does not exist. A 3, 50 or 0 cent coin is REJECTED and nothing changes. Cancel with nothing inserted returns 0.
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 VendingMachine {
/** @param {Object<string, {price: number, qty: number}>} stock slot code → price in cents and quantity */
constructor(stock) {
}
/** Insert one coin (5, 10, 25 or 100 cents). @return {"OK"|"REJECTED"} */
insert(cents) {
}
/** Choose a slot. @return {"DISPENSED"|"INSUFFICIENT"|"SOLD_OUT"|"INVALID_SLOT"|"NO_MONEY"|"OUT_OF_SERVICE"} */
select(slot) {
}
/** Give back everything inserted and go to IDLE. @return {number} the refunded cents */
cancel() {
}
/** Switch OUT_OF_SERVICE on or off, refunding any credit first. @return {number} the refunded cents */
service(on) {
}
/** Add `qty` items to a slot in any state. @return {number} the new quantity, or -1 for an unknown slot or a qty below 1 */
restock(slot, qty) {
}
/** @return {"IDLE"|"HAS_MONEY"|"OUT_OF_SERVICE"} */
state() {
}
/** @return {number} cents inserted and not yet spent or refunded */
balance() {
}
/** @return {number} the change paid out by the most recent DISPENSED (0 before any purchase) */
change() {
}
/** @return {number} items left in a slot, or -1 for an unknown slot */
stock(slot) {
}
}
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.