{
  "id": "P026",
  "slug": "temperature-log",
  "key": "P026-temperature-log",
  "title": "Temperature log",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P026 - Temperature log\n\nA day's temperature readings from up to six places. For each place it shows\nthe lowest, the highest and the average, a moving average over 2 to 10\nreadings, and alerts for readings outside the place's safe range: one as soon\nas such a reading comes in, and a list of all of them on request.\n\n- `main.eml` - the menu, the questions and their checks, and what the screen\n  shows\n- `readings.eml` - the places and their readings: finding a place, keeping\n  readings in time order, lowest/highest/total, how far a reading is outside\n  a safe range, and the moving totals\n- `units.eml` - reading and writing temperatures and times, the rounded\n  average, lower case, padding, lists in words\n\nHow each part works:\n\n- Temperatures are kept as whole tenths of a degree: 21.5 C is 215. Totals\n  and moving totals are therefore exact, and only the display rounds: an\n  average is the total over the count, rounded to a tenth with halves away\n  from zero (-171.67 tenths is -17.2 C).\n- Readings are kept in time order, whatever order they are typed in; a place\n  has at most one reading per minute of the day.\n- The moving average slides: each new reading is added to the running total\n  and the reading that leaves the window is taken away. With floating point\n  that running total would drift away from a fresh sum; in whole tenths it is\n  always exactly the sum of the window.\n- The lowest and the highest are the earliest when a value comes up again,\n  and across places the first place added wins a tie.\n- A safe range runs from a lowest to a highest temperature, both allowed. A\n  reading below or above it is reported with how far outside it is.\n\nWhat is checked: a place name is 1 to 16 letters, digits, spaces or hyphens,\nmatched without regard to case; there are at most 6 places and 48 readings a\nplace; a time is H:MM or HH:MM from 00:00 to 23:59, not already taken at that\nplace; a temperature is -60.0 to 60.0 with at most one decimal; a safe range's\nhighest is above its lowest; a window is 2 to 10 readings and no more than\nthe place has. An empty answer cancels.\n\nSessions: `sessions/basic.in` logs a greenhouse (one reading typed out of\norder, one place name typed in lower case) and a freezer, sets both safe\nranges, gets an alert at 31.5 C, and shows the summary, the moving average of\n3 and the alerts, before and after a third place without a range;\n`sessions/bad-input.in` asks for a summary, a moving average and alerts with\nno readings, then types a name with `#`, times `25:00` and `7:5`,\ntemperatures `abc`, `61`, `-60.05` and `21.25`, a reading at a time that is\ntaken, a moving average with one reading, a place that does not exist, a\nsafe range whose highest is not above its lowest, a seventh place, windows\n`1` and `x`, and a place's 49th reading.\n\nBuilt on the verified corpus cases `temperature-alert-system` (readings\ncompared with thresholds to raise alerts) and `moving-average` (a sliding\ntotal, and why a running float total drifts from a fresh sum).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P026 temperature log: readings from several places over one day, each\n# place's lowest, highest and average, a moving average, and alerts for\n# readings outside a place's safe range.\nimport readings\nimport units\n\n6 => most_places\n48 => most_readings\n\ndef good_name(s):\n    # 1 to 16 letters, digits, spaces or hyphens.\n    if s == \"\" or len(s) > 16:\n        return False\n    for c in s:\n        if not (c in \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -\"):\n            return False\n    return True\n\ndef ask_place(places, must_exist):\n    # [index, name]: the index of a known place, or len(places) for a new one\n    # (only when must_exist is False); [-1, \"\"] when the answer is empty,\n    # which cancels.\n    while True:\n        units.trim(input(\"place> \")) => name\n        if name == \"\":\n            return [0 - 1, \"\"]\n        readings.find(places, name) => i\n        if not good_name(name):\n            \"A place name is 1 to 16 letters, digits, spaces or hyphens.\" ^0\n        elif i >= 0:\n            return [i, places[i][0]]\n        elif must_exist:\n            (\"There is no place called \" + name + \" yet; add a reading for it first.\") ^0\n        elif len(places) >= most_places:\n            (\"There are already \" + str(most_places) + \" places; a reading has to go to one of them.\") ^0\n        else:\n            return [len(places), name]\n\ndef ask_temperature(prompt):\n    # [True, tenths], or [False, 0] when the answer is empty, which cancels.\n    while True:\n        input(prompt) => answer\n        if units.trim(answer) == \"\":\n            return [False, 0]\n        units.temperature(answer) => t\n        if t[0]:\n            return t\n        \"Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.\" ^0\n\ndef range_text(place):\n    return units.show(place[3]) + \" to \" + units.show(place[4]) + \" C\"\n\ndef add_reading(places):\n    # Returns the places, with a new one if the reading started a place.\n    ask_place(places, False) => answer\n    answer[0] => i\n    if i == 0 - 1:\n        \"Cancelled.\" ^0\n        return places\n    if i < len(places) and len(places[i][1]) >= most_readings:\n        (places[i][0] + \" already has \" + str(most_readings) + \" readings, the most a place keeps.\") ^0\n        return places\n    0 - 1 => minute\n    while minute == 0 - 1:\n        input(\"time (HH:MM)> \") => typed\n        if units.trim(typed) == \"\":\n            \"Cancelled.\" ^0\n            return places\n        units.minute_of(typed) => minute\n        if minute == 0 - 1:\n            \"Type a time from 00:00 to 23:59, such as 07:30.\" ^0\n        elif i < len(places) and readings.reading_at(places[i][1], minute) >= 0:\n            places[i][1][readings.reading_at(places[i][1], minute)] => there\n            (places[i][0] + \" already has a reading at \" + units.clock(minute) + \" (\" + units.show(there[1]) + \" C).\") ^0\n            0 - 1 => minute\n    ask_temperature(\"temperature (C)> \") => t\n    if not t[0]:\n        \"Cancelled.\" ^0\n        return places\n    if i == len(places):\n        places + [[answer[1], [[minute, t[1]]], False, 0, 0]] => places\n        places[i] => place\n        (\"Recorded: \" + place[0] + \" \" + units.clock(minute) + \" \" + units.show(t[1]) + \" C - a new place.\") ^0\n    else:\n        places[i] => place\n        readings.with_reading(place[1], minute, t[1]) => place[1]\n        (\"Recorded: \" + place[0] + \" \" + units.clock(minute) + \" \" + units.show(t[1]) + \" C.\") ^0\n    readings.off_range(place, t[1]) => off\n    if off < 0:\n        (\"ALERT: \" + units.show(t[1]) + \" C is \" + units.show(0 - off) + \" below \" + place[0] + \"'s safe range of \" + range_text(place) + \".\") ^0\n    elif off > 0:\n        (\"ALERT: \" + units.show(t[1]) + \" C is \" + units.show(off) + \" above \" + place[0] + \"'s safe range of \" + range_text(place) + \".\") ^0\n    return places\n\ndef set_range(places):\n    if len(places) == 0:\n        \"No places yet; add a reading first.\" ^0\n        return\n    ask_place(places, True)[0] => i\n    if i == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    places[i] => place\n    ask_temperature(\"lowest (C)> \") => low\n    if not low[0]:\n        \"Cancelled.\" ^0\n        return\n    [False, 0] => high\n    while not high[0]:\n        ask_temperature(\"highest (C)> \") => high\n        if not high[0]:\n            \"Cancelled.\" ^0\n            return\n        if high[1] <= low[1]:\n            (\"The highest has to be above the lowest (\" + units.show(low[1]) + \" C).\") ^0\n            [False, 0] => high\n    True => place[2]\n    low[1] => place[3]\n    high[1] => place[4]\n    readings.count_outside(place) => n\n    len(place[1]) => total\n    if n == 0:\n        \"Every reading so far is inside it.\" => message\n    elif n == total:\n        \"Every reading so far is outside it.\" => message\n    elif n == 1:\n        (\"1 of its \" + str(total) + \" readings is outside it.\") => message\n    else:\n        (str(n) + \" of its \" + str(total) + \" readings are outside it.\") => message\n    (place[0] + \"'s safe range: \" + range_text(place) + \". \" + message) ^0\n\ndef summary(places):\n    if len(places) == 0:\n        \"No readings yet.\" ^0\n        return\n    0 => all_count\n    for place in places:\n        readings.stats(place[1]) => s\n        place[1] => rs\n        if s[0] == 1:\n            (place[0] + \": 1 reading, at \" + units.clock(rs[0][0])) ^0\n        else:\n            (place[0] + \": \" + str(s[0]) + \" readings, \" + units.clock(rs[0][0]) + \" to \" + units.clock(rs[len(rs) - 1][0])) ^0\n        (\"  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\n        if place[2]:\n            (\"  safe range \" + range_text(place) + \", \" + units.plural(readings.count_outside(place), \"reading\") + \" outside it\") ^0\n        else:\n            \"  no safe range\" ^0\n        all_count + s[0] => all_count\n    if len(places) > 1:\n        readings.stats(places[0][1]) => first\n        first[1] => low\n        first[2] => low_at\n        places[0][0] => low_place\n        first[3] => high\n        first[4] => high_at\n        places[0][0] => high_place\n        for place in places:\n            readings.stats(place[1]) => s\n            if s[1] < low:\n                s[1] => low\n                s[2] => low_at\n                place[0] => low_place\n            if s[3] > high:\n                s[3] => high\n                s[4] => high_at\n                place[0] => high_place\n        (\"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\n\ndef moving_average(places):\n    if len(places) == 0:\n        \"No readings yet.\" ^0\n        return\n    ask_place(places, True)[0] => i\n    if i == 0 - 1:\n        \"Cancelled.\" ^0\n        return\n    places[i] => place\n    len(place[1]) => count\n    if count < 2:\n        (\"A moving average needs at least 2 readings; \" + place[0] + \" has 1.\") ^0\n        return\n    count => widest\n    if widest > 10:\n        10 => widest\n    0 => window\n    while window == 0:\n        input(\"window (2 to \" + str(widest) + \" readings)> \") => typed\n        if units.trim(typed) == \"\":\n            \"Cancelled.\" ^0\n            return\n        units.whole_number(typed) => window\n        if window < 2 or window > widest:\n            (\"Type a whole number from 2 to \" + str(widest) + \".\") ^0\n            0 => window\n    (place[0] + \", moving average of the last \" + str(window) + \" readings:\") ^0\n    for row in readings.moving(place[1], window):\n        (\"  \" + units.clock(row[0]) + units.pad_left(units.show(row[1]), 7)) => line\n        if row[2] != None:\n            line + \"   average \" + units.show(units.average(row[2], window)) => line\n        line ^0\n\ndef alerts(places):\n    if len(places) == 0:\n        \"No readings yet.\" ^0\n        return\n    [] => lines\n    0 => checked\n    [] => unranged\n    for place in places:\n        if place[2]:\n            for r in place[1]:\n                readings.off_range(place, r[1]) => off\n                checked + 1 => checked\n                if off < 0:\n                    lines + [\"  \" + place[0] + \" \" + units.clock(r[0]) + \"  \" + units.show(r[1]) + \" C, \" + units.show(0 - off) + \" below \" + units.show(place[3])] => lines\n                elif off > 0:\n                    lines + [\"  \" + place[0] + \" \" + units.clock(r[0]) + \"  \" + units.show(r[1]) + \" C, \" + units.show(off) + \" above \" + units.show(place[4])] => lines\n        else:\n            unranged + [place[0]] => unranged\n    if checked == 0:\n        \"No place has a safe range yet; set one with 2.\" ^0\n        return\n    if len(lines) == 0:\n        \"Every reading is inside its place's safe range.\" ^0\n    else:\n        \"Outside the safe ranges:\" ^0\n        for line in lines:\n            line ^0\n        if len(lines) == 1:\n            (\"1 of \" + units.plural(checked, \"reading\") + \" at places with a safe range is outside it.\") ^0\n        else:\n            (str(len(lines)) + \" of \" + units.plural(checked, \"reading\") + \" at places with a safe range are outside it.\") ^0\n    if len(unranged) > 0:\n        (\"No safe range yet: \" + units.listed(unranged) + \".\") ^0\n\n[] => places\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Temperature log ==\" ^0\n    \"1) add a reading  2) set a safe range  3) summary\" ^0\n    \"4) moving average  5) alerts  6) quit\" ^0\n    units.trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        add_reading(places) => places\n    elif choice == \"2\":\n        set_range(places)\n    elif choice == \"3\":\n        summary(places)\n    elif choice == \"4\":\n        moving_average(places)\n    elif choice == \"5\":\n        alerts(places)\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import readings\nimport units\nmost_places = 6\nmost_readings = 48\n\ndef good_name(s):\n    if s == \"\" or len(s) > 16:\n        return False\n    for c in s:\n        if not c in \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -\":\n            return False\n    return True\n\ndef ask_place(places, must_exist):\n    while True:\n        name = units.trim(input(\"place> \"))\n        if name == \"\":\n            return [0 - 1, \"\"]\n        i = readings.find(places, name)\n        if not good_name(name):\n            print(\"A place name is 1 to 16 letters, digits, spaces or hyphens.\")\n        elif i >= 0:\n            return [i, places[i][0]]\n        elif must_exist:\n            print(\"There is no place called \" + name + \" yet; add a reading for it first.\")\n        elif len(places) >= most_places:\n            print(\"There are already \" + str(most_places) + \" places; a reading has to go to one of them.\")\n        else:\n            return [len(places), name]\n\ndef ask_temperature(prompt):\n    while True:\n        answer = input(prompt)\n        if units.trim(answer) == \"\":\n            return [False, 0]\n        t = units.temperature(answer)\n        if t[0]:\n            return t\n        print(\"Type a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.\")\n\ndef range_text(place):\n    return units.show(place[3]) + \" to \" + units.show(place[4]) + \" C\"\n\ndef add_reading(places):\n    answer = ask_place(places, False)\n    i = answer[0]\n    if i == 0 - 1:\n        print(\"Cancelled.\")\n        return places\n    if i < len(places) and len(places[i][1]) >= most_readings:\n        print(places[i][0] + \" already has \" + str(most_readings) + \" readings, the most a place keeps.\")\n        return places\n    minute = 0 - 1\n    while minute == 0 - 1:\n        typed = input(\"time (HH:MM)> \")\n        if units.trim(typed) == \"\":\n            print(\"Cancelled.\")\n            return places\n        minute = units.minute_of(typed)\n        if minute == 0 - 1:\n            print(\"Type a time from 00:00 to 23:59, such as 07:30.\")\n        elif i < len(places) and readings.reading_at(places[i][1], minute) >= 0:\n            there = places[i][1][readings.reading_at(places[i][1], minute)]\n            print(places[i][0] + \" already has a reading at \" + units.clock(minute) + \" (\" + units.show(there[1]) + \" C).\")\n            minute = 0 - 1\n    t = ask_temperature(\"temperature (C)> \")\n    if not t[0]:\n        print(\"Cancelled.\")\n        return places\n    if i == len(places):\n        places = places + [[answer[1], [[minute, t[1]]], False, 0, 0]]\n        place = places[i]\n        print(\"Recorded: \" + place[0] + \" \" + units.clock(minute) + \" \" + units.show(t[1]) + \" C - a new place.\")\n    else:\n        place = places[i]\n        place[1] = readings.with_reading(place[1], minute, t[1])\n        print(\"Recorded: \" + place[0] + \" \" + units.clock(minute) + \" \" + units.show(t[1]) + \" C.\")\n    off = readings.off_range(place, t[1])\n    if off < 0:\n        print(\"ALERT: \" + units.show(t[1]) + \" C is \" + units.show(0 - off) + \" below \" + place[0] + \"'s safe range of \" + range_text(place) + \".\")\n    elif off > 0:\n        print(\"ALERT: \" + units.show(t[1]) + \" C is \" + units.show(off) + \" above \" + place[0] + \"'s safe range of \" + range_text(place) + \".\")\n    return places\n\ndef set_range(places):\n    if len(places) == 0:\n        print(\"No places yet; add a reading first.\")\n        return\n    i = ask_place(places, True)[0]\n    if i == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    place = places[i]\n    low = ask_temperature(\"lowest (C)> \")\n    if not low[0]:\n        print(\"Cancelled.\")\n        return\n    high = [False, 0]\n    while not high[0]:\n        high = ask_temperature(\"highest (C)> \")\n        if not high[0]:\n            print(\"Cancelled.\")\n            return\n        if high[1] <= low[1]:\n            print(\"The highest has to be above the lowest (\" + units.show(low[1]) + \" C).\")\n            high = [False, 0]\n    place[2] = True\n    place[3] = low[1]\n    place[4] = high[1]\n    n = readings.count_outside(place)\n    total = len(place[1])\n    if n == 0:\n        message = \"Every reading so far is inside it.\"\n    elif n == total:\n        message = \"Every reading so far is outside it.\"\n    elif n == 1:\n        message = \"1 of its \" + str(total) + \" readings is outside it.\"\n    else:\n        message = str(n) + \" of its \" + str(total) + \" readings are outside it.\"\n    print(place[0] + \"'s safe range: \" + range_text(place) + \". \" + message)\n\ndef summary(places):\n    if len(places) == 0:\n        print(\"No readings yet.\")\n        return\n    all_count = 0\n    for place in places:\n        s = readings.stats(place[1])\n        rs = place[1]\n        if s[0] == 1:\n            print(place[0] + \": 1 reading, at \" + units.clock(rs[0][0]))\n        else:\n            print(place[0] + \": \" + str(s[0]) + \" readings, \" + units.clock(rs[0][0]) + \" to \" + units.clock(rs[len(rs) - 1][0]))\n        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\")\n        if place[2]:\n            print(\"  safe range \" + range_text(place) + \", \" + units.plural(readings.count_outside(place), \"reading\") + \" outside it\")\n        else:\n            print(\"  no safe range\")\n        all_count = all_count + s[0]\n    if len(places) > 1:\n        first = readings.stats(places[0][1])\n        low = first[1]\n        low_at = first[2]\n        low_place = places[0][0]\n        high = first[3]\n        high_at = first[4]\n        high_place = places[0][0]\n        for place in places:\n            s = readings.stats(place[1])\n            if s[1] < low:\n                low = s[1]\n                low_at = s[2]\n                low_place = place[0]\n            if s[3] > high:\n                high = s[3]\n                high_at = s[4]\n                high_place = place[0]\n        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) + \").\")\n\ndef moving_average(places):\n    if len(places) == 0:\n        print(\"No readings yet.\")\n        return\n    i = ask_place(places, True)[0]\n    if i == 0 - 1:\n        print(\"Cancelled.\")\n        return\n    place = places[i]\n    count = len(place[1])\n    if count < 2:\n        print(\"A moving average needs at least 2 readings; \" + place[0] + \" has 1.\")\n        return\n    widest = count\n    if widest > 10:\n        widest = 10\n    window = 0\n    while window == 0:\n        typed = input(\"window (2 to \" + str(widest) + \" readings)> \")\n        if units.trim(typed) == \"\":\n            print(\"Cancelled.\")\n            return\n        window = units.whole_number(typed)\n        if window < 2 or window > widest:\n            print(\"Type a whole number from 2 to \" + str(widest) + \".\")\n            window = 0\n    print(place[0] + \", moving average of the last \" + str(window) + \" readings:\")\n    for row in readings.moving(place[1], window):\n        line = \"  \" + units.clock(row[0]) + units.pad_left(units.show(row[1]), 7)\n        if row[2] != None:\n            line = line + \"   average \" + units.show(units.average(row[2], window))\n        print(line)\n\ndef alerts(places):\n    if len(places) == 0:\n        print(\"No readings yet.\")\n        return\n    lines = []\n    checked = 0\n    unranged = []\n    for place in places:\n        if place[2]:\n            for r in place[1]:\n                off = readings.off_range(place, r[1])\n                checked = checked + 1\n                if off < 0:\n                    lines = lines + [\"  \" + place[0] + \" \" + units.clock(r[0]) + \"  \" + units.show(r[1]) + \" C, \" + units.show(0 - off) + \" below \" + units.show(place[3])]\n                elif off > 0:\n                    lines = lines + [\"  \" + place[0] + \" \" + units.clock(r[0]) + \"  \" + units.show(r[1]) + \" C, \" + units.show(off) + \" above \" + units.show(place[4])]\n        else:\n            unranged = unranged + [place[0]]\n    if checked == 0:\n        print(\"No place has a safe range yet; set one with 2.\")\n        return\n    if len(lines) == 0:\n        print(\"Every reading is inside its place's safe range.\")\n    else:\n        print(\"Outside the safe ranges:\")\n        for line in lines:\n            print(line)\n        if len(lines) == 1:\n            print(\"1 of \" + units.plural(checked, \"reading\") + \" at places with a safe range is outside it.\")\n        else:\n            print(str(len(lines)) + \" of \" + units.plural(checked, \"reading\") + \" at places with a safe range are outside it.\")\n    if len(unranged) > 0:\n        print(\"No safe range yet: \" + units.listed(unranged) + \".\")\n\nplaces = []\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Temperature log ==\")\n    print(\"1) add a reading  2) set a safe range  3) summary\")\n    print(\"4) moving average  5) alerts  6) quit\")\n    choice = units.trim(input(\"choice> \"))\n    if choice == \"1\":\n        places = add_reading(places)\n    elif choice == \"2\":\n        set_range(places)\n    elif choice == \"3\":\n        summary(places)\n    elif choice == \"4\":\n        moving_average(places)\n    elif choice == \"5\":\n        alerts(places)\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "readings.eml",
      "eml": "# P026 temperature log - the places and their readings. A place is\n# [name, readings, has_range, lowest, highest]; its readings are\n# [minute, tenths] pairs kept in time order, one per minute of the day.\nimport units\n\ndef find(places, name):\n    # The index of the place with this name, ignoring case; -1 if none.\n    units.lower(name) => wanted\n    0 => i\n    while i < len(places):\n        if units.lower(places[i][0]) == wanted:\n            return i\n        i + 1 => i\n    return 0 - 1\n\ndef reading_at(readings, minute):\n    # The index of the reading taken at this minute; -1 if none.\n    0 => i\n    while i < len(readings):\n        if readings[i][0] == minute:\n            return i\n        i + 1 => i\n    return 0 - 1\n\ndef with_reading(readings, minute, tenths):\n    # The readings with one more, still in time order.\n    0 => k\n    while k < len(readings) and readings[k][0] < minute:\n        k + 1 => k\n    return readings[0:k] + [[minute, tenths]] + readings[k:len(readings)]\n\ndef stats(readings):\n    # [count, lowest, its minute, highest, its minute, total]; the earliest\n    # reading counts when a lowest or highest value comes up again.\n    readings[0] => first\n    first[1] => low\n    first[0] => low_at\n    first[1] => high\n    first[0] => high_at\n    0 => total\n    for r in readings:\n        if r[1] < low:\n            r[1] => low\n            r[0] => low_at\n        if r[1] > high:\n            r[1] => high\n            r[0] => high_at\n        total + r[1] => total\n    return [len(readings), low, low_at, high, high_at, total]\n\ndef off_range(place, tenths):\n    # How far a reading is outside the place's safe range: below is negative,\n    # above is positive, inside (or no range) is 0.\n    if not place[2]:\n        return 0\n    if tenths < place[3]:\n        return tenths - place[3]\n    if tenths > place[4]:\n        return tenths - place[4]\n    return 0\n\ndef count_outside(place):\n    0 => n\n    for r in place[1]:\n        if off_range(place, r[1]) != 0:\n            n + 1 => n\n    return n\n\ndef moving(readings, window):\n    # For each reading [minute, tenths, total of the last `window` readings or\n    # None while fewer have come]. The total slides: the entering reading is\n    # added and the leaving one taken away, in whole tenths, so it never drifts.\n    [] => out\n    0 => total\n    0 => i\n    while i < len(readings):\n        total + readings[i][1] => total\n        if i >= window:\n            total - readings[i - window][1] => total\n        if i >= window - 1:\n            out + [[readings[i][0], readings[i][1], total]] => out\n        else:\n            out + [[readings[i][0], readings[i][1], None]] => out\n        i + 1 => i\n    return out\n",
      "python": "import units\n\ndef find(places, name):\n    wanted = units.lower(name)\n    i = 0\n    while i < len(places):\n        if units.lower(places[i][0]) == wanted:\n            return i\n        i = i + 1\n    return 0 - 1\n\ndef reading_at(readings, minute):\n    i = 0\n    while i < len(readings):\n        if readings[i][0] == minute:\n            return i\n        i = i + 1\n    return 0 - 1\n\ndef with_reading(readings, minute, tenths):\n    k = 0\n    while k < len(readings) and readings[k][0] < minute:\n        k = k + 1\n    return readings[0:k] + [[minute, tenths]] + readings[k:len(readings)]\n\ndef stats(readings):\n    first = readings[0]\n    low = first[1]\n    low_at = first[0]\n    high = first[1]\n    high_at = first[0]\n    total = 0\n    for r in readings:\n        if r[1] < low:\n            low = r[1]\n            low_at = r[0]\n        if r[1] > high:\n            high = r[1]\n            high_at = r[0]\n        total = total + r[1]\n    return [len(readings), low, low_at, high, high_at, total]\n\ndef off_range(place, tenths):\n    if not place[2]:\n        return 0\n    if tenths < place[3]:\n        return tenths - place[3]\n    if tenths > place[4]:\n        return tenths - place[4]\n    return 0\n\ndef count_outside(place):\n    n = 0\n    for r in place[1]:\n        if off_range(place, r[1]) != 0:\n            n = n + 1\n    return n\n\ndef moving(readings, window):\n    out = []\n    total = 0\n    i = 0\n    while i < len(readings):\n        total = total + readings[i][1]\n        if i >= window:\n            total = total - readings[i - window][1]\n        if i >= window - 1:\n            out = out + [[readings[i][0], readings[i][1], total]]\n        else:\n            out = out + [[readings[i][0], readings[i][1], None]]\n        i = i + 1\n    return out\n"
    },
    {
      "name": "units.eml",
      "eml": "# P026 temperature log - reading and writing temperatures and times.\n# Temperatures are kept as whole tenths of a degree (21.5 C is 215), so sums\n# and averages are exact; only the display rounds. The interpreter that checks\n# every session does not run string methods yet, so the text handling is\n# written out here.\n\n\"0123456789\" => digits\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef lower(s):\n    # s with A-Z as a-z.\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper\n    \"abcdefghijklmnopqrstuvwxyz\" => small\n    \"\" => out\n    for c in s:\n        0 => k\n        while k < 26 and upper[k] != c:\n            k + 1 => k\n        if k < 26:\n            out + small[k] => out\n        else:\n            out + c => out\n    return out\n\ndef quotient(a, b):\n    # a divided by b, rounded down, for a >= 0 and b > 0.\n    return int((a - a % b) / b)\n\ndef average(total, count):\n    # total / count rounded to a whole number, halves away from zero.\n    if total < 0:\n        return 0 - quotient(0 - 2 * total + count, 2 * count)\n    return quotient(2 * total + count, 2 * count)\n\ndef show(tenths):\n    # 215 as \"21.5\", -5 as \"-0.5\", 0 as \"0.0\".\n    \"\" => sign\n    tenths => a\n    if a < 0:\n        \"-\" => sign\n        0 - a => a\n    return sign + str(quotient(a, 10)) + \".\" + str(a % 10)\n\ndef temperature(s):\n    # [True, tenths] for \"21\", \"21.5\", \"-3.5\" or \"+4\" from -60.0 to 60.0 with\n    # at most one decimal; [False, 0] for anything else.\n    trim(s) => s\n    1 => sign\n    if s != \"\" and (s[0] == \"-\" or s[0] == \"+\"):\n        if s[0] == \"-\":\n            0 - 1 => sign\n        s[1:len(s)] => s\n    0 => whole\n    0 => n\n    while n < len(s) and s[n] in digits:\n        whole * 10 + int(s[n]) => whole\n        n + 1 => n\n    if n == 0 or n > 2:\n        return [False, 0]\n    0 => tenth\n    if n < len(s):\n        if s[n] != \".\" or n + 2 != len(s) or not (s[n + 1] in digits):\n            return [False, 0]\n        int(s[n + 1]) => tenth\n    sign * (whole * 10 + tenth) => t\n    if t < 0 - 600 or t > 600:\n        return [False, 0]\n    return [True, t]\n\ndef minute_of(s):\n    # Minutes after midnight for \"7:05\" or \"07:05\" (00:00 to 23:59), else -1.\n    trim(s) => s\n    0 => colon\n    while colon < len(s) and s[colon] != \":\":\n        colon + 1 => colon\n    if colon == 0 or colon > 2 or colon + 3 != len(s):\n        return 0 - 1\n    0 => h\n    for c in s[0:colon]:\n        if not (c in digits):\n            return 0 - 1\n        h * 10 + int(c) => h\n    if not (s[colon + 1] in digits and s[colon + 2] in digits):\n        return 0 - 1\n    int(s[colon + 1]) * 10 + int(s[colon + 2]) => m\n    if h > 23 or m > 59:\n        return 0 - 1\n    return h * 60 + m\n\ndef clock(minute):\n    # 425 as \"07:05\".\n    quotient(minute, 60) => h\n    minute % 60 => m\n    \"\" => out\n    if h < 10:\n        \"0\" => out\n    out + str(h) + \":\" => out\n    if m < 10:\n        out + \"0\" => out\n    return out + str(m)\n\ndef pad_left(s, width):\n    while len(s) < width:\n        \" \" + s => s\n    return s\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef whole_number(s):\n    # The value of 1 to 3 digits; -1 for anything else.\n    trim(s) => s\n    if s == \"\" or len(s) > 3:\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in digits):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\n\ndef listed(items):\n    # [\"a\", \"b\", \"c\"] as \"a, b and c\".\n    \"\" => out\n    0 => i\n    while i < len(items):\n        if i > 0 and i == len(items) - 1:\n            out + \" and \" => out\n        elif i > 0:\n            out + \", \" => out\n        out + items[i] => out\n        i + 1 => i\n    return out\n",
      "python": "digits = \"0123456789\"\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef lower(s):\n    upper = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    small = \"abcdefghijklmnopqrstuvwxyz\"\n    out = \"\"\n    for c in s:\n        k = 0\n        while k < 26 and upper[k] != c:\n            k = k + 1\n        if k < 26:\n            out = out + small[k]\n        else:\n            out = out + c\n    return out\n\ndef quotient(a, b):\n    return int((a - a % b) / b)\n\ndef average(total, count):\n    if total < 0:\n        return 0 - quotient(0 - 2 * total + count, 2 * count)\n    return quotient(2 * total + count, 2 * count)\n\ndef show(tenths):\n    sign = \"\"\n    a = tenths\n    if a < 0:\n        sign = \"-\"\n        a = 0 - a\n    return sign + str(quotient(a, 10)) + \".\" + str(a % 10)\n\ndef temperature(s):\n    s = trim(s)\n    sign = 1\n    if s != \"\" and (s[0] == \"-\" or s[0] == \"+\"):\n        if s[0] == \"-\":\n            sign = 0 - 1\n        s = s[1:len(s)]\n    whole = 0\n    n = 0\n    while n < len(s) and s[n] in digits:\n        whole = whole * 10 + int(s[n])\n        n = n + 1\n    if n == 0 or n > 2:\n        return [False, 0]\n    tenth = 0\n    if n < len(s):\n        if s[n] != \".\" or n + 2 != len(s) or not s[n + 1] in digits:\n            return [False, 0]\n        tenth = int(s[n + 1])\n    t = sign * (whole * 10 + tenth)\n    if t < 0 - 600 or t > 600:\n        return [False, 0]\n    return [True, t]\n\ndef minute_of(s):\n    s = trim(s)\n    colon = 0\n    while colon < len(s) and s[colon] != \":\":\n        colon = colon + 1\n    if colon == 0 or colon > 2 or colon + 3 != len(s):\n        return 0 - 1\n    h = 0\n    for c in s[0:colon]:\n        if not c in digits:\n            return 0 - 1\n        h = h * 10 + int(c)\n    if not (s[colon + 1] in digits and s[colon + 2] in digits):\n        return 0 - 1\n    m = int(s[colon + 1]) * 10 + int(s[colon + 2])\n    if h > 23 or m > 59:\n        return 0 - 1\n    return h * 60 + m\n\ndef clock(minute):\n    h = quotient(minute, 60)\n    m = minute % 60\n    out = \"\"\n    if h < 10:\n        out = \"0\"\n    out = out + str(h) + \":\"\n    if m < 10:\n        out = out + \"0\"\n    return out + str(m)\n\ndef pad_left(s, width):\n    while len(s) < width:\n        s = \" \" + s\n    return s\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef whole_number(s):\n    s = trim(s)\n    if s == \"\" or len(s) > 3:\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in digits:\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n\ndef listed(items):\n    out = \"\"\n    i = 0\n    while i < len(items):\n        if i > 0 and i == len(items) - 1:\n            out = out + \" and \"\n        elif i > 0:\n            out = out + \", \"\n        out = out + items[i]\n        i = i + 1\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nseven\n3\n4\n5\n2\n1\n\n1\nLab #1\nLab\n25:00\n7:5\n07:05\nabc\n61\n-60.05\n21.25\n-0.5\n1\nlab\n7:05\n\n4\nLab\n2\nKitchen\n\n2\nLab\n5\n5\n4.9\n\n5\n1\nA\n12:00\n10\n1\nB\n12:00\n10\n1\nC\n12:00\n10\n1\nD\n12:00\n10\n1\nE\n12:00\n10\n1\nF\n\n2\nLab\n-1\n1\n5\n1\nLab\n09:00\n2\n4\nLab\n1\nx\n2\n3\n1\nA\n00:00\n0\n1\nA\n00:30\n1\n1\nA\n01:00\n2\n1\nA\n01:30\n3\n1\nA\n02:00\n4\n1\nA\n02:30\n5\n1\nA\n03:00\n6\n1\nA\n03:30\n0\n1\nA\n04:00\n1\n1\nA\n04:30\n2\n1\nA\n05:00\n3\n1\nA\n05:30\n4\n1\nA\n06:00\n5\n1\nA\n06:30\n6\n1\nA\n07:00\n0\n1\nA\n07:30\n1\n1\nA\n08:00\n2\n1\nA\n08:30\n3\n1\nA\n09:00\n4\n1\nA\n09:30\n5\n1\nA\n10:00\n6\n1\nA\n10:30\n0\n1\nA\n11:00\n1\n1\nA\n11:30\n2\n1\nA\n12:30\n4\n1\nA\n13:00\n5\n1\nA\n13:30\n6\n1\nA\n14:00\n0\n1\nA\n14:30\n1\n1\nA\n15:00\n2\n1\nA\n15:30\n3\n1\nA\n16:00\n4\n1\nA\n16:30\n5\n1\nA\n17:00\n6\n1\nA\n17:30\n0\n1\nA\n18:00\n1\n1\nA\n18:30\n2\n1\nA\n19:00\n3\n1\nA\n19:30\n4\n1\nA\n20:00\n5\n1\nA\n20:30\n6\n1\nA\n21:00\n0\n1\nA\n21:30\n1\n1\nA\n22:00\n2\n1\nA\n22:30\n3\n1\nA\n23:00\n4\n1\nA\n23:30\n5\n1\nA\n6\n",
      "screen": "\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> seven\nPick a number from 1 to 6.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 3\nNo readings yet.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 4\nNo readings yet.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 5\nNo readings yet.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 2\nNo places yet; add a reading first.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> \nCancelled.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Lab #1\nA place name is 1 to 16 letters, digits, spaces or hyphens.\nplace> Lab\ntime (HH:MM)> 25:00\nType a time from 00:00 to 23:59, such as 07:30.\ntime (HH:MM)> 7:5\nType a time from 00:00 to 23:59, such as 07:30.\ntime (HH:MM)> 07:05\ntemperature (C)> abc\nType a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.\ntemperature (C)> 61\nType a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.\ntemperature (C)> -60.05\nType a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.\ntemperature (C)> 21.25\nType a temperature from -60.0 to 60.0 with at most one decimal, such as -3.5 or 21.\ntemperature (C)> -0.5\nRecorded: Lab 07:05 -0.5 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> lab\ntime (HH:MM)> 7:05\nLab already has a reading at 07:05 (-0.5 C).\ntime (HH:MM)> \nCancelled.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 4\nplace> Lab\nA moving average needs at least 2 readings; Lab has 1.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 2\nplace> Kitchen\nThere is no place called Kitchen yet; add a reading for it first.\nplace> \nCancelled.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 2\nplace> Lab\nlowest (C)> 5\nhighest (C)> 5\nThe highest has to be above the lowest (5.0 C).\nhighest (C)> 4.9\nThe highest has to be above the lowest (5.0 C).\nhighest (C)> \nCancelled.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 5\nNo place has a safe range yet; set one with 2.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 12:00\ntemperature (C)> 10\nRecorded: A 12:00 10.0 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> B\ntime (HH:MM)> 12:00\ntemperature (C)> 10\nRecorded: B 12:00 10.0 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> C\ntime (HH:MM)> 12:00\ntemperature (C)> 10\nRecorded: C 12:00 10.0 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> D\ntime (HH:MM)> 12:00\ntemperature (C)> 10\nRecorded: D 12:00 10.0 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> E\ntime (HH:MM)> 12:00\ntemperature (C)> 10\nRecorded: E 12:00 10.0 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> F\nThere are already 6 places; a reading has to go to one of them.\nplace> \nCancelled.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 2\nplace> Lab\nlowest (C)> -1\nhighest (C)> 1\nLab's safe range: -1.0 to 1.0 C. Every reading so far is inside it.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 5\nEvery reading is inside its place's safe range.\nNo safe range yet: A, B, C, D and E.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Lab\ntime (HH:MM)> 09:00\ntemperature (C)> 2\nRecorded: Lab 09:00 2.0 C.\nALERT: 2.0 C is 1.0 above Lab's safe range of -1.0 to 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 4\nplace> Lab\nwindow (2 to 2 readings)> 1\nType a whole number from 2 to 2.\nwindow (2 to 2 readings)> x\nType a whole number from 2 to 2.\nwindow (2 to 2 readings)> 2\nLab, moving average of the last 2 readings:\n  07:05   -0.5\n  09:00    2.0   average 0.8\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 3\nLab: 2 readings, 07:05 to 09:00\n  lowest -0.5 C at 07:05, highest 2.0 C at 09:00, average 0.8 C\n  safe range -1.0 to 1.0 C, 1 reading outside it\nA: 1 reading, at 12:00\n  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C\n  no safe range\nB: 1 reading, at 12:00\n  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C\n  no safe range\nC: 1 reading, at 12:00\n  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C\n  no safe range\nD: 1 reading, at 12:00\n  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C\n  no safe range\nE: 1 reading, at 12:00\n  lowest 10.0 C at 12:00, highest 10.0 C at 12:00, average 10.0 C\n  no safe range\nAll places: 7 readings; lowest -0.5 C (Lab, 07:05), highest 10.0 C (A, 12:00).\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 00:00\ntemperature (C)> 0\nRecorded: A 00:00 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 00:30\ntemperature (C)> 1\nRecorded: A 00:30 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 01:00\ntemperature (C)> 2\nRecorded: A 01:00 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 01:30\ntemperature (C)> 3\nRecorded: A 01:30 3.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 02:00\ntemperature (C)> 4\nRecorded: A 02:00 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 02:30\ntemperature (C)> 5\nRecorded: A 02:30 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 03:00\ntemperature (C)> 6\nRecorded: A 03:00 6.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 03:30\ntemperature (C)> 0\nRecorded: A 03:30 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 04:00\ntemperature (C)> 1\nRecorded: A 04:00 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 04:30\ntemperature (C)> 2\nRecorded: A 04:30 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 05:00\ntemperature (C)> 3\nRecorded: A 05:00 3.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 05:30\ntemperature (C)> 4\nRecorded: A 05:30 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 06:00\ntemperature (C)> 5\nRecorded: A 06:00 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 06:30\ntemperature (C)> 6\nRecorded: A 06:30 6.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 07:00\ntemperature (C)> 0\nRecorded: A 07:00 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 07:30\ntemperature (C)> 1\nRecorded: A 07:30 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 08:00\ntemperature (C)> 2\nRecorded: A 08:00 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 08:30\ntemperature (C)> 3\nRecorded: A 08:30 3.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 09:00\ntemperature (C)> 4\nRecorded: A 09:00 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 09:30\ntemperature (C)> 5\nRecorded: A 09:30 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 10:00\ntemperature (C)> 6\nRecorded: A 10:00 6.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 10:30\ntemperature (C)> 0\nRecorded: A 10:30 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 11:00\ntemperature (C)> 1\nRecorded: A 11:00 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 11:30\ntemperature (C)> 2\nRecorded: A 11:30 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 12:30\ntemperature (C)> 4\nRecorded: A 12:30 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 13:00\ntemperature (C)> 5\nRecorded: A 13:00 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 13:30\ntemperature (C)> 6\nRecorded: A 13:30 6.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 14:00\ntemperature (C)> 0\nRecorded: A 14:00 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 14:30\ntemperature (C)> 1\nRecorded: A 14:30 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 15:00\ntemperature (C)> 2\nRecorded: A 15:00 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 15:30\ntemperature (C)> 3\nRecorded: A 15:30 3.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 16:00\ntemperature (C)> 4\nRecorded: A 16:00 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 16:30\ntemperature (C)> 5\nRecorded: A 16:30 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 17:00\ntemperature (C)> 6\nRecorded: A 17:00 6.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 17:30\ntemperature (C)> 0\nRecorded: A 17:30 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 18:00\ntemperature (C)> 1\nRecorded: A 18:00 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 18:30\ntemperature (C)> 2\nRecorded: A 18:30 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 19:00\ntemperature (C)> 3\nRecorded: A 19:00 3.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 19:30\ntemperature (C)> 4\nRecorded: A 19:30 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 20:00\ntemperature (C)> 5\nRecorded: A 20:00 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 20:30\ntemperature (C)> 6\nRecorded: A 20:30 6.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 21:00\ntemperature (C)> 0\nRecorded: A 21:00 0.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 21:30\ntemperature (C)> 1\nRecorded: A 21:30 1.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 22:00\ntemperature (C)> 2\nRecorded: A 22:00 2.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 22:30\ntemperature (C)> 3\nRecorded: A 22:30 3.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 23:00\ntemperature (C)> 4\nRecorded: A 23:00 4.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\ntime (HH:MM)> 23:30\ntemperature (C)> 5\nRecorded: A 23:30 5.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> A\nA already has 48 readings, the most a place keeps.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nGreenhouse\n06:30\n12.5\n1\nGreenhouse\n8:00\n15\n1\ngreenhouse\n14:00\n28.0\n1\nGreenhouse\n10:00\n19\n1\nFreezer\n08:00\n-21\n1\nFreezer\n14:00\n-12\n1\nFreezer\n11:30\n-18.5\n2\nGreenhouse\n15\n30\n2\nFreezer\n-25\n-15\n1\nGreenhouse\n16:30\n31.5\n3\n4\nGreenhouse\n3\n5\n1\nHall\n12:00\n21.5\n5\n6\n",
      "screen": "\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Greenhouse\ntime (HH:MM)> 06:30\ntemperature (C)> 12.5\nRecorded: Greenhouse 06:30 12.5 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Greenhouse\ntime (HH:MM)> 8:00\ntemperature (C)> 15\nRecorded: Greenhouse 08:00 15.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> greenhouse\ntime (HH:MM)> 14:00\ntemperature (C)> 28.0\nRecorded: Greenhouse 14:00 28.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Greenhouse\ntime (HH:MM)> 10:00\ntemperature (C)> 19\nRecorded: Greenhouse 10:00 19.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Freezer\ntime (HH:MM)> 08:00\ntemperature (C)> -21\nRecorded: Freezer 08:00 -21.0 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Freezer\ntime (HH:MM)> 14:00\ntemperature (C)> -12\nRecorded: Freezer 14:00 -12.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Freezer\ntime (HH:MM)> 11:30\ntemperature (C)> -18.5\nRecorded: Freezer 11:30 -18.5 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 2\nplace> Greenhouse\nlowest (C)> 15\nhighest (C)> 30\nGreenhouse's safe range: 15.0 to 30.0 C. 1 of its 4 readings is outside it.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 2\nplace> Freezer\nlowest (C)> -25\nhighest (C)> -15\nFreezer's safe range: -25.0 to -15.0 C. 1 of its 3 readings is outside it.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Greenhouse\ntime (HH:MM)> 16:30\ntemperature (C)> 31.5\nRecorded: Greenhouse 16:30 31.5 C.\nALERT: 31.5 C is 1.5 above Greenhouse's safe range of 15.0 to 30.0 C.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 3\nGreenhouse: 5 readings, 06:30 to 16:30\n  lowest 12.5 C at 06:30, highest 31.5 C at 16:30, average 21.2 C\n  safe range 15.0 to 30.0 C, 2 readings outside it\nFreezer: 3 readings, 08:00 to 14:00\n  lowest -21.0 C at 08:00, highest -12.0 C at 14:00, average -17.2 C\n  safe range -25.0 to -15.0 C, 1 reading outside it\nAll places: 8 readings; lowest -21.0 C (Freezer, 08:00), highest 31.5 C (Greenhouse, 16:30).\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 4\nplace> Greenhouse\nwindow (2 to 5 readings)> 3\nGreenhouse, moving average of the last 3 readings:\n  06:30   12.5\n  08:00   15.0\n  10:00   19.0   average 15.5\n  14:00   28.0   average 20.7\n  16:30   31.5   average 26.2\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 5\nOutside the safe ranges:\n  Greenhouse 06:30  12.5 C, 2.5 below 15.0\n  Greenhouse 16:30  31.5 C, 1.5 above 30.0\n  Freezer 14:00  -12.0 C, 3.0 above -15.0\n3 of 8 readings at places with a safe range are outside it.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 1\nplace> Hall\ntime (HH:MM)> 12:00\ntemperature (C)> 21.5\nRecorded: Hall 12:00 21.5 C - a new place.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 5\nOutside the safe ranges:\n  Greenhouse 06:30  12.5 C, 2.5 below 15.0\n  Greenhouse 16:30  31.5 C, 1.5 above 30.0\n  Freezer 14:00  -12.0 C, 3.0 above -15.0\n3 of 8 readings at places with a safe range are outside it.\nNo safe range yet: Hall.\n\n== Temperature log ==\n1) add a reading  2) set a safe range  3) summary\n4) moving average  5) alerts  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "temperature-alert-system",
      "caseId": "046-temperature-alert-system",
      "title": "Temperature alert system"
    },
    {
      "slug": "moving-average",
      "caseId": "118-moving-average",
      "title": "Moving average (sliding window)"
    }
  ],
  "updated": "2026-10-05"
}
