<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P164-travel-expenses/ | updated: 2026-10-19 -->

# P164 Travel expenses

A trip paid by card in four currencies and reimbursed in dollars: an amount column that adds yen to cents, lines summed at ten days' rates, and a card that quotes at authorization and charges at capture.

EML-P project `projects/travel-expenses` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/travel-expenses` runs it and `eml project verify projects/travel-expenses` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: the-total-was-checked-and-the-currencies-were-mixed (https://efficientnewlanguage.org/cases/713-the-total-was-checked-and-the-currencies-were-mixed/), the-amounts-were-summed-at-each-days-rate (https://efficientnewlanguage.org/cases/807-the-amounts-were-summed-at-each-days-rate/), the-exchange-rate-was-right-and-its-timestamp-was-not (https://efficientnewlanguage.org/cases/569-the-exchange-rate-was-right-and-its-timestamp-was-not/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
5

5
chf
5
jpy

5
jpy
0
5
jpy
10000000
5
jpy
1.5
5
gbp

5
gbp
0
5
gbp
100000
5
gbp
12.345
5
gbp
x
5
gbp
20

5
gbp
20
0
5
gbp
20
11
5
gbp
20
4

5
gbp
20
4
3
5
gbp
20
4
11
5
gbp
20
4
x
5
GBP
20
4
4
6
```

Screen:

```text
== Travel expenses ==
A trip paid in four currencies and reimbursed in dollars.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 0
Pick a number from 1 to 6.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> x
Pick a number from 1 to 6.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> 
Cancelled.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> chf
Type USD, JPY, GBP or EUR.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> jpy
amount in yen (1 to 9999999)> 
Cancelled.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> jpy
amount in yen (1 to 9999999)> 0
Type a whole number of yen from 1 to 9999999.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> jpy
amount in yen (1 to 9999999)> 10000000
Type a whole number of yen from 1 to 9999999.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> jpy
amount in yen (1 to 9999999)> 1.5
Type a whole number of yen from 1 to 9999999.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 
Cancelled.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 0
Type an amount such as 12.50, from 0.01 to 99999.99.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 100000
Type an amount such as 12.50, from 0.01 to 99999.99.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 12.345
Type an amount such as 12.50, from 0.01 to 99999.99.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> x
Type an amount such as 12.50, from 0.01 to 99999.99.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 
Cancelled.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 0
Type a whole number from 1 to 10.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 11
Type a whole number from 1 to 10.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 4
day captured (4 to 10)> 
Cancelled.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 4
day captured (4 to 10)> 3
Type a whole number from 4 to 10.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 4
day captured (4 to 10)> 11
Type a whole number from 4 to 10.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> gbp
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 4
day captured (4 to 10)> x
Type a whole number from 4 to 10.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> GBP
amount (0.01 to 99999.99)> 20
day authorized (1 to 10)> 4
day captured (4 to 10)> 4
Quoted at authorization, day 4: 25.32; charged at capture, day 4: 25.32.
The charge is the quote.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
4
5
eur
85.50
3
4
5
JPY
64000
2
5
5
usd
10
1
9
6
```

Screen:

```text
== Travel expenses ==
A trip paid in four currencies and reimbursed in dollars.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 1
The trip, ten days, every payment by card, reimbursed in dollars:
  item                             amount  authorized  captured
  flight                       842.00 USD  day 1       day 1
  hotel, Tokyo, 3 nights       96,000 JPY  day 2       day 5
  dinner, Tokyo                12,800 JPY  day 3       day 4
  hotel, London, 3 nights      540.00 GBP  day 5       day 9
  taxi, London                  38.50 GBP  day 6       day 7
  hotel, Paris, 2 nights       410.00 EUR  day 8       day 10
  dinner, Paris                 96.40 EUR  day 9       day 10

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 2
Dollars for one unit, day by day:
  day        JPY      GBP      EUR
  1     0.006600   1.2600   1.0800
  2     0.006620   1.2640   1.0825
  3     0.006585   1.2615   1.0790
  4     0.006640   1.2660   1.0840
  5     0.006610   1.2705   1.0870
  6     0.006655   1.2680   1.0835
  7     0.006670   1.2720   1.0880
  8     0.006650   1.2695   1.0905
  9     0.006690   1.2750   1.0860
  10    0.006705   1.2790   1.0925

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 3
The amount column added: 301,490 - cents, yen, pence and euro cents in one number.
The report stores the same sum as its total, so the check of the lines against the total passes, and the total is in no currency.
By currency: 842.00 USD, 108,800 JPY, 578.50 GBP, 506.40 EUR.
In dollars, each line at the rate of the day it was paid: 2,848.50.
Every line at the last day's rate: 2,864.65, which is 16.15 more: the value at one moment, where the first total mixes the rates of 6 days.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 4
Quoted at authorization and charged at capture, in dollars:
  item                        quoted   charged  difference
  flight                      842.00    842.00        0.00
  hotel, Tokyo, 3 nights      635.52    634.56       -0.96
  dinner, Tokyo                84.29     84.99       +0.70
  hotel, London, 3 nights     686.07    688.50       +2.43
  taxi, London                 48.82     48.97       +0.15
  hotel, Paris, 2 nights      447.11    447.93       +0.82
  dinner, Paris               104.69    105.32       +0.63
Quoted in all 2,848.50, charged 2,852.27: +3.77.
5 lines were charged more than their quote and 1 less. Only those charged more get written in about, so the complaints see one side of a difference that runs both ways.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> eur
amount (0.01 to 99999.99)> 85.50
day authorized (1 to 10)> 3
day captured (3 to 10)> 4
Quoted at authorization, day 3: 92.25; charged at capture, day 4: 92.68.
+0.43: charged more than the quote the traveller agreed to.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> JPY
amount in yen (1 to 9999999)> 64000
day authorized (1 to 10)> 2
day captured (2 to 10)> 5
Quoted at authorization, day 2: 423.68; charged at capture, day 5: 423.04.
-0.64: charged less than the quote, and nobody writes in about that.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 5
currency (USD, JPY, GBP or EUR)> usd
amount (0.01 to 99999.99)> 10
day authorized (1 to 10)> 1
day captured (1 to 10)> 9
Quoted at authorization, day 1: 10.00; charged at capture, day 9: 10.00.
The charge is the quote.

1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P164 travel expenses: a trip paid in four currencies - the expenses, the
# rates, what the totals are sums of, the card's quote and charge, and an
# expense of your own.
import fx

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 commas(n):
    str(n) => s
    "" => out
    0 => k
    len(s) => i
    while i > 0:
        if k == 3:
            "," + out => out
            0 => k
        s[i - 1] + out => out
        k + 1 => k
        i - 1 => i
    return out

def zeros(n, places):
    str(n) => s
    while len(s) < places:
        "0" + s => s
    return s

def money(c):
    # c >= 0 cents
    return commas(int((c - c % 100) / 100)) + "." + zeros(c % 100, 2)

def signed(c):
    if c > 0:
        return "+" + money(c)
    if c < 0:
        return "-" + money(0 - c)
    return "0.00"

def amount(cur, m):
    if fx.places(cur) == 0:
        return commas(m) + " " + cur
    return money(m) + " " + cur

def show_expenses():
    "The trip, ten days, every payment by card, reimbursed in dollars:" ^0
    ("  " + pad("item", 25) + lpad("amount", 14) + "  " + pad("authorized", 12) + "captured") ^0
    for e in fx.expenses():
        ("  " + pad(e[0], 25) + lpad(amount(e[1], e[2]), 14) + "  " + pad("day " + str(e[3]), 12) + "day " + str(e[4])) ^0

def show_rates():
    "Dollars for one unit, day by day:" ^0
    ("  " + pad("day", 4) + lpad("JPY", 10) + lpad("GBP", 9) + lpad("EUR", 9)) ^0
    1 => d
    while d <= 10:
        fx.rate("JPY", d) => j
        fx.rate("GBP", d) => g
        fx.rate("EUR", d) => u
        ("  " + pad(str(d), 4) + lpad("0." + zeros(j, 6), 10) + lpad("1." + zeros(int((g - 1000000) / 100), 4), 9) + lpad("1." + zeros(int((u - 1000000) / 100), 4), 9)) ^0
        d + 1 => d

def show_totals():
    0 => column
    0 => own_day
    0 => last_day
    [] => days
    for e in fx.expenses():
        column + e[2] => column
        own_day + fx.cents(e[1], e[2], e[3]) => own_day
        last_day + fx.cents(e[1], e[2], 10) => last_day
        False => found
        for d in days:
            if d == e[3]:
                True => found
        if not found and e[1] != "USD":
            days + [e[3]] => days
    ("The amount column added: " + commas(column) + " - cents, yen, pence and euro cents in one number.") ^0
    "The report stores the same sum as its total, so the check of the lines against the total passes, and the total is in no currency." ^0
    "" => by
    for c in fx.currencies():
        0 => t
        for e in fx.expenses():
            if e[1] == c[0]:
                t + e[2] => t
        if by != "":
            by + ", " => by
        by + amount(c[0], t) => by
    ("By currency: " + by + ".") ^0
    ("In dollars, each line at the rate of the day it was paid: " + money(own_day) + ".") ^0
    ("Every line at the last day's rate: " + money(last_day) + ", which is " + money(last_day - own_day) + " more: the value at one moment, where the first total mixes the rates of " + str(len(days)) + " days.") ^0

def show_card():
    "Quoted at authorization and charged at capture, in dollars:" ^0
    ("  " + pad("item", 25) + lpad("quoted", 9) + lpad("charged", 10) + lpad("difference", 12)) ^0
    0 => q
    0 => ch
    0 => more
    0 => less
    for e in fx.expenses():
        fx.cents(e[1], e[2], e[3]) => a
        fx.cents(e[1], e[2], e[4]) => b
        q + a => q
        ch + b => ch
        if b > a:
            more + 1 => more
        if b < a:
            less + 1 => less
        ("  " + pad(e[0], 25) + lpad(money(a), 9) + lpad(money(b), 10) + lpad(signed(b - a), 12)) ^0
    ("Quoted in all " + money(q) + ", charged " + money(ch) + ": " + signed(ch - q) + ".") ^0
    (str(more) + " lines were charged more than their quote and " + str(less) + " less. Only those charged more get written in about, so the complaints see one side of a difference that runs both ways.") ^0

def price(t, top):
    # cents for an amount typed as 12, 12.5 or 12.50 with up to top digits
    # before the point; -1 if it is not one
    0 => dot
    0 => i
    0 => at
    for c in t:
        if c == ".":
            dot + 1 => dot
            i => at
        elif c < "0" or c > "9":
            return 0 - 1
        i + 1 => i
    if dot == 0:
        if len(t) < 1 or len(t) > top:
            return 0 - 1
        return int(t) * 100
    if dot > 1 or at < 1 or at > top or len(t) - at - 1 < 1 or len(t) - at - 1 > 2:
        return 0 - 1
    t[at + 1:] => f
    if len(f) == 1:
        f + "0" => f
    return int(t[0:at]) * 100 + int(f)

def own():
    plain(input("currency (USD, JPY, GBP or EUR)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    "" => cur
    0 => k
    for c in ["USD", "JPY", "GBP", "EUR"]:
        if t == c or t == ["usd", "jpy", "gbp", "eur"][k]:
            c => cur
        k + 1 => k
    if cur == "":
        "Type USD, JPY, GBP or EUR." ^0
        return
    if cur == "JPY":
        plain(input("amount in yen (1 to 9999999)> ")) => a
        if a == "":
            "Cancelled." ^0
            return
        if len(a) > 7 or not digits(a) or int(a) < 1:
            "Type a whole number of yen from 1 to 9999999." ^0
            return
        int(a) => m
    else:
        plain(input("amount (0.01 to 99999.99)> ")) => a
        if a == "":
            "Cancelled." ^0
            return
        price(a, 5) => m
        if m < 1:
            "Type an amount such as 12.50, from 0.01 to 99999.99." ^0
            return
    plain(input("day authorized (1 to 10)> ")) => d1
    if d1 == "":
        "Cancelled." ^0
        return
    if len(d1) > 2 or not digits(d1) or int(d1) < 1 or int(d1) > 10:
        "Type a whole number from 1 to 10." ^0
        return
    int(d1) => a1
    plain(input("day captured (" + str(a1) + " to 10)> ")) => d2
    if d2 == "":
        "Cancelled." ^0
        return
    if len(d2) > 2 or not digits(d2) or int(d2) < a1 or int(d2) > 10:
        ("Type a whole number from " + str(a1) + " to 10.") ^0
        return
    int(d2) => a2
    fx.cents(cur, m, a1) => qa
    fx.cents(cur, m, a2) => cb
    ("Quoted at authorization, day " + str(a1) + ": " + money(qa) + "; charged at capture, day " + str(a2) + ": " + money(cb) + ".") ^0
    if cb > qa:
        (signed(cb - qa) + ": charged more than the quote the traveller agreed to.") ^0
    elif cb < qa:
        (signed(cb - qa) + ": charged less than the quote, and nobody writes in about that.") ^0
    else:
        "The charge is the quote." ^0

"== Travel expenses ==" ^0
"A trip paid in four currencies and reimbursed in dollars." ^0
True => running
while running:
    "" ^0
    "1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_expenses()
    elif choice == "2":
        show_rates()
    elif choice == "3":
        show_totals()
    elif choice == "4":
        show_card()
    elif choice == "5":
        own()
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import fx

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 commas(n):
    s = str(n)
    out = ""
    k = 0
    i = len(s)
    while i > 0:
        if k == 3:
            out = "," + out
            k = 0
        out = s[i - 1] + out
        k = k + 1
        i = i - 1
    return out

def zeros(n, places):
    s = str(n)
    while len(s) < places:
        s = "0" + s
    return s

def money(c):
    return commas(int((c - c % 100) / 100)) + "." + zeros(c % 100, 2)

def signed(c):
    if c > 0:
        return "+" + money(c)
    if c < 0:
        return "-" + money(0 - c)
    return "0.00"

def amount(cur, m):
    if fx.places(cur) == 0:
        return commas(m) + " " + cur
    return money(m) + " " + cur

def show_expenses():
    print("The trip, ten days, every payment by card, reimbursed in dollars:")
    print("  " + pad("item", 25) + lpad("amount", 14) + "  " + pad("authorized", 12) + "captured")
    for e in fx.expenses():
        print("  " + pad(e[0], 25) + lpad(amount(e[1], e[2]), 14) + "  " + pad("day " + str(e[3]), 12) + "day " + str(e[4]))

def show_rates():
    print("Dollars for one unit, day by day:")
    print("  " + pad("day", 4) + lpad("JPY", 10) + lpad("GBP", 9) + lpad("EUR", 9))
    d = 1
    while d <= 10:
        j = fx.rate("JPY", d)
        g = fx.rate("GBP", d)
        u = fx.rate("EUR", d)
        print("  " + pad(str(d), 4) + lpad("0." + zeros(j, 6), 10) + lpad("1." + zeros(int((g - 1000000) / 100), 4), 9) + lpad("1." + zeros(int((u - 1000000) / 100), 4), 9))
        d = d + 1

def show_totals():
    column = 0
    own_day = 0
    last_day = 0
    days = []
    for e in fx.expenses():
        column = column + e[2]
        own_day = own_day + fx.cents(e[1], e[2], e[3])
        last_day = last_day + fx.cents(e[1], e[2], 10)
        found = False
        for d in days:
            if d == e[3]:
                found = True
        if not found and e[1] != "USD":
            days = days + [e[3]]
    print("The amount column added: " + commas(column) + " - cents, yen, pence and euro cents in one number.")
    print("The report stores the same sum as its total, so the check of the lines against the total passes, and the total is in no currency.")
    by = ""
    for c in fx.currencies():
        t = 0
        for e in fx.expenses():
            if e[1] == c[0]:
                t = t + e[2]
        if by != "":
            by = by + ", "
        by = by + amount(c[0], t)
    print("By currency: " + by + ".")
    print("In dollars, each line at the rate of the day it was paid: " + money(own_day) + ".")
    print("Every line at the last day's rate: " + money(last_day) + ", which is " + money(last_day - own_day) + " more: the value at one moment, where the first total mixes the rates of " + str(len(days)) + " days.")

def show_card():
    print("Quoted at authorization and charged at capture, in dollars:")
    print("  " + pad("item", 25) + lpad("quoted", 9) + lpad("charged", 10) + lpad("difference", 12))
    q = 0
    ch = 0
    more = 0
    less = 0
    for e in fx.expenses():
        a = fx.cents(e[1], e[2], e[3])
        b = fx.cents(e[1], e[2], e[4])
        q = q + a
        ch = ch + b
        if b > a:
            more = more + 1
        if b < a:
            less = less + 1
        print("  " + pad(e[0], 25) + lpad(money(a), 9) + lpad(money(b), 10) + lpad(signed(b - a), 12))
    print("Quoted in all " + money(q) + ", charged " + money(ch) + ": " + signed(ch - q) + ".")
    print(str(more) + " lines were charged more than their quote and " + str(less) + " less. Only those charged more get written in about, so the complaints see one side of a difference that runs both ways.")

def price(t, top):
    dot = 0
    i = 0
    at = 0
    for c in t:
        if c == ".":
            dot = dot + 1
            at = i
        elif c < "0" or c > "9":
            return 0 - 1
        i = i + 1
    if dot == 0:
        if len(t) < 1 or len(t) > top:
            return 0 - 1
        return int(t) * 100
    if dot > 1 or at < 1 or at > top or len(t) - at - 1 < 1 or len(t) - at - 1 > 2:
        return 0 - 1
    f = t[at + 1:]
    if len(f) == 1:
        f = f + "0"
    return int(t[0:at]) * 100 + int(f)

def own():
    t = plain(input("currency (USD, JPY, GBP or EUR)> "))
    if t == "":
        print("Cancelled.")
        return
    cur = ""
    k = 0
    for c in ["USD", "JPY", "GBP", "EUR"]:
        if t == c or t == ["usd", "jpy", "gbp", "eur"][k]:
            cur = c
        k = k + 1
    if cur == "":
        print("Type USD, JPY, GBP or EUR.")
        return
    if cur == "JPY":
        a = plain(input("amount in yen (1 to 9999999)> "))
        if a == "":
            print("Cancelled.")
            return
        if len(a) > 7 or not digits(a) or int(a) < 1:
            print("Type a whole number of yen from 1 to 9999999.")
            return
        m = int(a)
    else:
        a = plain(input("amount (0.01 to 99999.99)> "))
        if a == "":
            print("Cancelled.")
            return
        m = price(a, 5)
        if m < 1:
            print("Type an amount such as 12.50, from 0.01 to 99999.99.")
            return
    d1 = plain(input("day authorized (1 to 10)> "))
    if d1 == "":
        print("Cancelled.")
        return
    if len(d1) > 2 or not digits(d1) or int(d1) < 1 or int(d1) > 10:
        print("Type a whole number from 1 to 10.")
        return
    a1 = int(d1)
    d2 = plain(input("day captured (" + str(a1) + " to 10)> "))
    if d2 == "":
        print("Cancelled.")
        return
    if len(d2) > 2 or not digits(d2) or int(d2) < a1 or int(d2) > 10:
        print("Type a whole number from " + str(a1) + " to 10.")
        return
    a2 = int(d2)
    qa = fx.cents(cur, m, a1)
    cb = fx.cents(cur, m, a2)
    print("Quoted at authorization, day " + str(a1) + ": " + money(qa) + "; charged at capture, day " + str(a2) + ": " + money(cb) + ".")
    if cb > qa:
        print(signed(cb - qa) + ": charged more than the quote the traveller agreed to.")
    elif cb < qa:
        print(signed(cb - qa) + ": charged less than the quote, and nobody writes in about that.")
    else:
        print("The charge is the quote.")

print("== Travel expenses ==")
print("A trip paid in four currencies and reimbursed in dollars.")
running = True
while running:
    print("")
    print("1) the expenses  2) the rates  3) totals  4) quote and charge  5) an expense of your own  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_expenses()
    elif choice == "2":
        show_rates()
    elif choice == "3":
        show_totals()
    elif choice == "4":
        show_card()
    elif choice == "5":
        own()
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### fx.eml

```eml
# P164 travel expenses - a ten-day trip paid in four currencies and
# reimbursed in dollars. As in the corpus case
# the-total-was-checked-and-the-currencies-were-mixed, the amount column
# holds yen, pence, euro cents and cents, and adding it gives a number in
# no currency that a check against a stored sum of the same column passes.
# As in the-amounts-were-summed-at-each-days-rate, each line converted at
# its own day's rate is right and their sum mixes ten rates. And as in
# the-exchange-rate-was-right-and-its-timestamp-was-not, a card payment is
# quoted at authorization and charged at capture, two correct rates from two
# moments.

