Project P173

Cron scheduler

An office on UTC+1 and UTC+2 and a billing job set for 02:30 office time: on the spring change it does not run, on the autumn change it runs twice, and a report over the UTC day is not the day its readers mean.

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

An office's billing job set in office time and its daily report set in UTC, across the two clock changes of 2027.

  • main.eml - the menu, the jobs, the billing job on each kind of day, what the daily report covers, and an office time of your own
  • clock.eml - the kinds of day, the UTC minutes at which an office time happens on each, and the orders by office hour

How each part works:

  • The office is on UTC+1 in winter and UTC+2 in summer; the clocks go forward at 02:00 on 28 March and back at 03:00 on 31 October.
  • As in the corpus case local-time-gap-and-overlap, an office time is a label, not an instant. On 28 March 02:00 to 02:59 never happens, and the billing job set for 02:30 does not run; on 31 October that hour happens twice, and it runs twice. Over the year it still runs 365 times, one for each day. Set for 00:30 UTC instead, it runs once every day.
  • As in the-crontab-was-utc-and-the-report-was-local, the scheduler on UTC runs every job once, and the daily report at 00:30 UTC counts the UTC day just ended, which the office reads as yesterday: in winter that is 01:00 to 01:00 office time, and the 3 orders placed after midnight are counted in the day before's report; in summer 5. The totals still add up.

What is checked: menu choices 1 to 5; an office time written HH:MM; a day from 1 to 4. An empty answer cancels.

Sessions: sessions/basic.in gives: - the jobs, the billing job and the daily report; - 02:30 on 28 March (it does not happen) and on 31 October (twice), 00:30 on 31 October (the day before in UTC), 08:15 on a winter day and 23:59 on a summer day.

sessions/bad-input.in gives: - menu choices 0 and x; - times: a cancel, 2:30, 24:00, 12:60, ab:cd and 12-30; days: a cancel, 0, 5 and x; - the edges of the changes: 01:59 and 03:00 on 28 March, 02:00 and 03:00 on 31 October.

Built on the verified corpus cases local-time-gap-and-overlap (local times mapped to instants across a spring-forward and an autumn-back transition) and the-crontab-was-utc-and-the-report-was-local (a scheduler on UTC and a report of the local yesterday).

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
== Cron scheduler ==
A billing job on office time and a report on UTC, across the year's two clock changes.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 0
Pick a number from 1 to 5.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> x
Pick a number from 1 to 5.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 
Cancelled.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 2:30
Type a time such as 02:30.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 24:00
Type a time such as 02:30.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 12:60
Type a time such as 02:30.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> ab:cd
Type a time such as 02:30.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 12-30
Type a time such as 02:30.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 
Cancelled.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 0
Type a number from 1 to 4.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 5
Type a number from 1 to 4.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> x
Type a number from 1 to 4.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 01:59
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 1
On 28 March 01:59 is 00:59 UTC.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 03:00
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 1
On 28 March 03:00 is 01:00 UTC.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:00
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 2
On 31 October 02:00 happens twice, at 00:00 UTC and at 01:00 UTC. A job set for it runs twice.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 03:00
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 2
On 31 October 03:00 is 02:00 UTC.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 5
Bye.
What was typed (39 lines)
0
x
4

4
2:30
4
24:00
4
12:60
4
ab:cd
4
12-30
4
02:30

4
02:30
0
4
02:30
5
4
02:30
x
4
01:59
1
4
03:00
1
4
02:00
2
4
03:00
2
5

basic

interpreter: byte-equal
== Cron scheduler ==
A billing job on office time and a report on UTC, across the year's two clock changes.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 1
The office keeps local time: UTC+1 in winter, UTC+2 in summer. In 2027 the clocks go forward at 02:00 on 28 March and back at 03:00 on 31 October.
  billing   every day at 02:30 office time, the scheduler on office time
  report    every day at 00:30 UTC, the scheduler on UTC: the orders of the day just ended

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 2
The billing job, set for 02:30 office time, on each kind of day:
  28 March      02:30 does not happen: the clocks go from 01:59 to 03:00  runs 0 times
  31 October    02:30 happens at 00:30 UTC and again at 01:30 UTC         runs 2 times
  a winter day  02:30 is 01:30 UTC                                        runs once
  a summer day  02:30 is 00:30 UTC                                        runs once
