← All examples

A day at a corner shop

A longer Zag program, written to be read.

corner_shop.zg
# A day at a corner shop: a longer Zag program, written to be read.
#
# It keeps a small catalog, takes orders written as text, prices them in exact decimals (`deci64`,
# so 0.10 plus 0.20 is 0.30), applies discounts that a range type keeps between 0 and 50 percent,
# prints receipts, tracks stock and loyalty points in maps, and ends the day with a report, a
# restocking plan, the day's busy hours, the week's trade and a catalog search. Along the way it
# shows most of Zag's everyday features:
#
#   structs and enums with data, `match`         ownership said aloud: `give`, `copy`, `lend`
#   failures a caller must handle, `otherwise`    generics with an interface, and `Number`
#   functions as values                          a type's own function as an iterator (`Ticket.next`)
#   range types, `int32[min 0, max 50]`          counting loops, `for 'i' from 1 to 'n' by 1`
#   slices, `from pos 1, to pos 3`               consts, at the top and inside a body
#   tasks and channels, `concur`                 `print.useful`, `print.stored`, `print.format`
#
# Nothing here is clever on purpose. A shop's code is read far more often than it is written.

# ---------------------------------------------------------------------------------------------------
# The shop's fixed facts
# ---------------------------------------------------------------------------------------------------

const 'shop_name' := "Corner Shop";
const 'points_per_item' := 2;           # loyalty points earned for every item bought
const 'low_stock' := 3;                 # at or below this, an item goes on the reorder list

# ---------------------------------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------------------------------

enum Aisle {
    fresh,
    bakery,
    pantry,
    household
}

struct Item {
    name: str.utf8,
    aisle: Aisle,
    price: deci64
}

# One line of an order: what, and how many.
struct Line {
    name: str.utf8,
    count: int32[min 1, max 99]
}

# How a customer pays. A card carries its last four digits, a voucher its value.
enum Payment {
    cash,
    card: str.utf8,
    voucher: deci64
}

struct Order {
    customer: str.utf8,
    lines: list[Line],
    payment: Payment,
    discount: int32[min 0, max 50]
}

# What one order came to, kept for the end-of-day report.
struct Sale {
    ticket: int32,
    customer: str.utf8,
    units: int32,
    total: deci64
}

# Receipt numbers come from a counter that hands out the next one each time it is asked.
struct Ticket {
    now: int32,
    last: int32
}

fn Ticket.next [lent.mut 't': Ticket] -> optional[int32] {
    if 't'.now equal_to 't'.last { return optional.none; }
    change 't'.now = 't'.now plus 1;
    return optional.value['t'.now];
}

# ---------------------------------------------------------------------------------------------------
# Small generic helpers
# ---------------------------------------------------------------------------------------------------

interface Number {
    int32
    int64
    bin64
}

# The larger of two numbers, for any kind of number.
fn larger <T: Number> ['a': <T>, 'b': <T>] -> <T> {
    if 'a' > 'b' { return 'a'; }
    return 'b';
}

# The total of a list of numbers.
fn total_of <T: Number> [lent 'xs': list[<T>]] -> <T> {
    let 'sum': <T> = 0;
    for 'x' in lend 'xs' {
        change 'sum' = 'sum' plus 'x';
    }
    return 'sum';
}

# The list sorted from largest to smallest: a plain insertion sort, easy to check by eye.
fn sorted_down [lent 'xs': list[int32]] -> list[int32] {
    let 'out': list[int32] = list.empty;
    for 'x' in lend 'xs' {
        list.append[lend.mut 'out', value 'x'];
        let 'i' := list.length[lend 'out'];
        while 'i' > 1 and.then 'out'[pos 'i'] > 'out'[pos 'i' minus 1] {
            let 'held' := 'out'[pos 'i'];
            change 'out'[pos 'i'] = 'out'[pos 'i' minus 1];
            change 'out'[pos 'i' minus 1] = 'held';
            change 'i' = 'i' minus 1;
        }
    }
    return give 'out';
}

# ---------------------------------------------------------------------------------------------------
# The catalog and the stock room
# ---------------------------------------------------------------------------------------------------

