Project P026

Temperature log

A day's temperature readings from up to six places: each place's lowest, highest and average, a moving average over 2 to 10 readings, and alerts for readings outside a place's safe range - when a reading comes in and in a list. Temperatures are kept in whole tenths of a degree, so the averages are exact until the display rounds them.

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

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 day's temperature readings from up to six places. For each place it shows the lowest, the highest and the average, a moving average over 2 to 10 readings, and alerts for readings outside the place's safe range: one as soon as such a reading comes in, and a list of all of them on request.

  • main.eml - the menu, the questions and their checks, and what the screen shows
  • readings.eml - the places and their readings: finding a place, keeping readings in time order, lowest/highest/total, how far a reading is outside a safe range, and the moving totals
  • units.eml - reading and writing temperatures and times, the rounded average, lower case, padding, lists in words

How each part works:

  • Temperatures are kept as whole tenths of a degree: 21.5 C is 215. Totals and moving totals are therefore exact, and only the display rounds: an average is the total over the count, rounded to a tenth with halves away from zero (-171.67 tenths is -17.2 C).
  • Readings are kept in time order, whatever order they are typed in; a place has at most one reading per minute of the day.
  • The moving average slides: each new reading is added to the running total and the reading that leaves the window is taken away. With floating point that running total would drift away from a fresh sum; in whole tenths it is always exactly the sum of the window.
  • The lowest and the highest are the earliest when a value comes up again, and across places the first place added wins a tie.
  • A safe range runs from a lowest to a highest temperature, both allowed. A reading below or above it is reported with how far outside it is.

What is checked: a place name is 1 to 16 letters, digits, spaces or hyphens, matched without regard to case; there are at most 6 places and 48 readings a place; a time is H:MM or HH:MM from 00:00 to 23:59, not already taken at that place; a temperature is -60.0 to 60.0 with at most one decimal; a safe range's highest is above its lowest; a window is 2 to 10 readings and no more than the place has. An empty answer cancels.

Sessions: sessions/basic.in logs a greenhouse (one reading typed out of order, one place name typed in lower case) and a freezer, sets both safe ranges, gets an alert at 31.5 C, and shows the summary, the moving average of 3 and the alerts, before and after a third place without a range; sessions/bad-input.in asks for a summary, a moving average and alerts with no readings, then types a name with #, times 25:00 and 7:5, temperatures abc, 61, -60.05 and 21.25, a reading at a time that is taken, a moving average with one reading, a place that does not exist, a safe range whose highest is not above its lowest, a seventh place, windows 1 and x, and a place's 49th reading.

Built on the verified corpus cases temperature-alert-system (readings compared with thresholds to raise alerts) and moving-average (a sliding total, and why a running float total drifts from a fresh sum).

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

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 0
Pick a number from 1 to 6.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> seven
Pick a number from 1 to 6.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 3
No readings yet.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 4
No readings yet.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 5
No readings yet.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 2
No places yet; add a reading first.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> 
Cancelled.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Lab #1
A place name is 1 to 16 letters, digits, spaces or hyphens.
place> Lab
time (HH:MM)> 25:00
Type a time from 00:00 to 23:59, such as 07:30.
time (HH:MM)> 7:5
Type a time from 00:00 to 23:59, such as 07:30.
time (HH:MM)> 07:05
temperature (C)> abc
Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.
temperature (C)> 61
Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.
temperature (C)> -60.05
Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.
temperature (C)> 21.25
Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.
temperature (C)> -0.5
Recorded: Lab 07:05 -0.5 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> lab
time (HH:MM)> 7:05
Lab already has a reading at 07:05 (-0.5 C).
time (HH:MM)> 
Cancelled.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 4
place> Lab
A moving average needs at least 2 readings; Lab has 1.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 2
place> Kitchen
There is no place called Kitchen yet; add a reading for it first.
place> 
Cancelled.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 2
place> Lab
lowest (C)> 5
highest (C)> 5
The highest has to be above the lowest (5.0 C).
highest (C)> 4.9
The highest has to be above the lowest (5.0 C).
highest (C)> 
Cancelled.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 5
No place has a safe range yet; set one with 2.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 12:00
temperature (C)> 10
Recorded: A 12:00 10.0 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> B
time (HH:MM)> 12:00
temperature (C)> 10
Recorded: B 12:00 10.0 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> C
time (HH:MM)> 12:00
temperature (C)> 10
Recorded: C 12:00 10.0 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> D
time (HH:MM)> 12:00
temperature (C)> 10
Recorded: D 12:00 10.0 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> E
time (HH:MM)> 12:00
temperature (C)> 10
Recorded: E 12:00 10.0 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> F
There are already 6 places; a reading has to go to one of them.
place> 
Cancelled.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 2
place> Lab
lowest (C)> -1
highest (C)> 1
Lab's safe range: -1.0 to 1.0 C. Every reading so far is inside it.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 5
Every reading is inside its place's safe range.
No safe range yet: A, B, C, D and E.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Lab
time (HH:MM)> 09:00
temperature (C)> 2
Recorded: Lab 09:00 2.0 C.
ALERT: 2.0 C is 1.0 above Lab's safe range of -1.0 to 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 4
place> Lab
window (2 to 2 readings)> 1
Type a whole number from 2 to 2.
window (2 to 2 readings)> x
Type a whole number from 2 to 2.
window (2 to 2 readings)> 2
Lab, moving average of the last 2 readings:
  07:05   -0.5
  09:00    2.0   average 0.8

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 3
Lab: 2 readings, 07:05 to 09:00
  lowest -0.5 C at 07:05, highest 2.0 C at 09:00, average 0.8 C
  safe range -1.0 to 1.0 C, 1 reading outside it
