Project P175

Loyalty points

A cafe's points rule changed three times: today's pipeline rebuilding the points table calls 241 of 360 receipts ledger errors where 3 are, and a write-time store rule leaves the 19 old gaps in place.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-19

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

A cafe's points rule, changed three times between 2025 and 2027, and a points table rebuilt with today's rule.

  • main.eml - the menu, the rules, the receipts, the backfill, the store rule, and a receipt of your own
  • loyalty.eml - the calendar of 2025 to 2027, the rules and the day each began, the points each gives, and 360 receipts drawn from a fixed seed

How each part works:

  • v1 gave a point for each whole 1.00; v2 doubled it at weekends; v3 added 50 on a receipt of 30.00 or over; v4, today's rule, gives a point for each whole 2.00 and 50 on 40.00 or over. The ledger holds the points each receipt was given on its day, three of them wrongly.
  • As in the corpus case the-backfill-used-todays-code-and-the-rows-were-old, the points table rebuilt by today's pipeline differs from the ledger on 241 of the 360 receipts: every one before 2027, and one after. The reconciliation puts the recomputed value under expected and calls all 241 ledger errors. Recomputed with the rule in force on each receipt's day, 3 differ: the three really given the wrong points.
  • As in the-rule-was-enforced-at-write-time-and-the-data-predated-it, since 2026-04-01 no receipt can be written without a store. The 19 old receipts without one stay: 12.7% of the table when the rule began, 5.3% at the end of 2027. The rule stopped the problem growing and did not make it smaller.

What is checked: menu choices 1 to 6; a real date written YYYY-MM-DD from 2025-01-01 to 2027-12-31; an amount with two decimals from 0.01 to 999.99. An empty answer cancels.

Sessions: sessions/basic.in gives: - the rules, the receipts, the backfill and the store rule; - receipts of your own: 23.40 on a Saturday under v2, 45.00 under v4 (the backfill leaves it), and 1.99 under v1.

sessions/bad-input.in gives: - menu choices 0 and x; - dates: a cancel, 2024-12-31, 2028-01-01, 2026-02-29, 2026-13-01, 26-01-01 and 2026/01/01; amounts: a cancel, 0.00, 1000.00, 12.5 and x.yz; - 30.00 on the last day of v2 and on the first day of v3.

Built on the verified corpus cases the-backfill-used-todays-code-and-the-rows-were-old (three years of events reprocessed through the current pipeline) and the-rule-was-enforced-at-write-time-and-the-data-predated-it (a validation added to the write path, and the rows written before it).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Loyalty points ==
A cafe's points rule, changed three times, and a table rebuilt with today's.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 0
Pick a number from 1 to 6.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> x
Pick a number from 1 to 6.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 
Cancelled.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2024-12-31
Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2028-01-01
Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-02-29
Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-13-01
Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 26-01-01
Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026/01/01
Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-04-01
amount (0.01 to 999.99)> 
Cancelled.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-04-01
amount (0.01 to 999.99)> 0.00
Type an amount with two decimals, such as 23.40, from 0.01 to 999.99.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-04-01
amount (0.01 to 999.99)> 1000.00
Type an amount with two decimals, such as 23.40, from 0.01 to 999.99.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-04-01
amount (0.01 to 999.99)> 12.5
Type an amount with two decimals, such as 23.40, from 0.01 to 999.99.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-04-01
amount (0.01 to 999.99)> x.yz
Type an amount with two decimals, such as 23.40, from 0.01 to 999.99.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-03-31
amount (0.01 to 999.99)> 30.00
On 2026-03-31, a Tuesday, rule v2 gave 30 points for 30.00; today's rule v4 gives 15.
A backfill with today's rule rewrites this receipt, and the reconciliation calls the ledger wrong.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2026-04-01
amount (0.01 to 999.99)> 30.00
On 2026-04-01, a Wednesday, rule v3 gave 80 points for 30.00; today's rule v4 gives 15.
A backfill with today's rule rewrites this receipt, and the reconciliation calls the ledger wrong.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 6
Bye.
What was typed (38 lines)
0
x
5

5
2024-12-31
5
2028-01-01
5
2026-02-29
5
2026-13-01
5
26-01-01
5
2026/01/01
5
2026-04-01