fn catalog [] -> list[Item] {
    return [
        Item[name = "apple", aisle = Aisle.fresh, price = 0.45],
        Item[name = "banana", aisle = Aisle.fresh, price = 0.30],
        Item[name = "milk", aisle = Aisle.fresh, price = 1.20],
        Item[name = "bread", aisle = Aisle.bakery, price = 2.10],
        Item[name = "croissant", aisle = Aisle.bakery, price = 1.35],
        Item[name = "rice", aisle = Aisle.pantry, price = 3.80],
        Item[name = "beans", aisle = Aisle.pantry, price = 0.95],
        Item[name = "coffee", aisle = Aisle.pantry, price = 6.50],
        Item[name = "soap", aisle = Aisle.household, price = 1.75],
        Item[name = "candles", aisle = Aisle.household, price = 4.20]
    ];
}

fn opening_stock [] -> map[str.utf8, int32] {
    let 'stock': map[str.utf8, int32] = map.empty;
    map.set[lend.mut 'stock', key "apple", value 40];
    map.set[lend.mut 'stock', key "banana", value 30];
    map.set[lend.mut 'stock', key "milk", value 12];
    map.set[lend.mut 'stock', key "bread", value 8];
    map.set[lend.mut 'stock', key "croissant", value 6];
    map.set[lend.mut 'stock', key "rice", value 10];
    map.set[lend.mut 'stock', key "beans", value 24];
    map.set[lend.mut 'stock', key "coffee", value 5];
    map.set[lend.mut 'stock', key "soap", value 9];
    map.set[lend.mut 'stock', key "candles", value 4];
    return give 'stock';
}

# The catalog entry for a name, or a failure the caller must handle.
fn find_item [lent 'items': list[Item], lent 'name': str.utf8] -> Item or fail[unknown_item] {
    for 'it' in lend 'items' {
        if 'it'.name equal_to 'name' { return copy 'it'; }
    }
    fail unknown_item;
}

fn aisle_name ['a': Aisle] -> str.utf8 {
    match 'a' {
        fresh     { return "fresh"; }
        bakery    { return "bakery"; }
        pantry    { return "pantry"; }
        household { return "household"; }
    }
}

# ---------------------------------------------------------------------------------------------------
# Reading orders written as text: "apple 3, bread 1, coffee 2"
# ---------------------------------------------------------------------------------------------------

# One "name count" piece into a line; a count that is not a number, or not 1 to 99, fails.
fn parse_line [lent 'piece': str.utf8] -> Line or fail[bad_line: str.utf8] {
    let 'words' := std::str.split[lend 'piece', at " "];
    if list.length[lend 'words'] not_equal 2 { fail bad_line[copy 'piece']; }
    let 'name' := copy 'words'[pos 1];
    let 'count': int32[min 1, max 99] = to.int32[min 1, max 99]['words'[pos 2]] otherwise {
        not_a_number { fail bad_line[copy 'piece']; }
        out_of_range { fail bad_line[copy 'piece']; }
    };
    return Line[name = give 'name', count = 'count'];
}

fn parse_order [lent 'text': str.utf8] -> list[Line] or fail[bad_line: str.utf8] {
    let 'lines': list[Line] = list.empty;
    let 'pieces' := std::str.split[lend 'text', at ", "];
    for 'piece' in lend 'pieces' {
        let 'line' := parse_line[lend 'piece'] otherwise fail;
        list.append[lend.mut 'lines', value give 'line'];
    }
    return give 'lines';
}

# ---------------------------------------------------------------------------------------------------
# Pricing
# ---------------------------------------------------------------------------------------------------

# A pricing rule takes a line's price before any rule and gives what the customer pays for it.
fn no_rule ['line_total': deci64] -> deci64 { return 'line_total'; }

# Bakery goods are 20% off after six in the evening; the shop runs this rule all day here.
fn evening_bakery ['line_total': deci64] -> deci64 { return 'line_total' times 0.80; }

fn rule_for ['a': Aisle] -> fn[deci64] -> deci64 {
    match 'a' {
        bakery { return evening_bakery; }
        fresh { return no_rule; }
        pantry { return no_rule; }
        household { return no_rule; }
    }
}