A: 1 reading, at 12:00
  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C
  no safe range
B: 1 reading, at 12:00
  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C
  no safe range
C: 1 reading, at 12:00
  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C
  no safe range
D: 1 reading, at 12:00
  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C
  no safe range
E: 1 reading, at 12:00
  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C
  no safe range
All places: 7 readings; lowest -0.5 C (Lab, 07:05), highest 10.0 C (A, 12:00).

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 00:00
temperature (C)> 0
Recorded: A 00:00 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 00:30
temperature (C)> 1
Recorded: A 00:30 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 01:00
temperature (C)> 2
Recorded: A 01:00 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 01:30
temperature (C)> 3
Recorded: A 01:30 3.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 02:00
temperature (C)> 4
Recorded: A 02:00 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 02:30
temperature (C)> 5
Recorded: A 02:30 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 03:00
temperature (C)> 6
Recorded: A 03:00 6.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 03:30
temperature (C)> 0
Recorded: A 03:30 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 04:00
temperature (C)> 1
Recorded: A 04:00 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 04:30
temperature (C)> 2
Recorded: A 04:30 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 05:00
temperature (C)> 3
Recorded: A 05:00 3.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 05:30
temperature (C)> 4
Recorded: A 05:30 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 06:00
temperature (C)> 5
Recorded: A 06:00 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 06:30
temperature (C)> 6
Recorded: A 06:30 6.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 07:00
temperature (C)> 0
Recorded: A 07:00 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 07:30
temperature (C)> 1
Recorded: A 07:30 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 08:00
temperature (C)> 2
Recorded: A 08:00 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 08:30
temperature (C)> 3
Recorded: A 08:30 3.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 09:00
temperature (C)> 4
Recorded: A 09:00 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 09:30
temperature (C)> 5
Recorded: A 09:30 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 10:00
temperature (C)> 6
Recorded: A 10:00 6.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 10:30
temperature (C)> 0
Recorded: A 10:30 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 11:00
temperature (C)> 1
Recorded: A 11:00 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 11:30
temperature (C)> 2
Recorded: A 11:30 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 12:30
temperature (C)> 4
Recorded: A 12:30 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 13:00
temperature (C)> 5
Recorded: A 13:00 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 13:30
temperature (C)> 6
Recorded: A 13:30 6.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 14:00
temperature (C)> 0
Recorded: A 14:00 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 14:30
temperature (C)> 1
Recorded: A 14:30 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 15:00
temperature (C)> 2
Recorded: A 15:00 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 15:30
temperature (C)> 3
Recorded: A 15:30 3.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 16:00
temperature (C)> 4
Recorded: A 16:00 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 16:30
temperature (C)> 5
Recorded: A 16:30 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 17:00
temperature (C)> 6
Recorded: A 17:00 6.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 17:30
temperature (C)> 0
Recorded: A 17:30 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 18:00
temperature (C)> 1
Recorded: A 18:00 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 18:30
temperature (C)> 2
Recorded: A 18:30 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 19:00
temperature (C)> 3
Recorded: A 19:00 3.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 19:30
temperature (C)> 4
Recorded: A 19:30 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 20:00
temperature (C)> 5
Recorded: A 20:00 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 20:30
temperature (C)> 6
Recorded: A 20:30 6.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 21:00
temperature (C)> 0
Recorded: A 21:00 0.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 21:30
temperature (C)> 1
Recorded: A 21:30 1.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 22:00
temperature (C)> 2
Recorded: A 22:00 2.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 22:30
temperature (C)> 3
Recorded: A 22:30 3.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 23:00
temperature (C)> 4
Recorded: A 23:00 4.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
time (HH:MM)> 23:30
temperature (C)> 5
Recorded: A 23:30 5.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> A
A already has 48 readings, the most a place keeps.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 6
Bye.
What was typed (264 lines)
0
seven
3
4
5
2
1

1
Lab #1
Lab
25:00
7:5
07:05
abc
61
-60.05
21.25
-0.5
1
lab
7:05

4
Lab
2
Kitchen

2
Lab
5
5
4.9

5
1
A
12:00
10
1
B
12:00
10
1
C
12:00
10
1
D
12:00
10
1
E
12:00
10
1
F

2
Lab
-1
1
5
1
Lab
09:00
2
4
Lab
1
x
2
3
1
A
00:00
0
1
A
00:30
1
1
A
01:00
2
1
A
01:30
3
1
A
02:00
4
1
A
02:30
5
1
A
03:00
6
1
A
03:30
0
1
A
04:00
1
1
A
04:30
2
1
A
05:00
3
1
A
05:30
4
1
A
06:00
5
1
A
06:30
6
1
A
07:00
0
1
A
07:30
1
1
A
08:00
2
1
A
08:30
3
1
A
09:00
4
1
A
09:30
5
1
A
10:00
6
1
A
10:30
0
1
A
11:00
1
1
A
11:30
2
1
A
12:30
4
1
A
13:00
5
1
A
13:30
6
1
A
14:00
0
1
A
14:30
1
1
A
15:00
2
1
A
15:30
3
1
A
16:00
4
1
A
16:30
5
1
A
17:00
6
1
A
17:30
0
1
A
18:00
1
1
A
18:30
2
1
A
19:00
3
1
A
19:30
4
1
A
20:00
5
1
A
20:30
6
1
A
21:00
0
1
A
21:30
1
1
A
22:00
2
1
A
22:30
3
1
A
23:00
4
1
A
23:30
5
1
A
6