5
2026-04-01
0.00
5
2026-04-01
1000.00
5
2026-04-01
12.5
5
2026-04-01
x.yz
5
2026-03-31
30.00
5
2026-04-01
30.00
6

basic

interpreter: byte-equal
== Loyalty points ==
A cafe's points rule, changed three times, and a table rebuilt with today's.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 1
The points rule, as it has changed:
  v1  from 2025-01-01  1 point for each whole 1.00
  v2  from 2025-09-01  2 points for each whole 1.00 at weekends, 1 on other days
  v3  from 2026-04-01  1 point for each whole 1.00, and 50 more on a receipt of 30.00 or over
  v4  from 2027-01-01  1 point for each whole 2.00, and 50 more on a receipt of 40.00 or over
Today's rule is v4; the points pipeline implements it, and nothing else.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 2
360 receipts, 2025 to 2027; the first eight:
  date         amount  store  rule then  points given
  2025-01-01    32.00  S2     v1         32
  2025-01-05    17.00  S2     v1         17
  2025-01-07    54.00  S1     v1         54
  2025-01-11    29.50  S1     v1         29
  2025-01-13    55.00  S4     v1         55
  2025-01-17    15.00  S3     v1         15
  2025-01-21    17.00  S2     v1         17
  2025-01-23     8.00  S1     v1         8

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 3
The points table rebuilt from the receipts by today's pipeline, rule v4:
  rule then   receipts  differ from the ledger
  v1                80                      80
  v2                70                      70
  v3                90                      90
  v4               120                       1
With today's rule 241 of 360 receipts differ from the points the ledger recorded. The reconciliation puts the recomputed value under expected, and reports all 241 as ledger errors.
Recomputed with the rule in force on each receipt's day, 3 differ: the receipts that were given the wrong points at the time.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 4
Since 2026-04-01 a receipt cannot be written without a store. Receipts with no store, quarter by quarter:
  quarter end   receipts  no store   share
  2025-03-31          30         2    6.7%
  2025-06-30          60         7   11.7%
  2025-09-30          90        14   15.6%
  2025-12-31         120        17   14.2%
  2026-03-31         150        19   12.7%
  2026-06-30         180        19   10.6%
  2026-09-30         210        19    9.0%
  2026-12-31         240        19    7.9%
  2027-03-31         270        19    7.0%
  2027-06-30         300        19    6.3%
  2027-09-30         330        19    5.8%
  2027-12-31         360        19    5.3%
The count stops at 19 when the rule starts and never moves again; the share falls only as the table grows. The rule stopped the problem growing, and did not make it smaller.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2025-11-15
amount (0.01 to 999.99)> 23.40
On 2025-11-15, a Saturday, rule v2 gave 46 points for 23.40; today's rule v4 gives 11.
A backfill with today's rule rewrites this receipt, and the reconciliation calls the ledger wrong.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2027-03-02
amount (0.01 to 999.99)> 45.00
On 2027-03-02, a Tuesday, rule v4 gave 72 points for 45.00; today's rule v4 gives 72.
A backfill with today's rule leaves this receipt as it was.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 5
date (YYYY-MM-DD, 2025 to 2027)> 2025-02-05
amount (0.01 to 999.99)> 1.99
On 2025-02-05, a Wednesday, rule v1 gave 1 point for 1.99; today's rule v4 gives 0.
A backfill with today's rule rewrites this receipt, and the reconciliation calls the ledger wrong.

1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit
choice> 6
Bye.
What was typed (14 lines)
1
2
3
4
5
2025-11-15
23.40
5
2027-03-02
45.00
5
2025-02-05
1.99
6

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P175 loyalty points: a cafe's points rules over three years - the rules,
# the receipts, the points table rebuilt with today's rule, the store rule
# checked at write time, and a receipt of your own.
import loyalty

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def two(n):
    if n < 10:
        return "0" + str(n)
    return str(n)

def date(n):
    loyalty.date_of(n) => d
    return str(d[0]) + "-" + two(d[1]) + "-" + two(d[2])

def weekday(n):
    return ["Wednesday", "Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday"][n % 7]