# A whole-order discount, which the range type keeps between 0 and 50 percent.
fn apply_discount ['amount': deci64, 'percent': int32[min 0, max 50]] -> deci64 {
    let 'pct': deci64 = to.deci64['percent'];
    let 'off': deci64 = ('amount' times 'pct') div.near 100;
    return 'amount' minus 'off';
}

# ---------------------------------------------------------------------------------------------------
# Taking one order
# ---------------------------------------------------------------------------------------------------

fn take_order [
    lent 'items': list[Item],
    lent.mut 'stock': map[str.utf8, int32],
    lent 'order': Order,
    'ticket': int32
] -> Sale or fail[out_of_stock: str.utf8, unknown_item: str.utf8] {
    std::print.useful["--- receipt #", 'ticket', " for ", 'order'.customer, " ---", newln];
    let 'subtotal': deci64 = 0;
    let 'units' := 0;
    for 'line' in lend 'order'.lines {
        let 'item' := find_item[lend 'items', lend 'line'.name] otherwise {
            unknown_item { fail unknown_item[copy 'line'.name]; }
        };
        let 'have' := map.get[lend 'stock', key copy 'line'.name] otherwise 0;
        if 'have' < 'line'.count { fail out_of_stock[copy 'line'.name]; }
        map.set[lend.mut 'stock', key copy 'line'.name, value 'have' minus 'line'.count];
        let 'rule' := rule_for[copy 'item'.aisle];
        let 'count': deci64 = to.deci64['line'.count];
        let 'line_total' := 'rule'['item'.price times 'count'];
        change 'subtotal' = 'subtotal' plus 'line_total';
        change 'units' = 'units' plus 'line'.count;
        std::print.format["  {:<10} x{:>2}  {:>8.2}\n", 'line'.name, 'line'.count, 'line_total'];
    }
    let 'total' := apply_discount['subtotal', 'order'.discount];
    if 'order'.discount > 0 {
        std::print.format["  discount {}%\n", 'order'.discount];
    }
    std::print.format["  {:<14} {:>8.2}\n", "total", 'total'];
    std::print.useful["  paid by ", payment_text[lend 'order'.payment, 'total'], newln];
    return Sale[ticket = 'ticket', customer = copy 'order'.customer, units = 'units', total = 'total'];
}

fn payment_text [lent 'p': Payment, 'total': deci64] -> str.utf8 {
    match 'p' {
        cash { return "cash"; }
        card 'last4' { return "card ending " ++ 'last4'; }
        voucher 'worth' {
            if 'worth' at_least 'total' { return "voucher, nothing left to pay"; }
            return std::format["voucher, then {:.2} in cash", 'total' minus 'worth'];
        }
    }
}

# ---------------------------------------------------------------------------------------------------
# The day's orders
# ---------------------------------------------------------------------------------------------------

fn todays_orders [] -> list[Order] {
    let 'orders': list[Order] = list.empty;
    let 'texts' := [
        "apple 6, bread 1, milk 2",
        "coffee 2, croissant 4",
        "rice 1, beans 3, soap 2, candles 1",
        "banana 12, milk 1",
        "coffee 4",
        "pineapple 1",
        "apple two",
        "candles 2, bread 2, croissant 2"
    ];
    let 'customers' := ["Ann", "Bo", "Cy", "Dee", "Eli", "Fay", "Gus", "Hal"];
    for 'i' from 1 to list.length[lend 'texts'] by 1 {
        let 'lines' := parse_order[lend 'texts'[pos 'i']] otherwise {
            bad_line 'what' {
                std::print.useful["(order for ", 'customers'[pos 'i'], " skipped: cannot read \"", 'what', "\")", newln];
                continue;
            }
        };
        let 'payment' := choose_payment['i'];
        let 'discount': int32[min 0, max 50] = 0;
        if 'i' equal_to 3 { change 'discount' = 10; }
        if 'i' equal_to 8 { change 'discount' = 25; }
        list.append[lend.mut 'orders', value Order[customer = copy 'customers'[pos 'i'], lines = give 'lines', payment = give 'payment', discount = 'discount']];
    }
    return give 'orders';
}