def currencies():
    # [code, decimal places of its minor unit]
    return [["USD", 2], ["JPY", 0], ["GBP", 2], ["EUR", 2]]

def places(cur):
    for c in currencies():
        if c[0] == cur:
            return c[1]
    return 2

def rate(cur, d):
    # dollars for one unit of cur on day d (1 to 10), in millionths
    if cur == "JPY":
        return [6600, 6620, 6585, 6640, 6610, 6655, 6670, 6650, 6690, 6705][d - 1]
    if cur == "GBP":
        return [1260000, 1264000, 1261500, 1266000, 1270500, 1268000, 1272000, 1269500, 1275000, 1279000][d - 1]
    if cur == "EUR":
        return [1080000, 1082500, 1079000, 1084000, 1087000, 1083500, 1088000, 1090500, 1086000, 1092500][d - 1]
    return 1000000

def expenses():
    # [item, currency, amount in its minor unit, day authorized, day captured]
    return [["flight", "USD", 84200, 1, 1],
            ["hotel, Tokyo, 3 nights", "JPY", 96000, 2, 5],
            ["dinner, Tokyo", "JPY", 12800, 3, 4],
            ["hotel, London, 3 nights", "GBP", 54000, 5, 9],
            ["taxi, London", "GBP", 3850, 6, 7],
            ["hotel, Paris, 2 nights", "EUR", 41000, 8, 10],
            ["dinner, Paris", "EUR", 9640, 9, 10]]