basic

interpreter: byte-equal

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Greenhouse
time (HH:MM)> 06:30
temperature (C)> 12.5
Recorded: Greenhouse 06:30 12.5 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Greenhouse
time (HH:MM)> 8:00
temperature (C)> 15
Recorded: Greenhouse 08:00 15.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> greenhouse
time (HH:MM)> 14:00
temperature (C)> 28.0
Recorded: Greenhouse 14:00 28.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Greenhouse
time (HH:MM)> 10:00
temperature (C)> 19
Recorded: Greenhouse 10:00 19.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Freezer
time (HH:MM)> 08:00
temperature (C)> -21
Recorded: Freezer 08:00 -21.0 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Freezer
time (HH:MM)> 14:00
temperature (C)> -12
Recorded: Freezer 14:00 -12.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Freezer
time (HH:MM)> 11:30
temperature (C)> -18.5
Recorded: Freezer 11:30 -18.5 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 2
place> Greenhouse
lowest (C)> 15
highest (C)> 30
Greenhouse's safe range: 15.0 to 30.0 C. 1 of its 4 readings is outside it.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 2
place> Freezer
lowest (C)> -25
highest (C)> -15
Freezer's safe range: -25.0 to -15.0 C. 1 of its 3 readings is outside it.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Greenhouse
time (HH:MM)> 16:30
temperature (C)> 31.5
Recorded: Greenhouse 16:30 31.5 C.
ALERT: 31.5 C is 1.5 above Greenhouse's safe range of 15.0 to 30.0 C.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 3
Greenhouse: 5 readings, 06:30 to 16:30
  lowest 12.5 C at 06:30, highest 31.5 C at 16:30, average 21.2 C
  safe range 15.0 to 30.0 C, 2 readings outside it
Freezer: 3 readings, 08:00 to 14:00
  lowest -21.0 C at 08:00, highest -12.0 C at 14:00, average -17.2 C
  safe range -25.0 to -15.0 C, 1 reading outside it
All places: 8 readings; lowest -21.0 C (Freezer, 08:00), highest 31.5 C (Greenhouse, 16:30).

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 4
place> Greenhouse
window (2 to 5 readings)> 3
Greenhouse, moving average of the last 3 readings:
  06:30   12.5
  08:00   15.0
  10:00   19.0   average 15.5
  14:00   28.0   average 20.7
  16:30   31.5   average 26.2

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 5
Outside the safe ranges:
  Greenhouse 06:30  12.5 C, 2.5 below 15.0
  Greenhouse 16:30  31.5 C, 1.5 above 30.0
  Freezer 14:00  -12.0 C, 3.0 above -15.0
3 of 8 readings at places with a safe range are outside it.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 1
place> Hall
time (HH:MM)> 12:00
temperature (C)> 21.5
Recorded: Hall 12:00 21.5 C - a new place.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 5
Outside the safe ranges:
  Greenhouse 06:30  12.5 C, 2.5 below 15.0
  Greenhouse 16:30  31.5 C, 1.5 above 30.0
  Freezer 14:00  -12.0 C, 3.0 above -15.0
3 of 8 readings at places with a safe range are outside it.
No safe range yet: Hall.

== Temperature log ==
1) add a reading  2) set a safe range  3) summary
4) moving average  5) alerts  6) quit
choice> 6
Bye.
What was typed (51 lines)
1
Greenhouse
06:30
12.5
1
Greenhouse
8:00
15
1
greenhouse
14:00
28.0
1
Greenhouse
10:00
19
1
Freezer
08:00
-21
1
Freezer
14:00
-12
1
Freezer
11:30
-18.5
2
Greenhouse
15
30
2
Freezer
-25
-15
1
Greenhouse
16:30
31.5
3
4
Greenhouse
3
5
1
Hall
12:00
21.5
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
# P026 temperature log: readings from several places over one day, each
# place's lowest, highest and average, a moving average, and alerts for
# readings outside a place's safe range.
import readings
import units

6 => most_places
48 => most_readings

