Sensor logger
Temperature readings kept in a ring buffer of the last N, shown slot by slot with where the next write goes and how many were overwritten, and the average, lowest and highest over a window sliding along them, the extremes by a monotonic queue.
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
Temperature readings kept in a ring buffer of the last N. The program shows the buffer slot by slot, with where the next write goes and how many readings have been overwritten. It also gives the average, lowest and highest over a window sliding along the readings. It starts with ten readings in a buffer of eight, so two are already gone.
main.eml- the menu, reading temperatures, the buffer and the window tablelogger.eml- the ring buffer, its readings in order, window sums and window extremes
How each part works:
- Readings are whole tenths of a degree (21.5 is 215), so sums and averages are exact.
- The ring buffer writes at the next slot and moves on, wrapping round to slot 0. Once it is full, that slot held the oldest reading, which is overwritten. As in the corpus case
ring-buffer-bounds-report, every index stays inside 0 to N - 1, so nothing can be read or written out of range. The oldest reading sits where the next write will go. - Windows, as in the corpus case
sliding-window-slices: only full windows, len - k + 1 of them. Each window's sum adds the reading coming in and takes off the one going out. - The lowest and highest come from a queue of positions whose readings only rise (or only fall) from front to back. A new reading removes from the back everything it beats, and the front leaves when it slides out of the window. Each reading enters once and leaves at most once, so this is one pass, whatever the window size.
What is checked: menu choices 1 to 5; 1 to 20 readings at a time, each from -50.0 to 60.0 with at most one decimal; a window from 1 to the number of readings held; a capacity from 2 to 12. An empty answer cancels.
Sessions: sessions/basic.in gives: - the starting buffer, wrapped round, and windows of 3; - two more readings overwriting the two oldest; the one window of 8; - a new buffer of 4: three readings, then two more, the second overwriting the oldest; windows of 2.
sessions/bad-input.in gives: - menu choices 0 and x; - the readings abc, 21.55, 70 and -50.1; - window sizes 0, 9 and x; capacities 13 and 1; then cancels.
Built on the verified corpus cases ring-buffer-bounds-report (a fixed buffer whose indices never leave it) and sliding-window-slices (full windows, counted at both ends).
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== Sensor logger ==
The last readings in a ring buffer, and a window sliding along them.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 0
Pick a number from 1 to 5.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> x
Pick a number from 1 to 5.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> abc
Type 1 to 20 readings from -50.0 to 60.0, at most one decimal each.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> 21.55
Type 1 to 20 readings from -50.0 to 60.0, at most one decimal each.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> 70
Type 1 to 20 readings from -50.0 to 60.0, at most one decimal each.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> -50.1
Type 1 to 20 readings from -50.0 to 60.0, at most one decimal each.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)>
Cancelled.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 3
window size (1 to 8)> 0
Type a whole number from 1 to 8.
window size (1 to 8)> 9
Type a whole number from 1 to 8.
window size (1 to 8)> x
Type a whole number from 1 to 8.
window size (1 to 8)>
Cancelled.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 4
capacity (2 to 12)> 13
Type a whole number from 2 to 12.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 4
capacity (2 to 12)> 1
Type a whole number from 2 to 12.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 4
capacity (2 to 12)>
Cancelled.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 5
Bye.
What was typed (24 lines)
0
x
1
abc
1
21.55
1
70
1
-50.1
1
3
0
9
x
4
13
4
1
4
5
basic
interpreter: byte-equal== Sensor logger ==
The last readings in a ring buffer, and a window sliding along them.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 2
slot 20.8 20.1 21.4 22.1 23.0 22.6 21.9 21.2
^ next write
Holding 8 of 8; 2 overwritten so far.
Oldest to newest: 21.4 22.1 23.0 22.6 21.9 21.2 20.8 20.1
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 3
window size (1 to 8)> 3
readings average lowest highest
1 to 3 22.17 21.4 23.0
2 to 4 22.57 22.1 23.0
3 to 5 22.50 21.9 23.0
4 to 6 21.90 21.2 22.6
5 to 7 21.30 20.8 21.9
6 to 8 20.70 20.1 21.2
6 windows of 3 in 8 readings: each sum adds the reading coming in and drops the one going out.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> 19.5 -2.3
Logged 2; the 2 oldest readings were overwritten to make room.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 2
slot 20.8 20.1 19.5 -2.3 23.0 22.6 21.9 21.2
^ next write
Holding 8 of 8; 4 overwritten so far.
Oldest to newest: 23.0 22.6 21.9 21.2 20.8 20.1 19.5 -2.3
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 3
window size (1 to 8)> 8
readings average lowest highest
1 to 8 18.35 -2.3 23.0
1 window of 8 in 8 readings: it is all of them.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 4
capacity (2 to 12)> 4
A new, empty buffer of 4.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 2
slot - - - -
^ next write
Holding 0 of 4; 0 overwritten so far.
Nothing logged yet.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> 1 2 3
Logged 3; nothing had to be overwritten.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 1
readings (up to 20, like 21.5 22 -3.4)> 4.5 5
Logged 2; the oldest reading was overwritten to make room.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 2
slot 5.0 2.0 3.0 4.5
^ next write
Holding 4 of 4; 1 overwritten so far.
Oldest to newest: 2.0 3.0 4.5 5.0
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 3
window size (1 to 4)> 2
readings average lowest highest
1 to 2 2.50 2.0 3.0
2 to 3 3.75 3.0 4.5
3 to 4 4.75 4.5 5.0
3 windows of 2 in 4 readings: each sum adds the reading coming in and drops the one going out.
1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit
choice> 5
Bye.
What was typed (19 lines)
2
3
3
1
19.5 -2.3
2
3
8
4
4
2
1
1 2 3
1
4.5 5
2
3
2
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# P095 sensor logger: temperature readings kept in a ring buffer of the
# last N, the slots and where the next one goes, and the average, lowest
# and highest over a window sliding along what is kept.
import logger
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 tenths_of(s):
# "21.5", "-3", "0.4" as whole tenths, or [] if not a reading from
# -50.0 to 60.0 with at most one decimal.
s => t
1 => sg
if len(t) > 1 and t[0] == "-":
t[1:len(t)] => t
-1 => sg
0 => k
while k < len(t) and t[k] != ".":
k + 1 => k
t[0:k] => whole
"0" => frac
if k < len(t):
t[k + 1:len(t)] => frac
if len(whole) == 0 or len(whole) > 2 or len(frac) != 1:
return []
0 => n
for c in whole + frac:
if not (c >= "0" and c <= "9"):
return []
n * 10 + int(c) => n
sg * n => v
if v < -500 or v > 600:
return []
return [v]
def show(v):
if v < 0:
return "-" + show(0 - v)
return str(int((v - v % 10) / 10)) + "." + str(v % 10)
def average(s, k):
# s tenths over k readings, in degrees to two places, halves away from 0.
if s < 0:
return "-" + average(0 - s, k)
# hundredths: s / k tenths x 10 = 10 s / k
20 * s + k => n
2 * k => d
0 => q
while n >= d:
n - d => n
q + 1 => q
"" => z
if q % 100 < 10:
"0" => z
return str(int((q - q % 100) / 100)) + "." + z + str(q % 100)
def lpad(s, n):
while len(s) < n:
" " + s => s
return s
def add(r):
plain(input("readings (up to 20, like 21.5 22 -3.4)> ")) => s
if s == "":
"Cancelled." ^0
return r
[] => vs
"" => w
True => ok
for c in s + " ":
if c == " ":
if w != "":
tenths_of(w) => v
if len(v) == 0:
False => ok
else:
vs + v => vs
"" => w
else:
w + c => w
if not ok or len(vs) == 0 or len(vs) > 20:
"Type 1 to 20 readings from -50.0 to 60.0, at most one decimal each." ^0
return r
r[3] => before
for v in vs:
logger.put(r, v) => r
r[3] - before => gone
if gone == 0:
("Logged " + str(len(vs)) + "; nothing had to be overwritten.") ^0
elif gone == 1:
("Logged " + str(len(vs)) + "; the oldest reading was overwritten to make room.") ^0
else:
("Logged " + str(len(vs)) + "; the " + str(gone) + " oldest readings were overwritten to make room.") ^0
return r
def buffer(r):
r[0] => slots
"" => line
for i in [0:len(slots) - 1]:
" -" => cell
if i < r[2] or r[2] == len(slots):
lpad(show(slots[i]), 6) => cell
line + cell => line
(" slot " + line) ^0
"" => arrow
for i in [0:len(slots) - 1]:
if i == r[1]:
arrow + " ^" => arrow
else:
arrow + " " => arrow
(" " + logger_trim(arrow) + " next write") ^0
("Holding " + str(r[2]) + " of " + str(len(slots)) + "; " + str(r[3]) + " overwritten so far.") ^0
"" => seq
for v in logger.in_order(r):
if seq != "":
seq + " " => seq
seq + show(v) => seq
if seq == "":
"Nothing logged yet." ^0
else:
("Oldest to newest: " + seq) ^0
def logger_trim(s):
len(s) => j
while j > 0 and s[j - 1] == " ":
j - 1 => j
return s[0:j]
def windows(r):
logger.in_order(r) => xs
if len(xs) == 0:
"Nothing logged yet." ^0
return
while True:
plain(input("window size (1 to " + str(len(xs)) + ")> ")) => s
if s == "":
"Cancelled." ^0
return
0 => k
True => ok
if len(s) > 2:
False => ok
for c in s:
if c >= "0" and c <= "9":
k * 10 + int(c) => k
else:
False => ok
if ok and k >= 1 and k <= len(xs):
logger.window_sums(xs, k) => sums
logger.window_extremes(xs, k, False) => lows
logger.window_extremes(xs, k, True) => highs
" readings average lowest highest" ^0
for i in [0:len(sums) - 1]:
(" " + lpad(str(i + 1), 2) + " to " + lpad(str(i + k), 2) + lpad(average(sums[i], k), 10) + lpad(show(lows[i]), 9) + lpad(show(highs[i]), 9)) ^0
if len(sums) == 1:
("1 window of " + str(k) + " in " + str(len(xs)) + " readings: it is all of them.") ^0
else:
(str(len(sums)) + " windows of " + str(k) + " in " + str(len(xs)) + " readings: each sum adds the reading coming in and drops the one going out.") ^0
return
("Type a whole number from 1 to " + str(len(xs)) + ".") ^0
def resize(r):
plain(input("capacity (2 to 12)> ")) => s
if s == "":
"Cancelled." ^0
return r
if s == "2" or s == "3" or s == "4" or s == "5" or s == "6" or s == "7" or s == "8" or s == "9" or s == "10" or s == "11" or s == "12":
("A new, empty buffer of " + s + ".") ^0
return logger.ring(int(s))
"Type a whole number from 2 to 12." ^0
return r
logger.ring(8) => r
for v in [205, 210, 214, 221, 230, 226, 219, 212, 208, 201]:
logger.put(r, v) => r
"== Sensor logger ==" ^0
"The last readings in a ring buffer, and a window sliding along them." ^0
True => running
while running:
"" ^0
"1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
add(r) => r
elif choice == "2":
buffer(r)
elif choice == "3":
windows(r)
elif choice == "4":
resize(r) => r
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import logger
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 tenths_of(s):
t = s
sg = 1
if len(t) > 1 and t[0] == "-":
t = t[1:len(t)]
sg = -1
k = 0
while k < len(t) and t[k] != ".":
k = k + 1
whole = t[0:k]
frac = "0"
if k < len(t):
frac = t[k + 1:len(t)]
if len(whole) == 0 or len(whole) > 2 or len(frac) != 1:
return []
n = 0
for c in whole + frac:
if not (c >= "0" and c <= "9"):
return []
n = n * 10 + int(c)
v = sg * n
if v < -500 or v > 600:
return []
return [v]
def show(v):
if v < 0:
return "-" + show(0 - v)
return str(int((v - v % 10) / 10)) + "." + str(v % 10)
def average(s, k):
if s < 0:
return "-" + average(0 - s, k)
n = 20 * s + k
d = 2 * k
q = 0
while n >= d:
n = n - d
q = q + 1
z = ""
if q % 100 < 10:
z = "0"
return str(int((q - q % 100) / 100)) + "." + z + str(q % 100)
def lpad(s, n):
while len(s) < n:
s = " " + s
return s
def add(r):
s = plain(input("readings (up to 20, like 21.5 22 -3.4)> "))
if s == "":
print("Cancelled.")
return r
vs = []
w = ""
ok = True
for c in s + " ":
if c == " ":
if w != "":
v = tenths_of(w)
if len(v) == 0:
ok = False
else:
vs = vs + v
w = ""
else:
w = w + c
if not ok or len(vs) == 0 or len(vs) > 20:
print("Type 1 to 20 readings from -50.0 to 60.0, at most one decimal each.")
return r
before = r[3]
for v in vs:
r = logger.put(r, v)
gone = r[3] - before
if gone == 0:
print("Logged " + str(len(vs)) + "; nothing had to be overwritten.")
elif gone == 1:
print("Logged " + str(len(vs)) + "; the oldest reading was overwritten to make room.")
else:
print("Logged " + str(len(vs)) + "; the " + str(gone) + " oldest readings were overwritten to make room.")
return r
def buffer(r):
slots = r[0]
line = ""
for i in range(0, len(slots)):
cell = " -"
if i < r[2] or r[2] == len(slots):
cell = lpad(show(slots[i]), 6)
line = line + cell
print(" slot " + line)
arrow = ""
for i in range(0, len(slots)):
if i == r[1]:
arrow = arrow + " ^"
else:
arrow = arrow + " "
print(" " + logger_trim(arrow) + " next write")
print("Holding " + str(r[2]) + " of " + str(len(slots)) + "; " + str(r[3]) + " overwritten so far.")
seq = ""
for v in logger.in_order(r):
if seq != "":
seq = seq + " "
seq = seq + show(v)
if seq == "":
print("Nothing logged yet.")
else:
print("Oldest to newest: " + seq)
def logger_trim(s):
j = len(s)
while j > 0 and s[j - 1] == " ":
j = j - 1
return s[0:j]
def windows(r):
xs = logger.in_order(r)
if len(xs) == 0:
print("Nothing logged yet.")
return
while True:
s = plain(input("window size (1 to " + str(len(xs)) + ")> "))
if s == "":
print("Cancelled.")
return
k = 0
ok = True
if len(s) > 2:
ok = False
for c in s:
if c >= "0" and c <= "9":
k = k * 10 + int(c)
else:
ok = False
if ok and k >= 1 and k <= len(xs):
sums = logger.window_sums(xs, k)
lows = logger.window_extremes(xs, k, False)
highs = logger.window_extremes(xs, k, True)
print(" readings average lowest highest")
for i in range(0, len(sums)):
print(" " + lpad(str(i + 1), 2) + " to " + lpad(str(i + k), 2) + lpad(average(sums[i], k), 10) + lpad(show(lows[i]), 9) + lpad(show(highs[i]), 9))
if len(sums) == 1:
print("1 window of " + str(k) + " in " + str(len(xs)) + " readings: it is all of them.")
else:
print(str(len(sums)) + " windows of " + str(k) + " in " + str(len(xs)) + " readings: each sum adds the reading coming in and drops the one going out.")
return
print("Type a whole number from 1 to " + str(len(xs)) + ".")
def resize(r):
s = plain(input("capacity (2 to 12)> "))
if s == "":
print("Cancelled.")
return r
if s == "2" or s == "3" or s == "4" or s == "5" or s == "6" or s == "7" or s == "8" or s == "9" or s == "10" or s == "11" or s == "12":
print("A new, empty buffer of " + s + ".")
return logger.ring(int(s))
print("Type a whole number from 2 to 12.")
return r
r = logger.ring(8)
for v in [205, 210, 214, 221, 230, 226, 219, 212, 208, 201]:
r = logger.put(r, v)
print("== Sensor logger ==")
print("The last readings in a ring buffer, and a window sliding along them.")
running = True
while running:
print("")
print("1) add readings 2) the buffer 3) window statistics 4) new capacity 5) quit")
choice = plain(input("choice> "))
if choice == "1":
r = add(r)
elif choice == "2":
buffer(r)
elif choice == "3":
windows(r)
elif choice == "4":
r = resize(r)
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
logger.eml
eml# P095 sensor logger - readings in a ring buffer that keeps the last N and
# overwrites the oldest, and statistics over a window sliding along them.
# Readings are whole tenths of a degree (21.5 is 215). A buffer is
# [slots, next slot to write, how many are held, how many overwritten].
def ring(n):
return [[0] * n, 0, 0, 0]
def put(r, value):
# Write at the next slot and move it on, wrapping round; when the
# buffer is full that slot held the oldest reading, which is lost. As in
# the corpus case ring-buffer-bounds-report, every index stays inside
# 0 .. N-1, so no read or write can go out of range.
r[0] => slots
len(slots) => n
value => slots[r[1]]
(r[1] + 1) % n => nxt
r[2] => held
r[3] => lost
if held < n:
held + 1 => held
else:
lost + 1 => lost
return [slots, nxt, held, lost]
def in_order(r):
# The held readings, oldest first: they start where the next write goes
# once the buffer is full, at slot 0 before that.
r[0] => slots
len(slots) => n
0 => start
if r[2] == n:
r[1] => start
[0] * r[2] => out
for k in [0:r[2] - 1]:
slots[(start + k) % n] => out[k]
return out
def window_sums(xs, k):
# The sum of every full window of k in a row - as in the corpus case
# sliding-window-slices, exactly len(xs) - k + 1 of them - by adding the
# reading coming in and taking off the one going out.
[] => out
if k > len(xs):
return out
0 => s
for i in [0:k - 1]:
s + xs[i] => s
out + [s] => out
for i in [k:len(xs) - 1]:
s + xs[i] - xs[i - k] => s
out + [s] => out
return out
def window_extremes(xs, k, biggest):
# The largest (or smallest) of every full window, in one pass: a queue
# of positions whose readings only fall (or only rise) from front to
# back. A newcomer removes everything it beats from the back; the front
# leaves when it slides out of the window. Each reading goes in once and
# out at most once.
[] => out
[0] * len(xs) => q
0 => head
0 => tail
for i in [0:len(xs) - 1]:
True => going
while going and tail > head:
q[tail - 1] => j
if (biggest and xs[j] <= xs[i]) or (not biggest and xs[j] >= xs[i]):
tail - 1 => tail
else:
False => going
i => q[tail]
tail + 1 => tail
if q[head] <= i - k:
head + 1 => head
if i >= k - 1:
out + [xs[q[head]]] => out
return out
Python projection (logger.py)
def ring(n):
return [[0] * n, 0, 0, 0]
def put(r, value):
slots = r[0]
n = len(slots)
slots[r[1]] = value
nxt = (r[1] + 1) % n
held = r[2]
lost = r[3]
if held < n:
held = held + 1
else:
lost = lost + 1
return [slots, nxt, held, lost]
def in_order(r):
slots = r[0]
n = len(slots)
start = 0
if r[2] == n:
start = r[1]
out = [0] * r[2]
for k in range(0, r[2]):
out[k] = slots[(start + k) % n]
return out
def window_sums(xs, k):
out = []
if k > len(xs):
return out
s = 0
for i in range(0, k):
s = s + xs[i]
out = out + [s]
for i in range(k, len(xs)):
s = s + xs[i] - xs[i - k]
out = out + [s]
return out
def window_extremes(xs, k, biggest):
out = []
q = [0] * len(xs)
head = 0
tail = 0
for i in range(0, len(xs)):
going = True
while going and tail > head:
j = q[tail - 1]
if biggest and xs[j] <= xs[i] or not biggest and xs[j] >= xs[i]:
tail = tail - 1
else:
going = False
q[tail] = i
tail = tail + 1
if q[head] <= i - k:
head = head + 1
if i >= k - 1:
out = out + [xs[q[head]]]
return out