Project P018

Parking lot

Run an eight-space parking lot: cars come in and go out at typed times, each exit gets a ticket that shows how its fee was worked out - free up to 10 minutes, 3.00 for the first hour, 1.00 for each half hour after, at most 15.00 per 24 hours - and each day has a report of its takings, from a text menu.

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

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

An eight-space parking lot run from a text menu. Cars come in and go out at times the attendant types; each car takes the lowest free space. Each exit gets a ticket that shows the stay and how its fee was worked out, and each day has a report of the cars that went out and the takings.

  • main.eml - the menu and its questions, the lot itself (a class with its spaces), the tickets and the day report
  • fees.eml - the fee rules and the lines that explain a fee
  • text.eml - trimming, capitals, reading and writing times, stays, counts and money

There is no clock: a time is typed as HH:MM and may not be earlier than the last time typed that day, and "next day" starts the following day at 00:00, so a car can stay overnight or for days. A time is kept as minutes since day 1, 00:00, so a stay is a subtraction.

The fee rules: a stay of 10 minutes or less is free. Otherwise every full 24 hours costs the day's maximum, 15.00, and what is left costs 3.00 for the first hour (or part of it) and 1.00 for each half hour (or part of one) after that - but never more than 15.00. So 2 h 35 min costs 7.00, 12 h 40 min reaches the 15.00 maximum, and from 12:00 one day to 14:30 the next costs 15.00 for the full day and 6.00 for the 2 h 30 min after it. Money is whole cents throughout.

What is checked: a plate has 2 to 10 letters, digits or dashes (typed in any case, kept in capitals) and is not already parked; a car going out must be parked; a time is HH:MM from 00:00 to 23:59 and does not go back; a car cannot come in when the lot is full. Anything else asks again; nothing cancels.

Sessions: sessions/basic.in parks four cars and takes them out - one free after 7 minutes, one for 7.00, one that reaches the day's maximum - shows the spaces and the day's report, then lets one car stay overnight into day 2 and goes out at 21.00; sessions/bad-input.in takes a car out of an empty lot, types plates that are empty, too short, too long or have a space, times in the wrong shape, out of range or earlier than the clock, parks a car twice, takes out a car that is not there, stays exactly 10 minutes (free), fills all eight spaces, tries a ninth car, and keeps one car until 07:59 the next day - 23 h 59 min, which stops at the 15.00 maximum.

Built on the verified corpus case parking-lot-tracker (a lot as a class whose spaces are taken from the lowest free one, refusing a car when full).

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

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 7
Pick a number from 1 to 6.

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
No cars are parked.

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 5

-- day 1 --
  no cars went out today
  0 cars out, takings 0.00; 0 still parked

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> 
Cancelled.

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> A
A plate has 2 to 10 characters.
plate> TOO-LONG-PLATE
A plate has 2 to 10 characters.
plate> AB 12
A plate has only letters, digits and dashes.
plate> ab-12
time (HH:MM)> 8:30
Type the time as HH:MM, like 08:30, or nothing to cancel.
time (HH:MM)> 25:00
Type the time as HH:MM, like 08:30, or nothing to cancel.
time (HH:MM)> 07:60
Type the time as HH:MM, like 08:30, or nothing to cancel.
time (HH:MM)> 07:30
AB-12 parks in space 1 at day 1 07:30.

== Parking lot == day 1, 07:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> ab-12
AB-12 is already parked in space 1.

== Parking lot == day 1, 07:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> ZZ-99
No car ZZ-99 is parked here.

== Parking lot == day 1, 07:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> AB-12
time (HH:MM)> 07:00
It is already 07:30 on day 1; times only move forward.
time (HH:MM)> 07:40

AB-12 leaves space 1.
  in    day 1 07:30
  out   day 1 07:40
  stay  10 min
  free: 10 minutes or less
  fee   0.00

== Parking lot == day 1, 07:40
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C1
time (HH:MM)> 08:00
C1 parks in space 1 at day 1 08:00.

== Parking lot == day 1, 08:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C2
time (HH:MM)> 08:00
C2 parks in space 2 at day 1 08:00.

== Parking lot == day 1, 08:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C3
time (HH:MM)> 08:05
C3 parks in space 3 at day 1 08:05.

== Parking lot == day 1, 08:05
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C4
time (HH:MM)> 08:10
C4 parks in space 4 at day 1 08:10.

== Parking lot == day 1, 08:10
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C5
time (HH:MM)> 08:15
C5 parks in space 5 at day 1 08:15.