fn choose_payment ['i': int32] -> Payment {
    if ('i' mod.floor 3) equal_to 0 { return Payment.voucher[5.00]; }
    if ('i' mod.floor 2) equal_to 0 { return Payment.card["4242"]; }
    return Payment.cash;
}

# ---------------------------------------------------------------------------------------------------
# Loyalty points
# ---------------------------------------------------------------------------------------------------

fn add_points [lent.mut 'points': map[str.utf8, int32], lent 'sale': Sale] {
    let 'earned' := 'sale'.units times 'points_per_item';
    let 'before' := map.get[lend 'points', key copy 'sale'.customer] otherwise 0;
    map.set[lend.mut 'points', key copy 'sale'.customer, value 'before' plus 'earned'];
}

# ---------------------------------------------------------------------------------------------------
# The end-of-day report
# ---------------------------------------------------------------------------------------------------

fn reorder_list [lent 'items': list[Item], lent 'stock': map[str.utf8, int32]] -> list[str.utf8] {
    let 'out': list[str.utf8] = list.empty;
    for 'it' in lend 'items' {
        let 'left' := map.get[lend 'stock', key copy 'it'.name] otherwise 0;
        if 'left' at_most 'low_stock' {
            list.append[lend.mut 'out', value copy 'it'.name ++ " (" ++ to.stored.str.utf8['left'] ++ " left)"];
        }
    }
    return give 'out';
}

fn takings_by_aisle [lent 'items': list[Item], lent 'opening': map[str.utf8, int32], lent 'closing': map[str.utf8, int32], 'a': Aisle] -> deci64 {
    let 'sum': deci64 = 0;
    for 'it' in lend 'items' {
        if 'it'.aisle equal_to 'a' {
            let 'had' := map.get[lend 'opening', key copy 'it'.name] otherwise 0;
            let 'has' := map.get[lend 'closing', key copy 'it'.name] otherwise 0;
            let 'sold' := 'had' minus 'has';
            change 'sum' = 'sum' plus 'it'.price times to.deci64['sold'];
        }
    }
    return 'sum';
}

fn report [lent 'sales': list[Sale], lent 'items': list[Item], lent 'opening': map[str.utf8, int32], lent 'stock': map[str.utf8, int32], lent 'points': map[str.utf8, int32]] {
    std::print.useful[newln, "=== ", 'shop_name', ", end of day ===", newln];

    # Orders and units
    let 'units': list[int32] = list.empty;
    let 'takings': deci64 = 0;
    for 's' in lend 'sales' {
        list.append[lend.mut 'units', value 's'.units];
        change 'takings' = 'takings' plus 's'.total;
    }
    std::print.useful["orders served: ", list.length[lend 'sales'], ", units sold: ", total_of[lend 'units'], newln];
    std::print.format["takings: {:.2}\n", 'takings'];

    # The biggest baskets, by units: the first three of the list sorted largest first
    let 'ranked' := sorted_down[lend 'units'];
    let 'shown' := larger[1, list.length[lend 'ranked']];
    if 'shown' at_least 3 {
        let 'top' := list.slice[lend 'ranked', from pos 1, to pos 3];
        std::print.useful["biggest baskets (units): ", 'top', newln];
    }

    # The best sale of the day
    let 'best_total': deci64 = 0;
    let 'best_ticket' := 0;
    let 'best_customer' := "nobody";
    for 's' in lend 'sales' {
        if 's'.total > 'best_total' {
            change 'best_total' = 's'.total;
            change 'best_ticket' = 's'.ticket;
            change 'best_customer' = copy 's'.customer;
        }
    }
    std::print.format["best sale: #{}, {}, {:.2}\n", 'best_ticket', 'best_customer', 'best_total'];

    # Takings by aisle, counted from what left the shelves (at list price, before discounts)
    std::print.useful["by aisle, at list price:", newln];
    let 'aisles' := [Aisle.fresh, Aisle.bakery, Aisle.pantry, Aisle.household];
    for 'a' in lend 'aisles' {
        std::print.format["  {:<10} {:>8.2}\n", aisle_name[copy 'a'], takings_by_aisle[lend 'items', lend 'opening', lend 'stock', copy 'a']];
    }

    # Reorders
    let 'reorder' := reorder_list[lend 'items', lend 'stock'];
    if list.length[lend 'reorder'] equal_to 0 {
        std::print.useful["nothing to reorder", newln];
    } else {
        std::print.useful["reorder: ", std::str.join[lend 'reorder', between ", "], newln];
    }

    # Loyalty points, for the customers who were served
    std::print.useful["loyalty points:", newln];
    for 's' in lend 'sales' {
        let 'p' := map.get[lend 'points', key copy 's'.customer] otherwise 0;
        std::print.format["  {:<5} {:>3}\n", 's'.customer, 'p'];
    }
}