def money(c):
    c % 100 => r
    return str(int((c - r) / 100)) + "." + two(r)

def pct(a, b):
    a * 2000 + b => y
    int((y - y % (2 * b)) / (2 * b)) => t
    return str(int((t - t % 10) / 10)) + "." + str(t % 10) + "%"

def show_rules():
    "The points rule, as it has changed:" ^0
    for r in loyalty.rules():
        ("  " + r[0] + "  from " + date(r[1]) + "  " + r[2]) ^0
    "Today's rule is v4; the points pipeline implements it, and nothing else." ^0

def show_receipts():
    loyalty.receipts() => rs
    "360 receipts, 2025 to 2027; the first eight:" ^0
    ("  " + pad("date", 12) + lpad("amount", 7) + "  " + pad("store", 7) + pad("rule then", 11) + "points given") ^0
    0 => i
    while i < 8:
        rs[i] => r
        "-" => st
        if r[2] != "":
            r[2] => st
        ("  " + pad(date(r[0]), 12) + lpad(money(r[1]), 7) + "  " + pad(st, 7) + pad(loyalty.rules()[loyalty.rule_on(r[0])][0], 11) + str(r[3])) ^0
        i + 1 => i

def show_backfill():
    loyalty.receipts() => rs
    "The points table rebuilt from the receipts by today's pipeline, rule v4:" ^0
    ("  " + pad("rule then", 11) + lpad("receipts", 9) + lpad("differ from the ledger", 24)) ^0
    [0, 0, 0, 0] => n
    [0, 0, 0, 0] => differ
    0 => dated
    for r in rs:
        loyalty.rule_on(r[0]) => k
        n[k] + 1 => n[k]
        if loyalty.points(3, r[1], r[0]) != r[3]:
            differ[k] + 1 => differ[k]
        if loyalty.points(k, r[1], r[0]) != r[3]:
            dated + 1 => dated
    0 => total
    0 => k
    while k < 4:
        total + differ[k] => total
        ("  " + pad(loyalty.rules()[k][0], 11) + lpad(str(n[k]), 9) + lpad(str(differ[k]), 24)) ^0
        k + 1 => k
    ("With today's rule " + str(total) + " of 360 receipts differ from the points the ledger recorded. The reconciliation puts the recomputed value under expected, and reports all " + str(total) + " as ledger errors.") ^0
    ("Recomputed with the rule in force on each receipt's day, " + str(dated) + " differ: the receipts that were given the wrong points at the time.") ^0

def show_store():
    loyalty.receipts() => rs
    ("Since " + date(loyalty.store_rule_day()) + " a receipt cannot be written without a store. Receipts with no store, quarter by quarter:") ^0
    ("  " + pad("quarter end", 13) + lpad("receipts", 9) + lpad("no store", 10) + lpad("share", 8)) ^0
    [89, 180, 272, 364, 454, 545, 637, 729, 819, 910, 1002, 1094] => ends
    0 => last
    for e in ends:
        0 => n
        0 => bad
        for r in rs:
            if r[0] <= e:
                n + 1 => n
                if r[2] == "":
                    bad + 1 => bad
        ("  " + pad(date(e), 13) + lpad(str(n), 9) + lpad(str(bad), 10) + lpad(pct(bad, n), 8)) ^0
        bad => last
    ("The count stops at " + str(last) + " when the rule starts and never moves again; the share falls only as the table grows. The rule stopped the problem growing, and did not make it smaller.") ^0