Over 2027 - 147 winter days, 216 summer days and the two changes - it runs 365 times, one for each day, and still skips 28 March and bills 31 October's hour twice.
Set for 00:30 UTC instead, it runs once every day: at 01:30 office time in winter and at 02:30 in summer.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 3
The report runs at 00:30 UTC and counts the orders of the UTC day just ended; the office reads it as yesterday. Orders by office hour, on any ordinary day:
  00:00-05:59  3, 2, 1, 1, 0, 1
  06:00-11:59  2, 5, 9, 14, 18, 20
  12:00-17:59  24, 22, 19, 18, 20, 23
  18:00-23:59  26, 22, 15, 10, 7, 5
A day's 287 orders. In winter a UTC day is 01:00 to 01:00 office time: the report leaves out the 3 orders placed between midnight and 01:00, counted in the day before's report, and takes in the next day's first 3.
In summer it is 02:00 to 02:00: 5 orders a day are counted in the wrong day's report. The totals still add up, so no reconciliation notices.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 1
On 28 March 02:30 does not happen: the clocks go from 01:59 to 03:00. A job set for it does not run that day.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 02:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 2
On 31 October 02:30 happens twice, at 00:30 UTC and at 01:30 UTC. A job set for it runs twice.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 00:30
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 2
On 31 October 00:30 is 22:30 UTC the day before.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 08:15
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 3
On a winter day 08:15 is 07:15 UTC.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 4
office time (HH:MM)> 23:59
which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> 4
On a summer day 23:59 is 21:59 UTC.

1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit
choice> 5
Bye.
What was typed (19 lines)
1
2
3
4
02:30
1
4
02:30
2
4
00:30
2
4
08:15
3
4
23:59
4
5

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
# P173 cron scheduler: an office's billing job set in local time and its
# report set in UTC - the jobs, the billing job on each kind of day, what
# the daily report covers, and an office time of your own.
import clock

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 digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def hhmm(m):
    # m minutes from midnight, 0 to 1439
    m % 60 => mm
    int((m - mm) / 60) => h
    "" => s
    if h < 10:
        "0" => s
    s + str(h) + ":" => s
    if mm < 10:
        s + "0" => s
    return s + str(mm)

def utc(m):
    if m < 0:
        return hhmm(m + 1440) + " UTC the day before"
    return hhmm(m) + " UTC"

def times(n):
    if n == 1:
        return "once"
    return str(n) + " times"

def show_jobs():
    "The office keeps local time: UTC+1 in winter, UTC+2 in summer. In 2027 the clocks go forward at 02:00 on 28 March and back at 03:00 on 31 October." ^0
    "  billing   every day at 02:30 office time, the scheduler on office time" ^0
    "  report    every day at 00:30 UTC, the scheduler on UTC: the orders of the day just ended" ^0

def show_billing():
    "The billing job, set for 02:30 office time, on each kind of day:" ^0
    150 => m
    0 => d
    for day in clock.days():
        clock.utc_of_local(d, m) => us
        if len(us) == 0:
            "02:30 does not happen: the clocks go from 01:59 to 03:00" => what
        elif len(us) == 2:
            "02:30 happens at " + utc(us[0]) + " and again at " + utc(us[1]) => what
        else:
            "02:30 is " + utc(us[0]) => what
        ("  " + pad(day[0], 14) + pad(what, 58) + "runs " + times(len(us))) ^0
        d + 1 => d
    147 * len(clock.utc_of_local(2, m)) + 216 * len(clock.utc_of_local(3, m)) + len(clock.utc_of_local(0, m)) + len(clock.utc_of_local(1, m)) => runs
    ("Over 2027 - 147 winter days, 216 summer days and the two changes - it runs " + str(runs) + " times, one for each day, and still skips 28 March and bills 31 October's hour twice.") ^0
    "Set for 00:30 UTC instead, it runs once every day: at 01:30 office time in winter and at 02:30 in summer." ^0

def show_report():
    "The report runs at 00:30 UTC and counts the orders of the UTC day just ended; the office reads it as yesterday. Orders by office hour, on any ordinary day:" ^0
    0 => h
    while h < 24:
        "  " + hhmm(h * 60) + "-" + hhmm(h * 60 + 300 + 59) + "  " => line
        0 => k
        while k < 6:
            if k > 0:
                line + ", " => line
            line + str(clock.orders(h + k)) => line
            k + 1 => k
        line ^0
        h + 6 => h
    ("A day's " + str(clock.day_orders()) + " orders. In winter a UTC day is 01:00 to 01:00 office time: the report leaves out the " + str(clock.first_hours(1)) + " orders placed between midnight and 01:00, counted in the day before's report, and takes in the next day's first " + str(clock.first_hours(1)) + ".") ^0
    ("In summer it is 02:00 to 02:00: " + str(clock.first_hours(2)) + " orders a day are counted in the wrong day's report. The totals still add up, so no reconciliation notices.") ^0