# ---------------------------------------------------------------------------------------------------
# Counting at the same time: each aisle's shelf count is tallied by its own task, which reports back
# over a channel. The tasks run alongside each other; MAIN waits for every answer.
# ---------------------------------------------------------------------------------------------------

fn shelf_count [lent 'items': list[Item], lent 'stock': map[str.utf8, int32], 'a': Aisle] -> int32 {
    let 'n' := 0;
    for 'it' in lend 'items' {
        if 'it'.aisle equal_to 'a' {
            let 'on_shelf' := map.get[lend 'stock', key copy 'it'.name] otherwise 0;
            change 'n' = 'n' plus 'on_shelf';
        }
    }
    return 'n';
}

fn count_shelves [lent 'items': list[Item], lent 'stock': map[str.utf8, int32]] {
    let 'fresh_ch' := channel[int32];
    let 'bakery_ch' := channel[int32];
    let 'pantry_ch' := channel[int32];
    let 'household_ch' := channel[int32];
    concur with [lend 'fresh_ch', lend 'items', lend 'stock'] { channel.send[lend 'fresh_ch', value shelf_count[lend 'items', lend 'stock', Aisle.fresh]]; }
    concur with [lend 'bakery_ch', lend 'items', lend 'stock'] { channel.send[lend 'bakery_ch', value shelf_count[lend 'items', lend 'stock', Aisle.bakery]]; }
    concur with [lend 'pantry_ch', lend 'items', lend 'stock'] { channel.send[lend 'pantry_ch', value shelf_count[lend 'items', lend 'stock', Aisle.pantry]]; }
    concur with [lend 'household_ch', lend 'items', lend 'stock'] { channel.send[lend 'household_ch', value shelf_count[lend 'items', lend 'stock', Aisle.household]]; }
    let 'f' := channel.take[lend 'fresh_ch'] otherwise 0;
    let 'b' := channel.take[lend 'bakery_ch'] otherwise 0;
    let 'p' := channel.take[lend 'pantry_ch'] otherwise 0;
    let 'h' := channel.take[lend 'household_ch'] otherwise 0;
    std::print.useful["on the shelves tonight: fresh ", 'f', ", bakery ", 'b', ", pantry ", 'p', ", household ", 'h', newln];
}

# ---------------------------------------------------------------------------------------------------
# Looking things up: finding catalog items by what their name starts with or contains
# ---------------------------------------------------------------------------------------------------

fn names_starting [lent 'items': list[Item], lent 'start': str.utf8] -> list[str.utf8] {
    let 'out': list[str.utf8] = list.empty;
    for 'it' in lend 'items' {
        if std::str.starts_with[lend 'it'.name, part copy 'start'] {
            list.append[lend.mut 'out', value copy 'it'.name];
        }
    }
    return give 'out';
}

fn names_containing [lent 'items': list[Item], lent 'part': str.utf8] -> list[str.utf8] {
    let 'out': list[str.utf8] = list.empty;
    for 'it' in lend 'items' {
        if std::str.contains[lend 'it'.name, part copy 'part'] {
            list.append[lend.mut 'out', value copy 'it'.name];
        }
    }
    return give 'out';
}