def good_name(s):
    # 1 to 16 letters, digits, spaces or hyphens.
    if s == "" or len(s) > 16:
        return False
    for c in s:
        if not (c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -"):
            return False
    return True

def ask_place(places, must_exist):
    # [index, name]: the index of a known place, or len(places) for a new one
    # (only when must_exist is False); [-1, ""] when the answer is empty,
    # which cancels.
    while True:
        units.trim(input("place> ")) => name
        if name == "":
            return [0 - 1, ""]
        readings.find(places, name) => i
        if not good_name(name):
            "A place name is 1 to 16 letters, digits, spaces or hyphens." ^0
        elif i >= 0:
            return [i, places[i][0]]
        elif must_exist:
            ("There is no place called " + name + " yet; add a reading for it first.") ^0
        elif len(places) >= most_places:
            ("There are already " + str(most_places) + " places; a reading has to go to one of them.") ^0
        else:
            return [len(places), name]

def ask_temperature(prompt):
    # [True, tenths], or [False, 0] when the answer is empty, which cancels.
    while True:
        input(prompt) => answer
        if units.trim(answer) == "":
            return [False, 0]
        units.temperature(answer) => t
        if t[0]:
            return t
        "Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21." ^0

def range_text(place):
    return units.show(place[3]) + " to " + units.show(place[4]) + " C"

def add_reading(places):
    # Returns the places, with a new one if the reading started a place.
    ask_place(places, False) => answer
    answer[0] => i
    if i == 0 - 1:
        "Cancelled." ^0
        return places
    if i < len(places) and len(places[i][1]) >= most_readings:
        (places[i][0] + " already has " + str(most_readings) + " readings, the most a place keeps.") ^0
        return places
    0 - 1 => minute
    while minute == 0 - 1:
        input("time (HH:MM)> ") => typed
        if units.trim(typed) == "":
            "Cancelled." ^0
            return places
        units.minute_of(typed) => minute
        if minute == 0 - 1:
            "Type a time from 00:00 to 23:59, such as 07:30." ^0
        elif i < len(places) and readings.reading_at(places[i][1], minute) >= 0:
            places[i][1][readings.reading_at(places[i][1], minute)] => there
            (places[i][0] + " already has a reading at " + units.clock(minute) + " (" + units.show(there[1]) + " C).") ^0
            0 - 1 => minute
    ask_temperature("temperature (C)> ") => t
    if not t[0]:
        "Cancelled." ^0
        return places
    if i == len(places):
        places + [[answer[1], [[minute, t[1]]], False, 0, 0]] => places
        places[i] => place
        ("Recorded: " + place[0] + " " + units.clock(minute) + " " + units.show(t[1]) + " C - a new place.") ^0
    else:
        places[i] => place
        readings.with_reading(place[1], minute, t[1]) => place[1]
        ("Recorded: " + place[0] + " " + units.clock(minute) + " " + units.show(t[1]) + " C.") ^0
    readings.off_range(place, t[1]) => off
    if off < 0:
        ("ALERT: " + units.show(t[1]) + " C is " + units.show(0 - off) + " below " + place[0] + "'s safe range of " + range_text(place) + ".") ^0
    elif off > 0:
        ("ALERT: " + units.show(t[1]) + " C is " + units.show(off) + " above " + place[0] + "'s safe range of " + range_text(place) + ".") ^0
    return places

def set_range(places):
    if len(places) == 0:
        "No places yet; add a reading first." ^0
        return
    ask_place(places, True)[0] => i
    if i == 0 - 1:
        "Cancelled." ^0
        return
    places[i] => place
    ask_temperature("lowest (C)> ") => low
    if not low[0]:
        "Cancelled." ^0
        return
    [False, 0] => high
    while not high[0]:
        ask_temperature("highest (C)> ") => high
        if not high[0]:
            "Cancelled." ^0
            return
        if high[1] <= low[1]:
            ("The highest has to be above the lowest (" + units.show(low[1]) + " C).") ^0
            [False, 0] => high
    True => place[2]
    low[1] => place[3]
    high[1] => place[4]
    readings.count_outside(place) => n
    len(place[1]) => total
    if n == 0:
        "Every reading so far is inside it." => message
    elif n == total:
        "Every reading so far is outside it." => message
    elif n == 1:
        ("1 of its " + str(total) + " readings is outside it.") => message
    else:
        (str(n) + " of its " + str(total) + " readings are outside it.") => message
    (place[0] + "'s safe range: " + range_text(place) + ". " + message) ^0

def summary(places):
    if len(places) == 0:
        "No readings yet." ^0
        return
    0 => all_count
    for place in places:
        readings.stats(place[1]) => s
        place[1] => rs
        if s[0] == 1:
            (place[0] + ": 1 reading, at " + units.clock(rs[0][0])) ^0
        else:
            (place[0] + ": " + str(s[0]) + " readings, " + units.clock(rs[0][0]) + " to " + units.clock(rs[len(rs) - 1][0])) ^0
        ("  lowest " + units.show(s[1]) + " C at " + units.clock(s[2]) + ", highest " + units.show(s[3]) + " C at " + units.clock(s[4]) + ", average " + units.show(units.average(s[5], s[0])) + " C") ^0
        if place[2]:
            ("  safe range " + range_text(place) + ", " + units.plural(readings.count_outside(place), "reading") + " outside it") ^0
        else:
            "  no safe range" ^0
        all_count + s[0] => all_count
    if len(places) > 1:
        readings.stats(places[0][1]) => first
        first[1] => low
        first[2] => low_at
        places[0][0] => low_place
        first[3] => high
        first[4] => high_at
        places[0][0] => high_place
        for place in places:
            readings.stats(place[1]) => s
            if s[1] < low:
                s[1] => low
                s[2] => low_at
                place[0] => low_place
            if s[3] > high:
                s[3] => high
                s[4] => high_at
                place[0] => high_place
        ("All places: " + str(all_count) + " readings; lowest " + units.show(low) + " C (" + low_place + ", " + units.clock(low_at) + "), highest " + units.show(high) + " C (" + high_place + ", " + units.clock(high_at) + ").") ^0

def moving_average(places):
    if len(places) == 0:
        "No readings yet." ^0
        return
    ask_place(places, True)[0] => i
    if i == 0 - 1:
        "Cancelled." ^0
        return
    places[i] => place
    len(place[1]) => count
    if count < 2:
        ("A moving average needs at least 2 readings; " + place[0] + " has 1.") ^0
        return
    count => widest
    if widest > 10:
        10 => widest
    0 => window
    while window == 0:
        input("window (2 to " + str(widest) + " readings)> ") => typed
        if units.trim(typed) == "":
            "Cancelled." ^0
            return
        units.whole_number(typed) => window
        if window < 2 or window > widest:
            ("Type a whole number from 2 to " + str(widest) + ".") ^0
            0 => window
    (place[0] + ", moving average of the last " + str(window) + " readings:") ^0
    for row in readings.moving(place[1], window):
        ("  " + units.clock(row[0]) + units.pad_left(units.show(row[1]), 7)) => line
        if row[2] != None:
            line + "   average " + units.show(units.average(row[2], window)) => line
        line ^0

def alerts(places):
    if len(places) == 0:
        "No readings yet." ^0
        return
    [] => lines
    0 => checked
    [] => unranged
    for place in places:
        if place[2]:
            for r in place[1]:
                readings.off_range(place, r[1]) => off
                checked + 1 => checked
                if off < 0:
                    lines + ["  " + place[0] + " " + units.clock(r[0]) + "  " + units.show(r[1]) + " C, " + units.show(0 - off) + " below " + units.show(place[3])] => lines
                elif off > 0:
                    lines + ["  " + place[0] + " " + units.clock(r[0]) + "  " + units.show(r[1]) + " C, " + units.show(off) + " above " + units.show(place[4])] => lines
        else:
            unranged + [place[0]] => unranged
    if checked == 0:
        "No place has a safe range yet; set one with 2." ^0
        return
    if len(lines) == 0:
        "Every reading is inside its place's safe range." ^0
    else:
        "Outside the safe ranges:" ^0
        for line in lines:
            line ^0
        if len(lines) == 1:
            ("1 of " + units.plural(checked, "reading") + " at places with a safe range is outside it.") ^0
        else:
            (str(len(lines)) + " of " + units.plural(checked, "reading") + " at places with a safe range are outside it.") ^0
    if len(unranged) > 0:
        ("No safe range yet: " + units.listed(unranged) + ".") ^0

[] => places
True => running
while running:
    "" ^0
    "== Temperature log ==" ^0
    "1) add a reading  2) set a safe range  3) summary" ^0
    "4) moving average  5) alerts  6) quit" ^0
    units.trim(input("choice> ")) => choice
    if choice == "1":
        add_reading(places) => places
    elif choice == "2":
        set_range(places)
    elif choice == "3":
        summary(places)
    elif choice == "4":
        moving_average(places)
    elif choice == "5":
        alerts(places)
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import readings
import units
most_places = 6
most_readings = 48

def good_name(s):
    if s == "" or len(s) > 16:
        return False
    for c in s:
        if not c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -":
            return False
    return True

def ask_place(places, must_exist):
    while True:
        name = units.trim(input("place> "))
        if name == "":
            return [0 - 1, ""]
        i = readings.find(places, name)
        if not good_name(name):
            print("A place name is 1 to 16 letters, digits, spaces or hyphens.")
        elif i >= 0:
            return [i, places[i][0]]
        elif must_exist:
            print("There is no place called " + name + " yet; add a reading for it first.")
        elif len(places) >= most_places:
            print("There are already " + str(most_places) + " places; a reading has to go to one of them.")
        else:
            return [len(places), name]

def ask_temperature(prompt):
    while True:
        answer = input(prompt)
        if units.trim(answer) == "":
            return [False, 0]
        t = units.temperature(answer)
        if t[0]:
            return t
        print("Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.")

def range_text(place):
    return units.show(place[3]) + " to " + units.show(place[4]) + " C"

def add_reading(places):
    answer = ask_place(places, False)
    i = answer[0]
    if i == 0 - 1:
        print("Cancelled.")
        return places
    if i < len(places) and len(places[i][1]) >= most_readings:
        print(places[i][0] + " already has " + str(most_readings) + " readings, the most a place keeps.")
        return places
    minute = 0 - 1
    while minute == 0 - 1:
        typed = input("time (HH:MM)> ")
        if units.trim(typed) == "":
            print("Cancelled.")
            return places
        minute = units.minute_of(typed)
        if minute == 0 - 1:
            print("Type a time from 00:00 to 23:59, such as 07:30.")
        elif i < len(places) and readings.reading_at(places[i][1], minute) >= 0:
            there = places[i][1][readings.reading_at(places[i][1], minute)]
            print(places[i][0] + " already has a reading at " + units.clock(minute) + " (" + units.show(there[1]) + " C).")
            minute = 0 - 1
    t = ask_temperature("temperature (C)> ")
    if not t[0]:
        print("Cancelled.")
        return places
    if i == len(places):
        places = places + [[answer[1], [[minute, t[1]]], False, 0, 0]]
        place = places[i]
        print("Recorded: " + place[0] + " " + units.clock(minute) + " " + units.show(t[1]) + " C - a new place.")
    else:
        place = places[i]
        place[1] = readings.with_reading(place[1], minute, t[1])
        print("Recorded: " + place[0] + " " + units.clock(minute) + " " + units.show(t[1]) + " C.")
    off = readings.off_range(place, t[1])
    if off < 0:
        print("ALERT: " + units.show(t[1]) + " C is " + units.show(0 - off) + " below " + place[0] + "'s safe range of " + range_text(place) + ".")
    elif off > 0:
        print("ALERT: " + units.show(t[1]) + " C is " + units.show(off) + " above " + place[0] + "'s safe range of " + range_text(place) + ".")
    return places

def set_range(places):
    if len(places) == 0:
        print("No places yet; add a reading first.")
        return
    i = ask_place(places, True)[0]
    if i == 0 - 1:
        print("Cancelled.")
        return
    place = places[i]
    low = ask_temperature("lowest (C)> ")
    if not low[0]:
        print("Cancelled.")
        return
    high = [False, 0]
    while not high[0]:
        high = ask_temperature("highest (C)> ")
        if not high[0]:
            print("Cancelled.")
            return
        if high[1] <= low[1]:
            print("The highest has to be above the lowest (" + units.show(low[1]) + " C).")
            high = [False, 0]
    place[2] = True
    place[3] = low[1]
    place[4] = high[1]
    n = readings.count_outside(place)
    total = len(place[1])
    if n == 0:
        message = "Every reading so far is inside it."
    elif n == total:
        message = "Every reading so far is outside it."
    elif n == 1:
        message = "1 of its " + str(total) + " readings is outside it."
    else:
        message = str(n) + " of its " + str(total) + " readings are outside it."
    print(place[0] + "'s safe range: " + range_text(place) + ". " + message)

def summary(places):
    if len(places) == 0:
        print("No readings yet.")
        return
    all_count = 0
    for place in places:
        s = readings.stats(place[1])
        rs = place[1]
        if s[0] == 1:
            print(place[0] + ": 1 reading, at " + units.clock(rs[0][0]))
        else:
            print(place[0] + ": " + str(s[0]) + " readings, " + units.clock(rs[0][0]) + " to " + units.clock(rs[len(rs) - 1][0]))
        print("  lowest " + units.show(s[1]) + " C at " + units.clock(s[2]) + ", highest " + units.show(s[3]) + " C at " + units.clock(s[4]) + ", average " + units.show(units.average(s[5], s[0])) + " C")
        if place[2]:
            print("  safe range " + range_text(place) + ", " + units.plural(readings.count_outside(place), "reading") + " outside it")
        else:
            print("  no safe range")
        all_count = all_count + s[0]
    if len(places) > 1:
        first = readings.stats(places[0][1])
        low = first[1]
        low_at = first[2]
        low_place = places[0][0]
        high = first[3]
        high_at = first[4]
        high_place = places[0][0]
        for place in places:
            s = readings.stats(place[1])
            if s[1] < low:
                low = s[1]
                low_at = s[2]
                low_place = place[0]
            if s[3] > high:
                high = s[3]
                high_at = s[4]
                high_place = place[0]
        print("All places: " + str(all_count) + " readings; lowest " + units.show(low) + " C (" + low_place + ", " + units.clock(low_at) + "), highest " + units.show(high) + " C (" + high_place + ", " + units.clock(high_at) + ").")

def moving_average(places):
    if len(places) == 0:
        print("No readings yet.")
        return
    i = ask_place(places, True)[0]
    if i == 0 - 1:
        print("Cancelled.")
        return
    place = places[i]
    count = len(place[1])
    if count < 2:
        print("A moving average needs at least 2 readings; " + place[0] + " has 1.")
        return
    widest = count
    if widest > 10:
        widest = 10
    window = 0
    while window == 0:
        typed = input("window (2 to " + str(widest) + " readings)> ")
        if units.trim(typed) == "":
            print("Cancelled.")
            return
        window = units.whole_number(typed)
        if window < 2 or window > widest:
            print("Type a whole number from 2 to " + str(widest) + ".")
            window = 0
    print(place[0] + ", moving average of the last " + str(window) + " readings:")
    for row in readings.moving(place[1], window):
        line = "  " + units.clock(row[0]) + units.pad_left(units.show(row[1]), 7)
        if row[2] != None:
            line = line + "   average " + units.show(units.average(row[2], window))
        print(line)

def alerts(places):
    if len(places) == 0:
        print("No readings yet.")
        return
    lines = []
    checked = 0
    unranged = []
    for place in places:
        if place[2]:
            for r in place[1]:
                off = readings.off_range(place, r[1])
                checked = checked + 1
                if off < 0:
                    lines = lines + ["  " + place[0] + " " + units.clock(r[0]) + "  " + units.show(r[1]) + " C, " + units.show(0 - off) + " below " + units.show(place[3])]
                elif off > 0:
                    lines = lines + ["  " + place[0] + " " + units.clock(r[0]) + "  " + units.show(r[1]) + " C, " + units.show(off) + " above " + units.show(place[4])]
        else:
            unranged = unranged + [place[0]]
    if checked == 0:
        print("No place has a safe range yet; set one with 2.")
        return
    if len(lines) == 0:
        print("Every reading is inside its place's safe range.")
    else:
        print("Outside the safe ranges:")
        for line in lines:
            print(line)
        if len(lines) == 1:
            print("1 of " + units.plural(checked, "reading") + " at places with a safe range is outside it.")
        else:
            print(str(len(lines)) + " of " + units.plural(checked, "reading") + " at places with a safe range are outside it.")
    if len(unranged) > 0:
        print("No safe range yet: " + units.listed(unranged) + ".")

places = []
running = True
while running:
    print("")
    print("== Temperature log ==")
    print("1) add a reading  2) set a safe range  3) summary")
    print("4) moving average  5) alerts  6) quit")
    choice = units.trim(input("choice> "))
    if choice == "1":
        places = add_reading(places)
    elif choice == "2":
        set_range(places)
    elif choice == "3":
        summary(places)
    elif choice == "4":
        moving_average(places)
    elif choice == "5":
        alerts(places)
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")

readings.eml

eml
# P026 temperature log - the places and their readings. A place is
# [name, readings, has_range, lowest, highest]; its readings are
# [minute, tenths] pairs kept in time order, one per minute of the day.
import units

def find(places, name):
    # The index of the place with this name, ignoring case; -1 if none.
    units.lower(name) => wanted
    0 => i
    while i < len(places):
        if units.lower(places[i][0]) == wanted:
            return i
        i + 1 => i
    return 0 - 1

def reading_at(readings, minute):
    # The index of the reading taken at this minute; -1 if none.
    0 => i
    while i < len(readings):
        if readings[i][0] == minute:
            return i
        i + 1 => i
    return 0 - 1

def with_reading(readings, minute, tenths):
    # The readings with one more, still in time order.
    0 => k
    while k < len(readings) and readings[k][0] < minute:
        k + 1 => k
    return readings[0:k] + [[minute, tenths]] + readings[k:len(readings)]

def stats(readings):
    # [count, lowest, its minute, highest, its minute, total]; the earliest
    # reading counts when a lowest or highest value comes up again.
    readings[0] => first
    first[1] => low
    first[0] => low_at
    first[1] => high
    first[0] => high_at
    0 => total
    for r in readings:
        if r[1] < low:
            r[1] => low
            r[0] => low_at
        if r[1] > high:
            r[1] => high
            r[0] => high_at
        total + r[1] => total
    return [len(readings), low, low_at, high, high_at, total]

def off_range(place, tenths):
    # How far a reading is outside the place's safe range: below is negative,
    # above is positive, inside (or no range) is 0.
    if not place[2]:
        return 0
    if tenths < place[3]:
        return tenths - place[3]
    if tenths > place[4]:
        return tenths - place[4]
    return 0

def count_outside(place):
    0 => n
    for r in place[1]:
        if off_range(place, r[1]) != 0:
            n + 1 => n
    return n

def moving(readings, window):
    # For each reading [minute, tenths, total of the last `window` readings or
    # None while fewer have come]. The total slides: the entering reading is
    # added and the leaving one taken away, in whole tenths, so it never drifts.
    [] => out
    0 => total
    0 => i
    while i < len(readings):
        total + readings[i][1] => total
        if i >= window:
            total - readings[i - window][1] => total
        if i >= window - 1:
            out + [[readings[i][0], readings[i][1], total]] => out
        else:
            out + [[readings[i][0], readings[i][1], None]] => out
        i + 1 => i
    return out
Python projection (readings.py)
import units

def find(places, name):
    wanted = units.lower(name)
    i = 0
    while i < len(places):
        if units.lower(places[i][0]) == wanted:
            return i
        i = i + 1
    return 0 - 1

def reading_at(readings, minute):
    i = 0
    while i < len(readings):
        if readings[i][0] == minute:
            return i
        i = i + 1
    return 0 - 1

def with_reading(readings, minute, tenths):
    k = 0
    while k < len(readings) and readings[k][0] < minute:
        k = k + 1
    return readings[0:k] + [[minute, tenths]] + readings[k:len(readings)]

def stats(readings):
    first = readings[0]
    low = first[1]
    low_at = first[0]
    high = first[1]
    high_at = first[0]
    total = 0
    for r in readings:
        if r[1] < low:
            low = r[1]
            low_at = r[0]
        if r[1] > high:
            high = r[1]
            high_at = r[0]
        total = total + r[1]
    return [len(readings), low, low_at, high, high_at, total]

def off_range(place, tenths):
    if not place[2]:
        return 0
    if tenths < place[3]:
        return tenths - place[3]
    if tenths > place[4]:
        return tenths - place[4]
    return 0

def count_outside(place):
    n = 0
    for r in place[1]:
        if off_range(place, r[1]) != 0:
            n = n + 1
    return n

def moving(readings, window):
    out = []
    total = 0
    i = 0
    while i < len(readings):
        total = total + readings[i][1]
        if i >= window:
            total = total - readings[i - window][1]
        if i >= window - 1:
            out = out + [[readings[i][0], readings[i][1], total]]
        else:
            out = out + [[readings[i][0], readings[i][1], None]]
        i = i + 1
    return out

units.eml

eml
# P026 temperature log - reading and writing temperatures and times.
# Temperatures are kept as whole tenths of a degree (21.5 C is 215), so sums
# and averages are exact; only the display rounds. The interpreter that checks
# every session does not run string methods yet, so the text handling is
# written out here.

"0123456789" => digits

def trim(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 lower(s):
    # s with A-Z as a-z.
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper
    "abcdefghijklmnopqrstuvwxyz" => small
    "" => out
    for c in s:
        0 => k
        while k < 26 and upper[k] != c:
            k + 1 => k
        if k < 26:
            out + small[k] => out
        else:
            out + c => out
    return out

def quotient(a, b):
    # a divided by b, rounded down, for a >= 0 and b > 0.
    return int((a - a % b) / b)

def average(total, count):
    # total / count rounded to a whole number, halves away from zero.
    if total < 0:
        return 0 - quotient(0 - 2 * total + count, 2 * count)
    return quotient(2 * total + count, 2 * count)

def show(tenths):
    # 215 as "21.5", -5 as "-0.5", 0 as "0.0".
    "" => sign
    tenths => a
    if a < 0:
        "-" => sign
        0 - a => a
    return sign + str(quotient(a, 10)) + "." + str(a % 10)

def temperature(s):
    # [True, tenths] for "21", "21.5", "-3.5" or "+4" from -60.0 to 60.0 with
    # at most one decimal; [False, 0] for anything else.
    trim(s) => s
    1 => sign
    if s != "" and (s[0] == "-" or s[0] == "+"):
        if s[0] == "-":
            0 - 1 => sign
        s[1:len(s)] => s
    0 => whole
    0 => n
    while n < len(s) and s[n] in digits:
        whole * 10 + int(s[n]) => whole
        n + 1 => n
    if n == 0 or n > 2:
        return [False, 0]
    0 => tenth
    if n < len(s):
        if s[n] != "." or n + 2 != len(s) or not (s[n + 1] in digits):
            return [False, 0]
        int(s[n + 1]) => tenth
    sign * (whole * 10 + tenth) => t
    if t < 0 - 600 or t > 600:
        return [False, 0]
    return [True, t]

def minute_of(s):
    # Minutes after midnight for "7:05" or "07:05" (00:00 to 23:59), else -1.
    trim(s) => s
    0 => colon
    while colon < len(s) and s[colon] != ":":
        colon + 1 => colon
    if colon == 0 or colon > 2 or colon + 3 != len(s):
        return 0 - 1
    0 => h
    for c in s[0:colon]:
        if not (c in digits):
            return 0 - 1
        h * 10 + int(c) => h
    if not (s[colon + 1] in digits and s[colon + 2] in digits):
        return 0 - 1
    int(s[colon + 1]) * 10 + int(s[colon + 2]) => m
    if h > 23 or m > 59:
        return 0 - 1
    return h * 60 + m

def clock(minute):
    # 425 as "07:05".
    quotient(minute, 60) => h
    minute % 60 => m
    "" => out
    if h < 10:
        "0" => out
    out + str(h) + ":" => out
    if m < 10:
        out + "0" => out
    return out + str(m)

def pad_left(s, width):
    while len(s) < width:
        " " + s => s
    return s

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

def whole_number(s):
    # The value of 1 to 3 digits; -1 for anything else.
    trim(s) => s
    if s == "" or len(s) > 3:
        return 0 - 1
    0 => n
    for c in s:
        if not (c in digits):
            return 0 - 1
        n * 10 + int(c) => n
    return n

def listed(items):
    # ["a", "b", "c"] as "a, b and c".
    "" => out
    0 => i
    while i < len(items):
        if i > 0 and i == len(items) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + items[i] => out
        i + 1 => i
    return out
Python projection (units.py)
digits = "0123456789"

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 lower(s):
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    small = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        k = 0
        while k < 26 and upper[k] != c:
            k = k + 1
        if k < 26:
            out = out + small[k]
        else:
            out = out + c
    return out

def quotient(a, b):
    return int((a - a % b) / b)

def average(total, count):
    if total < 0:
        return 0 - quotient(0 - 2 * total + count, 2 * count)
    return quotient(2 * total + count, 2 * count)

def show(tenths):
    sign = ""
    a = tenths
    if a < 0:
        sign = "-"
        a = 0 - a
    return sign + str(quotient(a, 10)) + "." + str(a % 10)

def temperature(s):
    s = trim(s)
    sign = 1
    if s != "" and (s[0] == "-" or s[0] == "+"):
        if s[0] == "-":
            sign = 0 - 1
        s = s[1:len(s)]
    whole = 0
    n = 0
    while n < len(s) and s[n] in digits:
        whole = whole * 10 + int(s[n])
        n = n + 1
    if n == 0 or n > 2:
        return [False, 0]
    tenth = 0
    if n < len(s):
        if s[n] != "." or n + 2 != len(s) or not s[n + 1] in digits:
            return [False, 0]
        tenth = int(s[n + 1])
    t = sign * (whole * 10 + tenth)
    if t < 0 - 600 or t > 600:
        return [False, 0]
    return [True, t]

def minute_of(s):
    s = trim(s)
    colon = 0
    while colon < len(s) and s[colon] != ":":
        colon = colon + 1
    if colon == 0 or colon > 2 or colon + 3 != len(s):
        return 0 - 1
    h = 0
    for c in s[0:colon]:
        if not c in digits:
            return 0 - 1
        h = h * 10 + int(c)
    if not (s[colon + 1] in digits and s[colon + 2] in digits):
        return 0 - 1
    m = int(s[colon + 1]) * 10 + int(s[colon + 2])
    if h > 23 or m > 59:
        return 0 - 1
    return h * 60 + m

def clock(minute):
    h = quotient(minute, 60)
    m = minute % 60
    out = ""
    if h < 10:
        out = "0"
    out = out + str(h) + ":"
    if m < 10:
        out = out + "0"
    return out + str(m)

def pad_left(s, width):
    while len(s) < width:
        s = " " + s
    return s

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

def whole_number(s):
    s = trim(s)
    if s == "" or len(s) > 3:
        return 0 - 1
    n = 0
    for c in s:
        if not c in digits:
            return 0 - 1
        n = n * 10 + int(c)
    return n

def listed(items):
    out = ""
    i = 0
    while i < len(items):
        if i > 0 and i == len(items) - 1:
            out = out + " and "
        elif i > 0:
            out = out + ", "
        out = out + items[i]
        i = i + 1
    return out

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.