== Parking lot == day 1, 08:15
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C6
time (HH:MM)> 08:20
C6 parks in space 6 at day 1 08:20.

== Parking lot == day 1, 08:20
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C7
time (HH:MM)> 08:25
C7 parks in space 7 at day 1 08:25.

== Parking lot == day 1, 08:25
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> C8
time (HH:MM)> 08:30
C8 parks in space 8 at day 1 08:30.

== Parking lot == day 1, 08:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
The lot is full.

== Parking lot == day 1, 08:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 1

-- spaces: 8 of 8 taken --
   1  C1          in since day 1 08:00
   2  C2          in since day 1 08:00
   3  C3          in since day 1 08:05
   4  C4          in since day 1 08:10
   5  C5          in since day 1 08:15
   6  C6          in since day 1 08:20
   7  C7          in since day 1 08:25
   8  C8          in since day 1 08:30

== Parking lot == day 1, 08:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 4
Day 2 begins; it is 00:00. 8 cars still parked.

== Parking lot == day 2, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> C1
time (HH:MM)> 07:59

C1 leaves space 1.
  in    day 1 08:00
  out   day 2 07:59
  stay  23 h 59 min
  first hour 3.00, then 46 half hours at 1.00: 46.00 - 15.00 at most: 15.00
  fee   15.00

== Parking lot == day 2, 07:59
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> C2
time (HH:MM)> 
Cancelled.

== Parking lot == day 2, 07:59
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 6
Bye.
What was typed (56 lines)
7
3
5
2

2
A
TOO-LONG-PLATE
AB 12
ab-12
8:30
25:00
07:60
07:30
2
ab-12
3
ZZ-99
3
AB-12
07:00
07:40
2
C1
08:00
2
C2
08:00
2
C3
08:05
2
C4
08:10
2
C5
08:15
2
C6
08:20
2
C7
08:25
2
C8
08:30
2
1
4
3
C1
07:59
3
C2

6

basic

interpreter: byte-equal

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 1

-- spaces: 0 of 8 taken --
   1  free
   2  free
   3  free
   4  free
   5  free
   6  free
   7  free
   8  free

== Parking lot == day 1, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> ABC-123
time (HH:MM)> 08:30
ABC-123 parks in space 1 at day 1 08:30.

== Parking lot == day 1, 08:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> XYZ-789
time (HH:MM)> 08:45
XYZ-789 parks in space 2 at day 1 08:45.

== Parking lot == day 1, 08:45
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> XYZ-789
time (HH:MM)> 08:52

XYZ-789 leaves space 2.
  in    day 1 08:45
  out   day 1 08:52
  stay  7 min
  free: 10 minutes or less
  fee   0.00

== Parking lot == day 1, 08:52
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> KLM-456
time (HH:MM)> 09:00
KLM-456 parks in space 2 at day 1 09:00.

== Parking lot == day 1, 09:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> ABC-123
time (HH:MM)> 11:05

ABC-123 leaves space 1.
  in    day 1 08:30
  out   day 1 11:05
  stay  2 h 35 min
  first hour 3.00, then 4 half hours at 1.00: 4.00
  fee   7.00

== Parking lot == day 1, 11:05
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 2
plate> def-321
time (HH:MM)> 12:00
DEF-321 parks in space 1 at day 1 12:00.

== Parking lot == day 1, 12:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 1

-- spaces: 2 of 8 taken --
   1  DEF-321     in since day 1 12:00
   2  KLM-456     in since day 1 09:00
   3  free
   4  free
   5  free
   6  free
   7  free
   8  free

== Parking lot == day 1, 12:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> KLM-456
time (HH:MM)> 21:40

KLM-456 leaves space 2.
  in    day 1 09:00
  out   day 1 21:40
  stay  12 h 40 min
  first hour 3.00, then 24 half hours at 1.00: 24.00 - 15.00 at most: 15.00
  fee   15.00

== Parking lot == day 1, 21:40
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 5

-- day 1 --
  XYZ-789     day 1 08:45 - 08:52             0.00
  ABC-123     day 1 08:30 - 11:05             7.00
  KLM-456     day 1 09:00 - 21:40            15.00
  3 cars out, takings 22.00; 1 still parked

== Parking lot == day 1, 21:40
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 4
Day 2 begins; it is 00:00. 1 car still parked.

== Parking lot == day 2, 00:00
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 3
plate> DEF-321
time (HH:MM)> 14:30