def cents(cur, m, d):
    # m minor units of cur, in dollar cents at day d's rate, rounded half-up
    1 => den
    0 => k
    while k < places(cur) + 4:
        den * 10 => den
        k + 1 => k
    m * rate(cur, d) * 2 + den => x
    return int((x - x % (2 * den)) / (2 * den))
```

Python projection of fx.eml:

```python
def currencies():
    return [["USD", 2], ["JPY", 0], ["GBP", 2], ["EUR", 2]]

def places(cur):
    for c in currencies():
        if c[0] == cur:
            return c[1]
    return 2

def rate(cur, d):
    if cur == "JPY":
        return [6600, 6620, 6585, 6640, 6610, 6655, 6670, 6650, 6690, 6705][d - 1]
    if cur == "GBP":
        return [1260000, 1264000, 1261500, 1266000, 1270500, 1268000, 1272000, 1269500, 1275000, 1279000][d - 1]
    if cur == "EUR":
        return [1080000, 1082500, 1079000, 1084000, 1087000, 1083500, 1088000, 1090500, 1086000, 1092500][d - 1]
    return 1000000

def expenses():
    return [["flight", "USD", 84200, 1, 1], ["hotel, Tokyo, 3 nights", "JPY", 96000, 2, 5], ["dinner, Tokyo", "JPY", 12800, 3, 4], ["hotel, London, 3 nights", "GBP", 54000, 5, 9], ["taxi, London", "GBP", 3850, 6, 7], ["hotel, Paris, 2 nights", "EUR", 41000, 8, 10], ["dinner, Paris", "EUR", 9640, 9, 10]]