def own():
    plain(input("office time (HH:MM)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) != 5 or t[2] != ":" or not digits(t[0:2]) or not digits(t[3:5]) or int(t[0:2]) > 23 or int(t[3:5]) > 59:
        "Type a time such as 02:30." ^0
        return
    int(t[0:2]) * 60 + int(t[3:5]) => m
    plain(input("which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> ")) => d
    if d == "":
        "Cancelled." ^0
        return
    if len(d) != 1 or not digits(d) or int(d) < 1 or int(d) > 4:
        "Type a number from 1 to 4." ^0
        return
    int(d) - 1 => day
    clock.days()[day][0] => name
    clock.utc_of_local(day, m) => us
    if len(us) == 0:
        ("On " + name + " " + t + " does not happen: the clocks go from 01:59 to 03:00. A job set for it does not run that day.") ^0
    elif len(us) == 2:
        ("On " + name + " " + t + " happens twice, at " + utc(us[0]) + " and at " + utc(us[1]) + ". A job set for it runs twice.") ^0
    else:
        ("On " + name + " " + t + " is " + utc(us[0]) + ".") ^0

"== Cron scheduler ==" ^0
"A billing job on office time and a report on UTC, across the year's two clock changes." ^0
True => running
while running:
    "" ^0
    "1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_jobs()
    elif choice == "2":
        show_billing()
    elif choice == "3":
        show_report()
    elif choice == "4":
        own()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import clock

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 digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def hhmm(m):
    mm = m % 60
    h = int((m - mm) / 60)
    s = ""
    if h < 10:
        s = "0"
    s = s + str(h) + ":"
    if mm < 10:
        s = s + "0"
    return s + str(mm)

def utc(m):
    if m < 0:
        return hhmm(m + 1440) + " UTC the day before"
    return hhmm(m) + " UTC"

def times(n):
    if n == 1:
        return "once"
    return str(n) + " times"

def show_jobs():
    print("The office keeps local time: UTC+1 in winter, UTC+2 in summer. In 2027 the clocks go forward at 02:00 on 28 March and back at 03:00 on 31 October.")
    print("  billing   every day at 02:30 office time, the scheduler on office time")
    print("  report    every day at 00:30 UTC, the scheduler on UTC: the orders of the day just ended")

def show_billing():
    print("The billing job, set for 02:30 office time, on each kind of day:")
    m = 150
    d = 0
    for day in clock.days():
        us = clock.utc_of_local(d, m)
        if len(us) == 0:
            what = "02:30 does not happen: the clocks go from 01:59 to 03:00"
        elif len(us) == 2:
            what = "02:30 happens at " + utc(us[0]) + " and again at " + utc(us[1])
        else:
            what = "02:30 is " + utc(us[0])
        print("  " + pad(day[0], 14) + pad(what, 58) + "runs " + times(len(us)))
        d = d + 1
    runs = 147 * len(clock.utc_of_local(2, m)) + 216 * len(clock.utc_of_local(3, m)) + len(clock.utc_of_local(0, m)) + len(clock.utc_of_local(1, m))
    print("Over 2027 - 147 winter days, 216 summer days and the two changes - it runs " + str(runs) + " times, one for each day, and still skips 28 March and bills 31 October's hour twice.")
    print("Set for 00:30 UTC instead, it runs once every day: at 01:30 office time in winter and at 02:30 in summer.")

def show_report():
    print("The report runs at 00:30 UTC and counts the orders of the UTC day just ended; the office reads it as yesterday. Orders by office hour, on any ordinary day:")
    h = 0
    while h < 24:
        line = "  " + hhmm(h * 60) + "-" + hhmm(h * 60 + 300 + 59) + "  "
        k = 0
        while k < 6:
            if k > 0:
                line = line + ", "
            line = line + str(clock.orders(h + k))
            k = k + 1
        print(line)
        h = h + 6
    print("A day's " + str(clock.day_orders()) + " orders. In winter a UTC day is 01:00 to 01:00 office time: the report leaves out the " + str(clock.first_hours(1)) + " orders placed between midnight and 01:00, counted in the day before's report, and takes in the next day's first " + str(clock.first_hours(1)) + ".")
    print("In summer it is 02:00 to 02:00: " + str(clock.first_hours(2)) + " orders a day are counted in the wrong day's report. The totals still add up, so no reconciliation notices.")

def own():
    t = plain(input("office time (HH:MM)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) != 5 or t[2] != ":" or not digits(t[0:2]) or not digits(t[3:5]) or int(t[0:2]) > 23 or int(t[3:5]) > 59:
        print("Type a time such as 02:30.")
        return
    m = int(t[0:2]) * 60 + int(t[3:5])
    d = plain(input("which day (1 28 March, 2 31 October, 3 a winter day, 4 a summer day)> "))
    if d == "":
        print("Cancelled.")
        return
    if len(d) != 1 or not digits(d) or int(d) < 1 or int(d) > 4:
        print("Type a number from 1 to 4.")
        return
    day = int(d) - 1
    name = clock.days()[day][0]
    us = clock.utc_of_local(day, m)
    if len(us) == 0:
        print("On " + name + " " + t + " does not happen: the clocks go from 01:59 to 03:00. A job set for it does not run that day.")
    elif len(us) == 2:
        print("On " + name + " " + t + " happens twice, at " + utc(us[0]) + " and at " + utc(us[1]) + ". A job set for it runs twice.")
    else:
        print("On " + name + " " + t + " is " + utc(us[0]) + ".")

print("== Cron scheduler ==")
print("A billing job on office time and a report on UTC, across the year's two clock changes.")
running = True
while running:
    print("")
    print("1) the jobs  2) the billing job  3) the daily report  4) an office time of your own  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_jobs()
    elif choice == "2":
        show_billing()
    elif choice == "3":
        show_report()
    elif choice == "4":
        own()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

clock.eml

eml
# P173 cron scheduler - an office on UTC+1 in winter and UTC+2 in summer,
# with the clocks going forward at 02:00 on 28 March 2027 and back at 03:00
# on 31 October. As in the corpus case local-time-gap-and-overlap, a local
# time is a label, not an instant: on the spring day 02:00 to 02:59 never
# happens, and on the autumn day it happens twice, so a job set for 02:30
# local does not run, or runs twice. And as in
# the-crontab-was-utc-and-the-report-was-local, a scheduler on UTC runs
# every job exactly once, and a daily report over the UTC day is not the
# day its readers mean. Times are minutes from midnight.

def days():
    # the kinds of day: [name, what happens to the clocks]
    return [["28 March", "forward an hour at 02:00"], ["31 October", "back an hour at 03:00"], ["a winter day", "UTC+1 all day"], ["a summer day", "UTC+2 all day"]]

def utc_of_local(day, m):
    # the UTC minutes at which local minute m happens on that day (0, 1 or
    # 2 of them); a negative minute is on the day before
    if day == 0:
        if m < 120:
            return [m - 60]
        if m < 180:
            return []
        return [m - 120]
    if day == 1:
        if m < 120:
            return [m - 120]
        if m < 180:
            return [m - 120, m - 60]
        return [m - 60]
    if day == 2:
        return [m - 60]
    return [m - 120]

def offset(day):
    # minutes local time is ahead of UTC, all day long (days 2 and 3)
    if day == 2:
        return 60
    return 120

def orders(h):
    # orders placed in the local hour h, on any ordinary day
    return [3, 2, 1, 1, 0, 1, 2, 5, 9, 14, 18, 20, 24, 22, 19, 18, 20, 23, 26, 22, 15, 10, 7, 5][h]

def day_orders():
    0 => t
    0 => h
    while h < 24:
        t + orders(h) => t
        h + 1 => h
    return t

def first_hours(n):
    # orders in the first n local hours of a day
    0 => t
    0 => h
    while h < n:
        t + orders(h) => t
        h + 1 => h
    return t
Python projection (clock.py)
def days():
    return [["28 March", "forward an hour at 02:00"], ["31 October", "back an hour at 03:00"], ["a winter day", "UTC+1 all day"], ["a summer day", "UTC+2 all day"]]

def utc_of_local(day, m):
    if day == 0:
        if m < 120:
            return [m - 60]
        if m < 180:
            return []
        return [m - 120]
    if day == 1:
        if m < 120:
            return [m - 120]
        if m < 180:
            return [m - 120, m - 60]
        return [m - 60]
    if day == 2:
        return [m - 60]
    return [m - 120]

def offset(day):
    if day == 2:
        return 60
    return 120

def orders(h):
    return [3, 2, 1, 1, 0, 1, 2, 5, 9, 14, 18, 20, 24, 22, 19, 18, 20, 23, 26, 22, 15, 10, 7, 5][h]

def day_orders():
    t = 0
    h = 0
    while h < 24:
        t = t + orders(h)
        h = h + 1
    return t

def first_hours(n):
    t = 0
    h = 0
    while h < n:
        t = t + orders(h)
        h = h + 1
    return t

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.