fn show_search [lent 'items': list[Item]] {
    std::print.useful[newln, "searching the catalog:", newln];
    let 'starts' := ["b", "c", "z"];
    for 's' in lend 'starts' {
        let 'found' := names_starting[lend 'items', lend 's'];
        if list.length[lend 'found'] equal_to 0 {
            std::print.useful["  nothing starts with \"", 's', "\"", newln];
        } else {
            std::print.useful["  starting with \"", 's', "\": ", std::str.join[lend 'found', between ", "], newln];
        }
    }
    let 'has_an' := names_containing[lend 'items', "an"];
    std::print.useful["  containing \"an\": ", std::str.join[lend 'has_an', between ", "], newln];
    let 'sign' := 'shop_name';                     # a const is a fresh value each use, so it is named to lend
    let 'loud' := std::str.upper[lend 'sign'];
    std::print.useful["  the sign over the door says ", 'loud', newln];
}

# ---------------------------------------------------------------------------------------------------
# Restocking: what to order so every shelf is back to its target, in whole cases
# ---------------------------------------------------------------------------------------------------

struct Restock {
    name: str.utf8,
    cases: int32,
    cost: deci64
}

# How many items one case holds, by aisle.
fn case_size ['a': Aisle] -> int32 {
    let 'size' := match 'a' {
        fresh { is 10; }
        bakery { is 6; }
        pantry { is 12; }
        household { is 4; }
    };
    return 'size';
}

# The supplier charges 60% of the shop's price for each item in a case.
fn case_cost ['price': deci64, 'size': int32] -> deci64 {
    let 'each': deci64 = 'price' times 0.60;
    let 'n': deci64 = to.deci64['size'];
    return 'each' times 'n';
}

fn plan_restock [lent 'items': list[Item], lent 'opening': map[str.utf8, int32], lent 'stock': map[str.utf8, int32]] -> list[Restock] {
    let 'plan': list[Restock] = list.empty;
    for 'it' in lend 'items' {
        let 'target' := map.get[lend 'opening', key copy 'it'.name] otherwise 0;
        let 'left' := map.get[lend 'stock', key copy 'it'.name] otherwise 0;
        let 'missing' := 'target' minus 'left';
        if 'missing' > 0 {
            let 'size' := case_size[copy 'it'.aisle];
            # whole cases only: round the count of cases up
            let 'cases' := ('missing' plus 'size' minus 1) div.floor 'size';
            let 'one_case' := case_cost['it'.price, 'size'];
            let 'n': deci64 = to.deci64['cases'];
            list.append[lend.mut 'plan', value Restock[name = copy 'it'.name, cases = 'cases', cost = 'one_case' times 'n']];
        }
    }
    return give 'plan';
}

fn show_restock [lent 'plan': list[Restock]] {
    std::print.useful[newln, "tomorrow's delivery:", newln];
    let 'bill': deci64 = 0;
    for 'r' in lend 'plan' {
        std::print.format["  {:<10} {:>2} case(s)  {:>7.2}\n", 'r'.name, 'r'.cases, 'r'.cost];
        change 'bill' = 'bill' plus 'r'.cost;
    }
    std::print.format["  {:<22} {:>7.2}\n", "to the supplier", 'bill'];
}

# ---------------------------------------------------------------------------------------------------
# A week of trade: seven days of made-up but repeatable numbers, averaged and compared
# ---------------------------------------------------------------------------------------------------

# Customers on day 'd' (1 is Monday): busier towards the weekend, quieter on Sunday.
fn customers_on ['d': int32[min 1, max 7]] -> int32 {
    if 'd' equal_to 7 { return 18; }
    return 20 plus 'd' times 4;
}

fn show_week [] {
    const 'days' := 7;
    std::print.useful[newln, "this week:", newln];
    let 'counts': list[int32] = list.empty;
    for 'd' from 1 to 'days' by 1 {
        let 'n' := customers_on['d'];
        list.append[lend.mut 'counts', value 'n'];
    }
    let 'names' := ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
    let 'busiest' := 0;
    for 'i', 'n' in lend 'counts' {
        std::print.format["  {} {:>3}  ", 'names'[pos 'i'], 'n'];
        for 'k' from 0 before 'n' by 4 {
            std::print.useful["#"];
        }
        std::print.useful[newln];
        change 'busiest' = larger['busiest', 'n'];
    }
    let 'sum' := total_of[lend 'counts'];
    let 'average': bin64 = to.bin64['sum'] div.near to.bin64['days'];
    std::print.format["  {} customers, {:.1} a day, {} on the busiest\n", 'sum', 'average', 'busiest'];
    let 'weekend' := list.slice[lend 'counts', from pos 6, to pos 7];
    let 'weekdays' := list.slice[lend 'counts', from pos 1, before pos 6];
    std::print.useful["  weekdays ", total_of[lend 'weekdays'], ", weekend ", total_of[lend 'weekend'], newln];
}