def cents(cur, m, d):
    den = 1
    k = 0
    while k < places(cur) + 4:
        den = den * 10
        k = k + 1
    x = m * rate(cur, d) * 2 + den
    return int((x - x % (2 * den)) / (2 * den))
```

## README

# P164 - Travel expenses

A ten-day trip paid by card in four currencies and reimbursed in dollars:
what each total is a sum of.

- `main.eml` - the menu, the expenses, the rates, the totals, the card's
  quote and charge, and an expense of your own
- `fx.eml` - the currencies and their minor units, the rates day by day,
  the expenses, and conversion to dollar cents

How each part works:

- Seven expenses in dollars, yen, pounds and euros, each authorized on one
  day and captured on a later one; the rates move both ways over the ten
  days. Amounts are whole minor units (yen has none smaller), rates are in
  millionths of a dollar, and a conversion is rounded half-up to the cent.
- As in the corpus case `the-total-was-checked-and-the-currencies-were-mixed`,
  the amount column adds up to 301,490: cents, yen, pence and euro cents in
  one number, which a check of the lines against a stored sum of the same
  column passes. By currency the trip is 842.00 USD, 108,800 JPY, 578.50
  GBP and 506.40 EUR.
- As in `the-amounts-were-summed-at-each-days-rate`, each line at the rate of
  the day it was paid comes to 2,848.50, a sum over the rates of six days;
  every line at the last day's rate, the value at one moment, is 2,864.65.
- As in `the-exchange-rate-was-right-and-its-timestamp-was-not`, the card
  quotes at authorization and charges at capture: 2,848.50 quoted and
  2,852.27 charged. Five lines were charged more than their quote and the
  Tokyo hotel 0.96 less, the side nobody writes in about.

What is checked: menu choices 1 to 6; a currency (USD, JPY, GBP or EUR, in
capitals or not); an amount in yen from 1 to 9999999, or otherwise written
as 12, 12.5 or 12.50 from 0.01 to 99999.99; a day authorized from 1 to 10
and a day captured from that day to 10. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the expenses, the rates, the totals, and the quote and charge;
- 85.50 EUR from day 3 to day 4 (charged more), 64,000 JPY from day 2 to
  day 5 (charged less), and 10.00 USD (the charge is the quote).

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- currencies: a cancel and CHF;
- yen: a cancel, 0, 10000000 and 1.5; pounds: a cancel, 0, 100000, 12.345
  and x;
- days authorized: a cancel, 0 and 11; days captured: a cancel, 3, 11 and
  x;
- 20.00 GBP authorized and captured on day 4.

Built on the verified corpus cases `the-total-was-checked-and-the-currencies-were-mixed`
(a nightly reconciliation that adds the amount column across nine
currencies), `the-amounts-were-summed-at-each-days-rate` (a balance whose
lines keep the rates of their own days) and
`the-exchange-rate-was-right-and-its-timestamp-was-not` (a payment quoted at
authorization and settled at capture).