def own():
    plain(input("date (YYYY-MM-DD, 2025 to 2027)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    0 - 1 => n
    if len(t) == 10 and t[4] == "-" and t[7] == "-" and digits(t[0:4]) and digits(t[5:7]) and digits(t[8:10]):
        int(t[0:4]) => y
        int(t[5:7]) => m
        int(t[8:10]) => d
        if y >= 2025 and y <= 2027 and m >= 1 and m <= 12 and d >= 1 and d <= loyalty.month_days()[m - 1]:
            loyalty.day_number(y, m, d) => n
    if n < 0:
        "Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09." ^0
        return
    plain(input("amount (0.01 to 999.99)> ")) => a
    if a == "":
        "Cancelled." ^0
        return
    0 - 1 => c
    if len(a) >= 4 and len(a) <= 6 and a[len(a) - 3] == "." and digits(a[0:len(a) - 3]) and digits(a[len(a) - 2:]):
        int(a[0:len(a) - 3]) * 100 + int(a[len(a) - 2:]) => c
    if c < 1:
        "Type an amount with two decimals, such as 23.40, from 0.01 to 999.99." ^0
        return
    loyalty.rule_on(n) => k
    loyalty.points(k, c, n) => then
    loyalty.points(3, c, n) => now
    " points" => word
    if then == 1:
        " point" => word
    ("On " + t + ", a " + weekday(n) + ", rule " + loyalty.rules()[k][0] + " gave " + str(then) + word + " for " + money(c) + "; today's rule v4 gives " + str(now) + ".") ^0
    if then == now:
        "A backfill with today's rule leaves this receipt as it was." ^0
    else:
        "A backfill with today's rule rewrites this receipt, and the reconciliation calls the ledger wrong." ^0

"== Loyalty points ==" ^0
"A cafe's points rule, changed three times, and a table rebuilt with today's." ^0
True => running
while running:
    "" ^0
    "1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_rules()
    elif choice == "2":
        show_receipts()
    elif choice == "3":
        show_backfill()
    elif choice == "4":
        show_store()
    elif choice == "5":
        own()
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import loyalty

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def two(n):
    if n < 10:
        return "0" + str(n)
    return str(n)

def date(n):
    d = loyalty.date_of(n)
    return str(d[0]) + "-" + two(d[1]) + "-" + two(d[2])

def weekday(n):
    return ["Wednesday", "Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday"][n % 7]

def money(c):
    r = c % 100
    return str(int((c - r) / 100)) + "." + two(r)

def pct(a, b):
    y = a * 2000 + b
    t = int((y - y % (2 * b)) / (2 * b))
    return str(int((t - t % 10) / 10)) + "." + str(t % 10) + "%"

def show_rules():
    print("The points rule, as it has changed:")
    for r in loyalty.rules():
        print("  " + r[0] + "  from " + date(r[1]) + "  " + r[2])
    print("Today's rule is v4; the points pipeline implements it, and nothing else.")

def show_receipts():
    rs = loyalty.receipts()
    print("360 receipts, 2025 to 2027; the first eight:")
    print("  " + pad("date", 12) + lpad("amount", 7) + "  " + pad("store", 7) + pad("rule then", 11) + "points given")
    i = 0
    while i < 8:
        r = rs[i]
        st = "-"
        if r[2] != "":
            st = r[2]
        print("  " + pad(date(r[0]), 12) + lpad(money(r[1]), 7) + "  " + pad(st, 7) + pad(loyalty.rules()[loyalty.rule_on(r[0])][0], 11) + str(r[3]))
        i = i + 1

def show_backfill():
    rs = loyalty.receipts()
    print("The points table rebuilt from the receipts by today's pipeline, rule v4:")
    print("  " + pad("rule then", 11) + lpad("receipts", 9) + lpad("differ from the ledger", 24))
    n = [0, 0, 0, 0]
    differ = [0, 0, 0, 0]
    dated = 0
    for r in rs:
        k = loyalty.rule_on(r[0])
        n[k] = n[k] + 1
        if loyalty.points(3, r[1], r[0]) != r[3]:
            differ[k] = differ[k] + 1
        if loyalty.points(k, r[1], r[0]) != r[3]:
            dated = dated + 1
    total = 0
    k = 0
    while k < 4:
        total = total + differ[k]
        print("  " + pad(loyalty.rules()[k][0], 11) + lpad(str(n[k]), 9) + lpad(str(differ[k]), 24))
        k = k + 1
    print("With today's rule " + str(total) + " of 360 receipts differ from the points the ledger recorded. The reconciliation puts the recomputed value under expected, and reports all " + str(total) + " as ledger errors.")
    print("Recomputed with the rule in force on each receipt's day, " + str(dated) + " differ: the receipts that were given the wrong points at the time.")

def show_store():
    rs = loyalty.receipts()
    print("Since " + date(loyalty.store_rule_day()) + " a receipt cannot be written without a store. Receipts with no store, quarter by quarter:")
    print("  " + pad("quarter end", 13) + lpad("receipts", 9) + lpad("no store", 10) + lpad("share", 8))
    ends = [89, 180, 272, 364, 454, 545, 637, 729, 819, 910, 1002, 1094]
    last = 0
    for e in ends:
        n = 0
        bad = 0
        for r in rs:
            if r[0] <= e:
                n = n + 1
                if r[2] == "":
                    bad = bad + 1
        print("  " + pad(date(e), 13) + lpad(str(n), 9) + lpad(str(bad), 10) + lpad(pct(bad, n), 8))
        last = bad
    print("The count stops at " + str(last) + " when the rule starts and never moves again; the share falls only as the table grows. The rule stopped the problem growing, and did not make it smaller.")

def own():
    t = plain(input("date (YYYY-MM-DD, 2025 to 2027)> "))
    if t == "":
        print("Cancelled.")
        return
    n = 0 - 1
    if len(t) == 10 and t[4] == "-" and t[7] == "-" and digits(t[0:4]) and digits(t[5:7]) and digits(t[8:10]):
        y = int(t[0:4])
        m = int(t[5:7])
        d = int(t[8:10])
        if y >= 2025 and y <= 2027 and m >= 1 and m <= 12 and d >= 1 and d <= loyalty.month_days()[m - 1]:
            n = loyalty.day_number(y, m, d)
    if n < 0:
        print("Type a real date from 2025-01-01 to 2027-12-31, such as 2026-05-09.")
        return
    a = plain(input("amount (0.01 to 999.99)> "))
    if a == "":
        print("Cancelled.")
        return
    c = 0 - 1
    if len(a) >= 4 and len(a) <= 6 and a[len(a) - 3] == "." and digits(a[0:len(a) - 3]) and digits(a[len(a) - 2:]):
        c = int(a[0:len(a) - 3]) * 100 + int(a[len(a) - 2:])
    if c < 1:
        print("Type an amount with two decimals, such as 23.40, from 0.01 to 999.99.")
        return
    k = loyalty.rule_on(n)
    then = loyalty.points(k, c, n)
    now = loyalty.points(3, c, n)
    word = " points"
    if then == 1:
        word = " point"
    print("On " + t + ", a " + weekday(n) + ", rule " + loyalty.rules()[k][0] + " gave " + str(then) + word + " for " + money(c) + "; today's rule v4 gives " + str(now) + ".")
    if then == now:
        print("A backfill with today's rule leaves this receipt as it was.")
    else:
        print("A backfill with today's rule rewrites this receipt, and the reconciliation calls the ledger wrong.")

print("== Loyalty points ==")
print("A cafe's points rule, changed three times, and a table rebuilt with today's.")
running = True
while running:
    print("")
    print("1) the rules  2) the receipts  3) the backfill  4) the store rule  5) a receipt of your own  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_rules()
    elif choice == "2":
        show_receipts()
    elif choice == "3":
        show_backfill()
    elif choice == "4":
        show_store()
    elif choice == "5":
        own()
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")

loyalty.eml

eml
# P175 loyalty points - a cafe's points rule, changed three times between
# 2025 and 2027, and 360 receipts. As in the corpus case
# the-backfill-used-todays-code-and-the-rows-were-old, recomputing every
# receipt with today's rule is correct for today and replaces what each old
# receipt was actually given. And as in
# the-rule-was-enforced-at-write-time-and-the-data-predated-it, a rule
# checked when a receipt is written stops new violations and leaves every
# old one where it was. Days are counted from 2025-01-01, a Wednesday;
# none of 2025, 2026 and 2027 is a leap year.

def month_days():
    return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

def day_number(y, m, d):
    (y - 2025) * 365 => n
    1 => k
    while k < m:
        n + month_days()[k - 1] => n
        k + 1 => k
    return n + d - 1

def date_of(n):
    # [year, month, day] for day number n
    2025 + int((n - n % 365) / 365) => y
    n % 365 => r
    1 => m
    while r >= month_days()[m - 1]:
        r - month_days()[m - 1] => r
        m + 1 => m
    return [y, m, r + 1]

def weekend(n):
    # 2025-01-01 is a Wednesday: day 3 and 4 of each week are Saturday, Sunday
    return n % 7 == 3 or n % 7 == 4

def rules():
    # [name, first day, what it gives]
    return [["v1", 0, "1 point for each whole 1.00"],
            ["v2", 243, "2 points for each whole 1.00 at weekends, 1 on other days"],
            ["v3", 455, "1 point for each whole 1.00, and 50 more on a receipt of 30.00 or over"],
            ["v4", 730, "1 point for each whole 2.00, and 50 more on a receipt of 40.00 or over"]]

def rule_on(n):
    # the index of the rule in force on day n
    0 => r
    0 => i
    for x in rules():
        if n >= x[1]:
            i => r
        i + 1 => i
    return r

def points(r, cents, n):
    # what rule r gives for a receipt of cents on day n
    int((cents - cents % 100) / 100) => whole
    if r == 0:
        return whole
    if r == 1:
        if weekend(n):
            return 2 * whole
        return whole
    if r == 2:
        if cents >= 3000:
            return whole + 50
        return whole
    int((cents - cents % 200) / 200) => twos
    if cents >= 4000:
        return twos + 50
    return twos

def step(x):
    return (x * 48271) % 2147483647

def receipts():
    # [day, cents, store, points the ledger recorded]; a store of "" was
    # never filled in; three receipts were given the wrong points at the time
    [0] * 360 => out
    777 => x
    0 => k
    while k < 360:
        step(x) => x
        int(k * 1095 / 360) + x % 3 => day
        step(x) => x
        200 + 50 * (x % 117) => cents
        step(x) => x
        "S" + str(1 + x % 4) => store
        step(x) => x
        if day < 455 and x % 100 < 9:
            "" => store
        points(rule_on(day), cents, day) => given
        if k % 113 == 40:
            given + 5 => given
        [day, cents, store, given] => out[k]
        k + 1 => k
    return out

def store_rule_day():
    # the write path has required a store since 2026-04-01
    return 455
Python projection (loyalty.py)
def month_days():
    return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

def day_number(y, m, d):
    n = (y - 2025) * 365
    k = 1
    while k < m:
        n = n + month_days()[k - 1]
        k = k + 1
    return n + d - 1

def date_of(n):
    y = 2025 + int((n - n % 365) / 365)
    r = n % 365
    m = 1
    while r >= month_days()[m - 1]:
        r = r - month_days()[m - 1]
        m = m + 1
    return [y, m, r + 1]

def weekend(n):
    return n % 7 == 3 or n % 7 == 4

def rules():
    return [["v1", 0, "1 point for each whole 1.00"], ["v2", 243, "2 points for each whole 1.00 at weekends, 1 on other days"], ["v3", 455, "1 point for each whole 1.00, and 50 more on a receipt of 30.00 or over"], ["v4", 730, "1 point for each whole 2.00, and 50 more on a receipt of 40.00 or over"]]

def rule_on(n):
    r = 0
    i = 0
    for x in rules():
        if n >= x[1]:
            r = i
        i = i + 1
    return r

def points(r, cents, n):
    whole = int((cents - cents % 100) / 100)
    if r == 0:
        return whole
    if r == 1:
        if weekend(n):
            return 2 * whole
        return whole
    if r == 2:
        if cents >= 3000:
            return whole + 50
        return whole
    twos = int((cents - cents % 200) / 200)
    if cents >= 4000:
        return twos + 50
    return twos

def step(x):
    return x * 48271 % 2147483647

def receipts():
    out = [0] * 360
    x = 777
    k = 0
    while k < 360:
        x = step(x)
        day = int(k * 1095 / 360) + x % 3
        x = step(x)
        cents = 200 + 50 * (x % 117)
        x = step(x)
        store = "S" + str(1 + x % 4)
        x = step(x)
        if day < 455 and x % 100 < 9:
            store = ""
        given = points(rule_on(day), cents, day)
        if k % 113 == 40:
            given = given + 5
        out[k] = [day, cents, store, given]
        k = k + 1
    return out

def store_rule_day():
    return 455

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.