DEF-321 leaves space 1.
  in    day 1 12:00
  out   day 2 14:30
  stay  1 day 2 h 30 min
  1 full day at 15.00: 15.00
  first hour 3.00, then 3 half hours at 1.00: 3.00
  fee   21.00

== Parking lot == day 2, 14:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 5

-- day 2 --
  DEF-321     day 1 12:00 - day 2 14:30      21.00
  1 car out, takings 21.00; 0 still parked

== Parking lot == day 2, 14:30
1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit
choice> 6
Bye.
What was typed (30 lines)
1
2
ABC-123
08:30
2
XYZ-789
08:45
3
XYZ-789
08:52
2
KLM-456
09:00
3
ABC-123
11:05
2
def-321
12:00
1
3
KLM-456
21:40
5
4
3
DEF-321
14:30
5
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
# P018 parking lot: an eight-space lot. Cars come in and go out at times the
# attendant types; the clock only moves forward, and "next day" starts the
# following day at 00:00, so a car can stay overnight or longer. Each exit
# gets a ticket showing how its fee was worked out, and each day has a report
# of its takings. A time is kept as minutes since day 1, 00:00.
import fees
import text

8 => space_count

class ParkingLot:
    # Spaces numbered from 1; each holds None or [plate, time in].
    def __init__(self, size):
        [] => self.spaces
        for i in [1:size]:
            self.spaces + [None] => self.spaces

    def free_space(self):
        # The lowest free space number, or 0 if the lot is full.
        for i in [0:len(self.spaces) - 1]:
            if self.spaces[i] == None:
                return i + 1
        return 0

    def find(self, plate):
        # The space number of plate, or 0 if it is not parked here.
        for i in [0:len(self.spaces) - 1]:
            if self.spaces[i] != None and self.spaces[i][0] == plate:
                return i + 1
        return 0

    def taken(self):
        0 => n
        for s in self.spaces:
            if s != None:
                n + 1 => n
        return n

    def park(self, plate, t):
        self.free_space() => n
        if n == 0:
            raise ValueError("the lot is full")
        [plate, t] => self.spaces[n - 1]
        return n

    def leave(self, n):
        self.spaces[n - 1] => car
        None => self.spaces[n - 1]
        return car

def day_of(t):
    return int((t - t % 1440) / 1440) + 1

def stamp(t):
    return "day " + str(day_of(t)) + " " + text.clock(t % 1440)

