Re: Factor

Factor: the language, the theory, and the practice.

Cash Register

Thursday, August 8, 2024

#command-line

Building a “cash register” is an often used example project, from places like the freeCodeCamp’s Javascript Algorithms and Data Structures Project “Cash Register” or codecademy’s “Building a Cash Register” along with other examples like the Simple Cash Register in Python.

I thought it would be fun to write about building something similar, but not the same, in Factor.

We are going to make a few assumptions:

  1. We handle one currency – the “buck”.
  2. We can make change in various units – from the penny to the Benjamin.
  3. Despite still being legal tender, we do not support $500, $1,000, $5,000, or $10,000 bills.
  4. Despite being rare, we include the two-dollar bill.

Here are our units of change along with their descriptions:

CONSTANT: COINS {
    { 10000 "$100" }
    { 5000 "$50" }
    { 2000 "$20" }
    { 1000 "$10" }
    { 500 "$5" }
    { 200 "$2" }
    { 100 "$1" }
    { 25 "quarters" }
    { 10 "dimes" }
    { 5 "nickels" }
    { 1 "pennies" }
}

If we want to make change, we can generate it using something like the greedy algorithm to find minimum number of coins, starting with the largest denomination possible and iterating to smaller ones:

: make-change ( n -- assoc )
    COINS [ [ /mod swap ] dip ] assoc-map swap 0 assert= ;

For convenience, we can make a formatting word to format our coins into dollars:

: $. ( n -- )
    100 /f "$%.2f\n" printf ;

And now a word to print out the change we made:

: change. ( n -- )
    "CHANGE: " write dup $. make-change [
        '[ _ "%d of %s\n" printf ] unless-zero
    ] assoc-each ;

We can store the amount owed and the amount paid in dynamic variables:

INITIALIZED-SYMBOL: owed [ 0 ]

INITIALIZED-SYMBOL: paid [ 0 ]

Using that, we can make a word to display the balance due:

: balance. ( -- )
    "OWED: " write owed get-global $.
    "PAID: " write paid get-global $. ;

A word to add a charge, increasing the amount owed:

: charge ( n -- )
    "CHARGE: " write dup $.
    owed [ + ] change-global balance. ;

A word to make a payment, providing change if the amount paid is greater than the amount owed:

: pay ( n -- )
    "PAY: " write dup $.
    paid [ + ] change-global balance.
    paid get-global owed get-global - dup 0 >=
    [ change. 0 owed set-global 0 paid set-global ] [ drop ] if ;

And a word to cancel a transaction, refunding any paid amounts:

: cancel ( -- )
    "CANCEL" print
    0 owed set-global
    paid [ change. 0 ] change-global ;

Using a word that parses input into a number of pennies:

: parse-$ ( args -- n )
    "$" ?head drop string>number 100 * round >integer ;

We can then define a set of commands using the command-loop vocabulary:

CONSTANT: COMMANDS {
    T{ command
        { name "balance" }
        { quot [ drop balance. ] }
        { help "Display current balance." }
        { abbrevs { "b" } } }
    T{ command
        { name "charge" }
        { quot [ parse-$ charge ] }
        { help "Charge an item." }
        { abbrevs { "c" } } }
    T{ command
        { name "pay" }
        { quot [ parse-$ pay ] }
        { help "Pay with money." }
        { abbrevs { "p" } } }
    T{ command
        { name "cancel" }
        { quot [ drop cancel ] }
        { help "Cancel transaction." }
        { abbrevs { "x" } } }
}

And then define the loop that we run as MAIN:

: cash-register-main ( -- )
    "Welcome to the Cash Register!" "$>"
    command-loop new-command-loop
    COMMANDS [ over add-command ] each
    run-command-loop ;

MAIN: cash-register-main

And you can see an example from running it:

Welcome to the Cash Register!
$> c 10.23
CHARGE: $10.23
OWED: $10.23
PAID: $0.00

$> c 15.37
CHARGE: $15.37
OWED: $25.60
PAID: $0.00

$> p 100.00
PAY: $100.00
OWED: $25.60
PAID: $100.00
CHANGE: $74.40
1 of $50
1 of $20
2 of $2
1 of quarters
1 of dimes
1 of nickels

It could be fun to extend this example to have an inventory of purchasable items, allow users to ring up these items instead of a series of charges, maybe implement taxable items and discounts, display and print receipts, handle refunds, handle available bills and coins when making change, support other currencies, and other features that you might find in a more “complete” or “real-world” cash register.

The code for this is on my GitHub.