# ---------------------------------------------------------------------------------------------------
# Sales by the hour: a bar for each opening hour, counted with `before`
# ---------------------------------------------------------------------------------------------------

fn show_hours ['open': int32, 'close': int32] {
    std::print.useful[newln, "customers by the hour:", newln];
    let 'peak' := 0;
    let 'peak_hour' := 'open';
    for 'h' from 'open' before 'close' by 1 {
        # a lunch rush at noon and an evening rush at five, quiet otherwise
        let 'n' := 2;
        if 'h' equal_to 12 or 'h' equal_to 17 { change 'n' = 9; }
        if 'h' equal_to 13 or 'h' equal_to 18 { change 'n' = 6; }
        std::print.format["  {:>2}:00 ", 'h'];
        let 'bar' := list.fill[value "*", count 'n'];
        std::print.useful[std::str.join[lend 'bar', between str.empty], newln];
        if 'n' > 'peak' {
            change 'peak' = 'n';
            change 'peak_hour' = 'h';
        }
    }
    std::print.useful["  busiest from ", 'peak_hour', ":00", newln];
}

# ---------------------------------------------------------------------------------------------------
# The day
# ---------------------------------------------------------------------------------------------------

MAIN {
    const 'opening_hour' := 8;
    const 'closing_hour' := 20;
    std::print.useful['shop_name', ", open ", 'opening_hour', ":00 to ", 'closing_hour', ":00", newln];

    let 'items' := catalog[];
    let 'stock' := opening_stock[];
    let 'opening' := copy 'stock';
    let 'points': map[str.utf8, int32] = map.empty;
    let 'sales': list[Sale] = list.empty;

    # The price list, three columns of a table
    std::print.useful[newln, "today's prices:", newln];
    for 'it' in lend 'items' {
        std::print.format["  {:<10} {:<10} {:>6.2}\n", 'it'.name, aisle_name[copy 'it'.aisle], 'it'.price];
    }
    std::print.useful[newln];

    # Hours the shop is open, counted the new way: from opening, stopping before closing
    let 'hours' := 0;
    for 'h' from 'opening_hour' before 'closing_hour' by 1 {
        change 'hours' = 'hours' plus 1;
    }
    std::print.useful["open for ", 'hours', " hours", newln, newln];

    # Take every order, each with the next receipt number
    let 'orders' := todays_orders[];
    std::print.useful[newln];
    let 'tickets' := Ticket[now = 100, last = 199];
    for 'order' in lend 'orders' {
        let 'n' := Ticket.next[lend.mut 'tickets'];
        let 'ticket' := match 'n' {
            value 'number' { is 'number'; }
            none { is 0; }
        };
        let 'sale' := take_order[lend 'items', lend.mut 'stock', lend 'order', 'ticket'] otherwise {
            out_of_stock 'what' {
                std::print.useful["  sorry, ", 'order'.customer, ": not enough ", 'what', newln, newln];
                continue;
            }
            unknown_item 'what' {
                std::print.useful["  sorry, ", 'order'.customer, ": we do not sell ", 'what', newln, newln];
                continue;
            }
        };
        add_points[lend.mut 'points', lend 'sale'];
        list.append[lend.mut 'sales', value give 'sale'];
        std::print.useful[newln];
    }

    report[lend 'sales', lend 'items', lend 'opening', lend 'stock', lend 'points'];
    count_shelves[lend 'items', lend 'stock'];
    let 'plan' := plan_restock[lend 'items', lend 'opening', lend 'stock'];
    show_restock[lend 'plan'];
    show_hours['opening_hour', 'closing_hour'];
    show_week[];
    show_search[lend 'items'];

    # Exact decimals: the reason money is `deci64` and not `bin64`
    let 'dime': deci64 = 0.10;
    let 'cents_sum': deci64 = 'dime' plus 0.20;
    let 'float_sum': bin64 = 0.1 plus 0.2;
    std::print.useful[newln, "0.10 plus 0.20, stored exactly as money: "];
    std::print.stored['cents_sum', newln];
    std::print.useful["0.1 plus 0.2, stored as a binary float: "];
    std::print.stored['float_sum', newln];
    std::print.useful["good night from the ", 'shop_name', newln];
}
Output
Corner Shop, open 8:00 to 20:00