def ask_plate():
    # A plate in capitals, asked again until a good one is typed; "" cancels.
    while True:
        text.upper(text.trim(input("plate> "))) => plate
        if plate == "":
            return ""
        True => good
        for c in plate:
            if not (c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"):
                False => good
        if len(plate) < 2 or len(plate) > 10:
            "A plate has 2 to 10 characters." ^0
        elif not good:
            "A plate has only letters, digits and dashes." ^0
        else:
            return plate

def ask_time(now):
    # A time today, not before now, as minutes since day 1; -1 cancels.
    while True:
        text.trim(input("time (HH:MM)> ")) => answer
        if answer == "":
            return 0 - 1
        text.parse_time(answer) => m
        (day_of(now) - 1) * 1440 + m => t
        if m == 0 - 1:
            "Type the time as HH:MM, like 08:30, or nothing to cancel." ^0
        elif t < now:
            ("It is already " + text.clock(now % 1440) + " on day " + str(day_of(now)) + "; times only move forward.") ^0
        else:
            return t

ParkingLot(space_count) => lot
0 => now
[] => log
True => running
while running:
    "" ^0
    ("== Parking lot == day " + str(day_of(now)) + ", " + text.clock(now % 1440)) ^0
    "1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        "" ^0
        ("-- spaces: " + str(lot.taken()) + " of " + str(space_count) + " taken --") ^0
        for i in [0:space_count - 1]:
            lot.spaces[i] => car
            if car == None:
                ("  " + ("%2d" % (i + 1)) + "  free") ^0
            else:
                ("  " + ("%2d" % (i + 1)) + "  " + ("%-12s" % car[0]) + "in since " + stamp(car[1])) ^0
    elif choice == "2":
        if lot.free_space() == 0:
            "The lot is full." ^0
        else:
            ask_plate() => plate
            0 - 1 => t
            if plate != "" and lot.find(plate) > 0:
                (plate + " is already parked in space " + str(lot.find(plate)) + ".") ^0
            else:
                if plate != "":
                    ask_time(now) => t
                if t == 0 - 1:
                    "Cancelled." ^0
                else:
                    t => now
                    lot.park(plate, t) => n
                    (plate + " parks in space " + str(n) + " at " + stamp(t) + ".") ^0
    elif choice == "3":
        if lot.taken() == 0:
            "No cars are parked." ^0
        else:
            ask_plate() => plate
            0 - 1 => t
            if plate != "" and lot.find(plate) == 0:
                ("No car " + plate + " is parked here.") ^0
            else:
                if plate != "":
                    ask_time(now) => t
                if t == 0 - 1:
                    "Cancelled." ^0
                else:
                    t => now
                    lot.find(plate) => n
                    lot.leave(n) => car
                    t - car[1] => minutes
                    fees.fee(minutes) => cents
                    log + [[plate, n, car[1], t, cents]] => log
                    "" ^0
                    (plate + " leaves space " + str(n) + ".") ^0
                    ("  in    " + stamp(car[1])) ^0
                    ("  out   " + stamp(t)) ^0
                    ("  stay  " + text.duration(minutes)) ^0
                    for line in fees.explain(minutes):
                        ("  " + line) ^0
                    ("  fee   " + text.money(cents)) ^0
    elif choice == "4":
        day_of(now) * 1440 => now
        ("Day " + str(day_of(now)) + " begins; it is 00:00. " + text.count(lot.taken(), "car") + " still parked.") ^0
    elif choice == "5":
        0 => takings
        "" ^0
        ("-- day " + str(day_of(now)) + " --") ^0
        0 => outs
        for e in log:
            if day_of(e[3]) == day_of(now):
                outs + 1 => outs
                takings + e[4] => takings
                text.clock(e[3] % 1440) => out_time
                if day_of(e[2]) != day_of(e[3]):
                    stamp(e[3]) => out_time
                ("  " + ("%-12s" % e[0]) + ("%-28s" % (stamp(e[2]) + " - " + out_time)) + ("%8s" % text.money(e[4]))) ^0
        if outs == 0:
            "  no cars went out today" ^0
        ("  " + text.count(outs, "car") + " out, takings " + text.money(takings) + "; " + str(lot.taken()) + " still parked") ^0
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import fees
import text
space_count = 8

class ParkingLot:
    def __init__(self, size):
        self.spaces = []
        for i in range(1, size+1):
            self.spaces = self.spaces + [None]
    def free_space(self):
        for i in range(0, len(self.spaces)):
            if self.spaces[i] == None:
                return i + 1
        return 0
    def find(self, plate):
        for i in range(0, len(self.spaces)):
            if self.spaces[i] != None and self.spaces[i][0] == plate:
                return i + 1
        return 0
    def taken(self):
        n = 0
        for s in self.spaces:
            if s != None:
                n = n + 1
        return n
    def park(self, plate, t):
        n = self.free_space()
        if n == 0:
            raise ValueError("the lot is full")
        self.spaces[n - 1] = [plate, t]
        return n
    def leave(self, n):
        car = self.spaces[n - 1]
        self.spaces[n - 1] = None
        return car

def day_of(t):
    return int((t - t % 1440) / 1440) + 1

def stamp(t):
    return "day " + str(day_of(t)) + " " + text.clock(t % 1440)

def ask_plate():
    while True:
        plate = text.upper(text.trim(input("plate> ")))
        if plate == "":
            return ""
        good = True
        for c in plate:
            if not c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-":
                good = False
        if len(plate) < 2 or len(plate) > 10:
            print("A plate has 2 to 10 characters.")
        elif not good:
            print("A plate has only letters, digits and dashes.")
        else:
            return plate

def ask_time(now):
    while True:
        answer = text.trim(input("time (HH:MM)> "))
        if answer == "":
            return 0 - 1
        m = text.parse_time(answer)
        t = (day_of(now) - 1) * 1440 + m
        if m == 0 - 1:
            print("Type the time as HH:MM, like 08:30, or nothing to cancel.")
        elif t < now:
            print("It is already " + text.clock(now % 1440) + " on day " + str(day_of(now)) + "; times only move forward.")
        else:
            return t

lot = ParkingLot(space_count)
now = 0
log = []
running = True
while running:
    print("")
    print("== Parking lot == day " + str(day_of(now)) + ", " + text.clock(now % 1440))
    print("1) spaces  2) car in  3) car out  4) next day  5) day report  6) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        print("")
        print("-- spaces: " + str(lot.taken()) + " of " + str(space_count) + " taken --")
        for i in range(0, space_count):
            car = lot.spaces[i]
            if car == None:
                print("  " + "%2d" % (i + 1) + "  free")
            else:
                print("  " + "%2d" % (i + 1) + "  " + "%-12s" % car[0] + "in since " + stamp(car[1]))
    elif choice == "2":
        if lot.free_space() == 0:
            print("The lot is full.")
        else:
            plate = ask_plate()
            t = 0 - 1
            if plate != "" and lot.find(plate) > 0:
                print(plate + " is already parked in space " + str(lot.find(plate)) + ".")
            else:
                if plate != "":
                    t = ask_time(now)
                if t == 0 - 1:
                    print("Cancelled.")
                else:
                    now = t
                    n = lot.park(plate, t)
                    print(plate + " parks in space " + str(n) + " at " + stamp(t) + ".")
    elif choice == "3":
        if lot.taken() == 0:
            print("No cars are parked.")
        else:
            plate = ask_plate()
            t = 0 - 1
            if plate != "" and lot.find(plate) == 0:
                print("No car " + plate + " is parked here.")
            else:
                if plate != "":
                    t = ask_time(now)
                if t == 0 - 1:
                    print("Cancelled.")
                else:
                    now = t
                    n = lot.find(plate)
                    car = lot.leave(n)
                    minutes = t - car[1]
                    cents = fees.fee(minutes)
                    log = log + [[plate, n, car[1], t, cents]]
                    print("")
                    print(plate + " leaves space " + str(n) + ".")
                    print("  in    " + stamp(car[1]))
                    print("  out   " + stamp(t))
                    print("  stay  " + text.duration(minutes))
                    for line in fees.explain(minutes):
                        print("  " + line)
                    print("  fee   " + text.money(cents))
    elif choice == "4":
        now = day_of(now) * 1440
        print("Day " + str(day_of(now)) + " begins; it is 00:00. " + text.count(lot.taken(), "car") + " still parked.")
    elif choice == "5":
        takings = 0
        print("")
        print("-- day " + str(day_of(now)) + " --")
        outs = 0
        for e in log:
            if day_of(e[3]) == day_of(now):
                outs = outs + 1
                takings = takings + e[4]
                out_time = text.clock(e[3] % 1440)
                if day_of(e[2]) != day_of(e[3]):
                    out_time = stamp(e[3])
                print("  " + "%-12s" % e[0] + "%-28s" % (stamp(e[2]) + " - " + out_time) + "%8s" % text.money(e[4]))
        if outs == 0:
            print("  no cars went out today")
        print("  " + text.count(outs, "car") + " out, takings " + text.money(takings) + "; " + str(lot.taken()) + " still parked")
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")

fees.eml

eml
# P018 parking lot - the fee rules. A stay is a whole number of minutes and
# money is whole cents. Up to 10 minutes is free; otherwise every full 24
# hours costs the day's maximum, and what is left costs 3.00 for the first
# hour (or part of it) and 1.00 for each half hour (or part of one) after
# that, but never more than the day's maximum.
import text

10 => grace_minutes
300 => first_hour
100 => per_half_hour
1500 => day_cap
1440 => day_minutes

def part_of_day(minutes):
    # The fee for less than 24 hours: [cents, half hours after the first
    # hour, whether the day's maximum was reached].
    if minutes == 0:
        return [0, 0, False]
    0 => halves
    if minutes > 60:
        minutes - 60 => after
        int((after + 29 - (after + 29) % 30) / 30) => halves
    first_hour + halves * per_half_hour => cents
    if cents > day_cap:
        return [day_cap, halves, True]
    return [cents, halves, False]

def fee(minutes):
    # The fee for a stay of minutes, in cents.
    if minutes <= grace_minutes:
        return 0
    int((minutes - minutes % day_minutes) / day_minutes) => days
    return days * day_cap + part_of_day(minutes % day_minutes)[0]

def explain(minutes):
    # The lines that show how fee(minutes) was worked out.
    if minutes <= grace_minutes:
        return ["free: " + str(grace_minutes) + " minutes or less"]
    [] => out
    int((minutes - minutes % day_minutes) / day_minutes) => days
    if days > 0:
        out + [text.count(days, "full day") + " at " + text.money(day_cap) + ": " + text.money(days * day_cap)] => out
    part_of_day(minutes % day_minutes) => p
    if minutes % day_minutes > 0:
        "first hour " + text.money(first_hour) => line
        if p[1] > 0:
            line + ", then " + text.count(p[1], "half hour") + " at " + text.money(per_half_hour) + ": " + text.money(p[1] * per_half_hour) => line
        if p[2]:
            line + " - " + text.money(day_cap) + " at most: " + text.money(day_cap) => line
        out + [line] => out
    return out
Python projection (fees.py)
import text
grace_minutes = 10
first_hour = 300
per_half_hour = 100
day_cap = 1500
day_minutes = 1440

def part_of_day(minutes):
    if minutes == 0:
        return [0, 0, False]
    halves = 0
    if minutes > 60:
        after = minutes - 60
        halves = int((after + 29 - (after + 29) % 30) / 30)
    cents = first_hour + halves * per_half_hour
    if cents > day_cap:
        return [day_cap, halves, True]
    return [cents, halves, False]

def fee(minutes):
    if minutes <= grace_minutes:
        return 0
    days = int((minutes - minutes % day_minutes) / day_minutes)
    return days * day_cap + part_of_day(minutes % day_minutes)[0]

def explain(minutes):
    if minutes <= grace_minutes:
        return ["free: " + str(grace_minutes) + " minutes or less"]
    out = []
    days = int((minutes - minutes % day_minutes) / day_minutes)
    if days > 0:
        out = out + [text.count(days, "full day") + " at " + text.money(day_cap) + ": " + text.money(days * day_cap)]
    p = part_of_day(minutes % day_minutes)
    if minutes % day_minutes > 0:
        line = "first hour " + text.money(first_hour)
        if p[1] > 0:
            line = line + ", then " + text.count(p[1], "half hour") + " at " + text.money(per_half_hour) + ": " + text.money(p[1] * per_half_hour)
        if p[2]:
            line = line + " - " + text.money(day_cap) + " at most: " + text.money(day_cap)
        out = out + [line]
    return out

text.eml

eml
# P018 parking lot - reading what is typed and writing times and money. The
# interpreter that checks every session does not run string methods yet, so
# the text handling is written out here.

def trim(s):
    # s without the spaces at either end.
    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 upper(s):
    # s with a-z turned into A-Z; everything else as it was.
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if lower_letters[i] == c:
                upper_letters[i] => d
        out + d => out
    return out

def clock(t):
    # Minutes since midnight as HH:MM.
    return ("%02d" % int((t - t % 60) / 60)) + ":" + ("%02d" % (t % 60))

def parse_time(s):
    # Minutes since midnight for HH:MM (00:00 to 23:59), or -1.
    if len(s) != 5 or s[2] != ":":
        return 0 - 1
    for i in [0, 1, 3, 4]:
        if not (s[i] in "0123456789"):
            return 0 - 1
    int(s[0:2]) => h
    int(s[3:5]) => m
    if h > 23 or m > 59:
        return 0 - 1
    return h * 60 + m

def duration(minutes):
    # A stay as "2 h 35 min", "45 min" or "1 day 2 h 30 min".
    int((minutes - minutes % 1440) / 1440) => days
    minutes % 1440 => rest
    int((rest - rest % 60) / 60) => hours
    rest % 60 => mins
    "" => out
    if days > 0:
        count(days, "day") + " " => out
    if hours > 0 or days > 0:
        out + str(hours) + " h " => out
    return out + str(mins) + " min"

def count(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def money(c):
    # Whole cents as 12.50.
    str(c) => s
    while len(s) < 3:
        "0" + s => s
    return s[0:len(s) - 2] + "." + s[len(s) - 2:len(s)]
Python projection (text.py)
def trim(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 upper(s):
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if lower_letters[i] == c:
                d = upper_letters[i]
        out = out + d
    return out

def clock(t):
    return "%02d" % int((t - t % 60) / 60) + ":" + "%02d" % (t % 60)

def parse_time(s):
    if len(s) != 5 or s[2] != ":":
        return 0 - 1
    for i in [0, 1, 3, 4]:
        if not s[i] in "0123456789":
            return 0 - 1
    h = int(s[0:2])
    m = int(s[3:5])
    if h > 23 or m > 59:
        return 0 - 1
    return h * 60 + m

def duration(minutes):
    days = int((minutes - minutes % 1440) / 1440)
    rest = minutes % 1440
    hours = int((rest - rest % 60) / 60)
    mins = rest % 60
    out = ""
    if days > 0:
        out = count(days, "day") + " "
    if hours > 0 or days > 0:
        out = out + str(hours) + " h "
    return out + str(mins) + " min"

def count(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def money(c):
    s = str(c)
    while len(s) < 3:
        s = "0" + s
    return s[0:len(s) - 2] + "." + s[len(s) - 2:len(s)]

Built on these corpus cases