today's prices:
  apple      fresh        0.45
  banana     fresh        0.30
  milk       fresh        1.20
  bread      bakery       2.10
  croissant  bakery       1.35
  rice       pantry       3.80
  beans      pantry       0.95
  coffee     pantry       6.50
  soap       household    1.75
  candles    household    4.20

open for 12 hours

(order for Gus skipped: cannot read "apple two")

--- receipt #101 for Ann ---
  apple      x 6      2.70
  bread      x 1      1.68
  milk       x 2      2.40
  total              6.78
  paid by cash

--- receipt #102 for Bo ---
  coffee     x 2     13.00
  croissant  x 4      4.32
  total             17.32
  paid by card ending 4242

--- receipt #103 for Cy ---
  rice       x 1      3.80
  beans      x 3      2.85
  soap       x 2      3.50
  candles    x 1      4.20
  discount 10%
  total             12.92
  paid by voucher, then 7.92 in cash

--- receipt #104 for Dee ---
  banana     x12      3.60
  milk       x 1      1.20
  total              4.80
  paid by card ending 4242

--- receipt #105 for Eli ---
  sorry, Eli: not enough coffee

--- receipt #106 for Fay ---
  sorry, Fay: we do not sell pineapple

--- receipt #107 for Hal ---
  candles    x 2      8.40
  bread      x 2      3.36
  croissant  x 2      2.16
  discount 25%
  total             10.44
  paid by card ending 4242


=== Corner Shop, end of day ===
orders served: 5, units sold: 41
takings: 52.26
biggest baskets (units): [13, 9, 7]
best sale: #102, Bo, 17.32
by aisle, at list price:
  fresh          9.90
  bakery        14.40
  pantry        19.65
  household     16.10
reorder: croissant (0 left), coffee (3 left), candles (1 left)
loyalty points:
  Ann    18
  Bo     12
  Cy     14
  Dee    26
  Hal    12
on the shelves tonight: fresh 61, bakery 5, pantry 33, household 8

tomorrow's delivery:
  apple       1 case(s)     2.70
  banana      2 case(s)     3.60
  milk        1 case(s)     7.20
  bread       1 case(s)     7.56
  croissant   1 case(s)     4.86
  rice        1 case(s)    27.36
  beans       1 case(s)     6.84
  coffee      1 case(s)    46.80
  soap        1 case(s)     4.20
  candles     1 case(s)    10.08
  to the supplier         121.20

customers by the hour:
   8:00 **
   9:00 **
  10:00 **
  11:00 **
  12:00 *********
  13:00 ******
  14:00 **
  15:00 **
  16:00 **
  17:00 *********
  18:00 ******
  19:00 **
  busiest from 12:00

this week:
  Mon  24  ######
  Tue  28  #######
  Wed  32  ########
  Thu  36  #########
  Fri  40  ##########
  Sat  44  ###########
  Sun  18  #####
  222 customers, 31.7 a day, 44 on the busiest
  weekdays 160, weekend 62

searching the catalog:
  starting with "b": banana, bread, beans
  starting with "c": croissant, coffee, candles
  nothing starts with "z"
  containing "an": banana, croissant, beans, candles
  the sign over the door says CORNER SHOP

0.10 plus 0.20, stored exactly as money: 0.30
0.1 plus 0.2, stored as a binary float: 0.3000000000000000444089209850062616169452667236328125
good night from the Corner Shop