{
  "id": "P059",
  "slug": "workout-log",
  "key": "P059-workout-log",
  "title": "Workout log",
  "summary": "Exercises logged as sets x reps at a weight on real dates, kept exact in tenths of a kilogram: the history day by day, each exercise's heaviest weight and best estimated one-rep max (Epley), and the volume lifted week by week, Monday to Sunday, drawn as a chart that adds back up to the total.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P059 - Workout log\n\nLog exercises as sets x reps at a weight on real dates. See the history day\nby day, each exercise's records - the heaviest weight and the best\nestimated one-rep maximum - and the volume lifted week by week as a chart.\n\n- `main.eml` - the menu, reading workout lines, and the history, records\n  and weekly views\n- `log.eml` - entries: weights, volume, the one-rep estimate, records and\n  weeks\n- `dates.eml` - calendar dates, from P055: day numbers since 1900-01-01\n  and weekdays by Zeller's congruence\n\nHow each part works:\n\n- Weights are kept in whole tenths of a kilogram, so 102.5 kg is 1025, and\n  every sum is exact. The volume of an entry is sets x reps x weight.\n- The one-rep estimate is Epley's: weight x (1 + reps / 30), to the\n  nearest tenth, rounded half up. For a single rep it is the weight itself.\n  Records keep the earliest entry when two are equal.\n- A week runs from Monday to Sunday. Each entry falls in exactly one week,\n  from its Monday up to, but not including, the next. So the weekly\n  volumes add back up to the total of all entries, and the weekly view\n  shows both numbers. This is the check the corpus case\n  `histogram-builder` makes of its buckets. Weeks with no workout are\n  shown too, and the bars are scaled so the biggest week gets 30 marks.\n- The log keeps entries in date order. A workout typed for an earlier date\n  goes in its place; entries on the same day keep the order typed.\n\nWhat is checked: menu choices 1 to 6; a real date from 1900-01-01 to\n2099-12-31, or nothing for the last date used; workout lines with:\n- an exercise name of up to 20 characters;\n- sets x reps from 1x1 to 20x100;\n- a weight from 0.1 to 500 kg with at most one decimal.\n\nAt most 200 entries. An empty line stops.\n\nSessions: `sessions/basic.in` loads three weeks of sample workouts:\nsquat, bench press and deadlift, a little heavier each time.\n- The history and the records come first. Squat's heaviest is 120 kg for\n  1 rep, but its best estimate, 122.5 kg, comes from 105 kg x 5. Bench's\n  62.5 x 8 makes 79.2 kg, and it wins over the equal entry a week later\n  because it came first.\n- The weekly volumes are 6577.5, 6520 and 945 kg, which add up to the\n  13 entries' 14042.5 kg.\n- A workout for Monday 2026-10-12 is logged. Squat's 112.5 kg x 5 sets a\n  new estimate of 131.3 kg (131.25, rounded up), and a fourth week appears.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- the three views of an empty log;\n- the date 2026-02-30, then nothing, for the last date used;\n- the lines \"squat\", \"squat 3x5\", 3x, 0x5, 21x5 and 3x101;\n- weights 0, 500.5, 100.25 and abc;\n- a name of 25 characters;\n- a weight of .5 kg, which is kept, so the records show 0.5 kg and an\n  estimate of 0.6 kg.\n\nBuilt on the verified corpus case `histogram-builder` (values counted into\nranges, every value in exactly one, the counts adding back up to the\ntotal).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P059 workout log: log exercises as sets x reps at a weight on real dates,\n# see the history, each exercise's records - the heaviest weight and the\n# best estimated one-rep max - and the volume lifted week by week.\nimport dates\nimport log\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 words(s):\n    [] => out\n    \"\" => word\n    for c in s + \" \":\n        if c == \" \":\n            if word != \"\":\n                out + [word] => out\n            \"\" => word\n        else:\n            word + c => word\n    return out\n\ndef whole(s, high):\n    # A whole number from 1 to high, or -1.\n    if s == \"\" or len(s) > 4:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    if n < 1 or n > high:\n        return -1\n    return n\n\ndef tenths(s):\n    # A weight like 100, 102.5 or 0.5 (kilograms, at most one decimal, up to\n    # 500) in tenths, or -1.\n    0 => k\n    while k < len(s) and s[k] != \".\":\n        k + 1 => k\n    s[0:k] => a\n    \"0\" => b\n    if k < len(s):\n        s[k + 1:len(s)] => b\n        if len(b) != 1:\n            return -1\n    if a == \"\":\n        \"0\" => a\n    for c in a + b:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n    if len(a) > 3:\n        return -1\n    int(a) * 10 + int(b) => t\n    if t < 1 or t > 5000:\n        return -1\n    return t\n\ndef parsed_line(line):\n    # \"bench press 3x8 62.5\" as [exercise, sets, reps, tenths], or [why].\n    words(line) => ws\n    if len(ws) < 3:\n        return [\"an exercise, sets x reps and a weight, like squat 3x5 100\"]\n    ws[len(ws) - 1] => w\n    ws[len(ws) - 2] => sr\n    \"\" => name\n    for k in [0:len(ws) - 3]:\n        if name != \"\":\n            name + \" \" => name\n        name + ws[k] => name\n    if len(name) > 20:\n        return [\"an exercise name of at most 20 characters\"]\n    0 => x\n    while x < len(sr) and sr[x] != \"x\":\n        x + 1 => x\n    whole(sr[0:x], 20) => sets\n    -1 => reps\n    if x < len(sr):\n        whole(sr[x + 1:len(sr)], 100) => reps\n    if sets == -1 or reps == -1:\n        return [\"sets x reps as two whole numbers like 3x5, at most 20 sets of 100 reps\"]\n    tenths(w) => t\n    if t == -1:\n        return [\"a weight in kilograms from 0.1 to 500, at most one decimal, like 102.5\"]\n    return [name, sets, reps, t]\n\ndef reps_text(n):\n    if n == 1:\n        return \"1 rep\"\n    return str(n) + \" reps\"\n\ndef shown_day(n):\n    dates.date_of(n) => d\n    return dates.shown(d) + \" \" + dates.day_name(d)[0:3]\n\ndef entry_text(e):\n    return e[1] + \" \" + str(e[2]) + \" x \" + str(e[3]) + \" x \" + log.kg(e[4]) + \" kg\"\n\ndef log_workout(state):\n    if len(state[0]) >= 200:\n        \"The log holds 200 entries, the most it keeps.\" ^0\n        return\n    dates.shown(dates.date_of(state[1])) => last\n    while True:\n        trim(input(\"date (YYYY-MM-DD, nothing for \" + last + \")> \")) => answer\n        if answer == \"\":\n            state[1] => day\n            break\n        dates.parsed(answer) => d\n        if len(d) > 0:\n            dates.day_number(d) => day\n            break\n        \"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\" ^0\n    day => state[1]\n    (\"Exercises for \" + shown_day(day) + \", one a line, like squat 3x5 100; nothing to stop.\") ^0\n    0 => added\n    while len(state[0]) < 200:\n        trim(input(\"exercise> \")) => line\n        if line == \"\":\n            break\n        parsed_line(line) => p\n        if len(p) == 1:\n            (\"Type \" + p[0] + \".\") ^0\n        else:\n            [day, p[0], p[1], p[2], p[3]] => e\n            # keep the log in date order; same-day entries in the order typed\n            len(state[0]) => k\n            while k > 0 and state[0][k - 1][0] > day:\n                k - 1 => k\n            state[0][0:k] + [e] + state[0][k:len(state[0])] => state[0]\n            added + 1 => added\n            (\"  \" + entry_text(e) + \": volume \" + log.kg(log.volume(e)) + \" kg\") ^0\n    (str(added) + \" added, \" + str(len(state[0])) + \" in the log.\") ^0\n\ndef history(state):\n    if len(state[0]) == 0:\n        \"The log is empty.\" ^0\n        return\n    -1 => day\n    for e in state[0]:\n        if e[0] != day:\n            e[0] => day\n            shown_day(day) ^0\n        (\"  \" + entry_text(e)) ^0\n\ndef exercises(state):\n    [] => out\n    for e in state[0]:\n        False => seen\n        for x in out:\n            if x == e[1]:\n                True => seen\n        if not seen:\n            out + [e[1]] => out\n    return out\n\ndef show_records(state):\n    if len(state[0]) == 0:\n        \"The log is empty.\" ^0\n        return\n    for x in exercises(state):\n        log.records(state[0], x) => r\n        r[0] => heavy\n        r[1] => best\n        (x + \": heaviest \" + log.kg(heavy[0]) + \" kg (\" + reps_text(heavy[1][3]) + \", \" + dates.shown(dates.date_of(heavy[1][0])) + \"); best estimated 1-rep max \" + log.kg(best[0]) + \" kg (\" + log.kg(best[1][4]) + \" x \" + str(best[1][3]) + \", \" + dates.shown(dates.date_of(best[1][0])) + \")\") ^0\n\ndef weekly(state):\n    if len(state[0]) == 0:\n        \"The log is empty.\" ^0\n        return\n    log.weekly(state[0]) => weeks\n    log.bars(weeks, 30) => b\n    0 => total\n    for k in [0:len(weeks) - 1]:\n        log.kg(weeks[k][1]) => v\n        while len(v) < 9:\n            \" \" + v => v\n        total + weeks[k][1] => total\n        (\"week of \" + dates.shown(dates.date_of(weeks[k][0])) + v + \" kg  \" + \"#\" * b[k]) ^0\n    0 => all\n    for e in state[0]:\n        all + log.volume(e) => all\n    (\"Total \" + log.kg(total) + \" kg over \" + str(len(weeks)) + \" weeks - the same as all \" + str(len(state[0])) + \" entries added up, \" + log.kg(all) + \" kg.\") ^0\n\ndef sample(state):\n    # Three weeks of squat, bench press and deadlift, each a little heavier.\n    [] => entries\n    dates.day_number([2026, 9, 21]) => start\n    [[0, \"squat\", 3, 5, 1000], [0, \"bench press\", 3, 8, 600],\n     [2, \"deadlift\", 1, 5, 1200], [4, \"squat\", 3, 5, 1025], [4, \"bench press\", 3, 8, 625],\n     [7, \"squat\", 3, 5, 1050], [7, \"bench press\", 3, 6, 650], [9, \"deadlift\", 1, 5, 1250],\n     [11, \"squat\", 5, 3, 1100], [11, \"bench press\", 3, 8, 625],\n     [14, \"squat\", 1, 1, 1200], [14, \"bench press\", 2, 3, 700], [16, \"deadlift\", 1, 3, 1350]] => plan\n    for p in plan:\n        entries + [[start + p[0], p[1], p[2], p[3], p[4]]] => entries\n    entries => state[0]\n    start + 16 => state[1]\n    \"Three weeks of sample workouts, 2026-09-21 to 2026-10-07.\" ^0\n\n\"== Workout log ==\" ^0\n\"Log exercises as sets x reps at a weight; see records and weekly volume.\" ^0\n# [entries in date order, the last date used]\n[[], dates.day_number([2026, 10, 12])] => state\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        log_workout(state)\n    elif choice == \"2\":\n        history(state)\n    elif choice == \"3\":\n        show_records(state)\n    elif choice == \"4\":\n        weekly(state)\n    elif choice == \"5\":\n        sample(state)\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import dates\nimport log\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 words(s):\n    out = []\n    word = \"\"\n    for c in s + \" \":\n        if c == \" \":\n            if word != \"\":\n                out = out + [word]\n            word = \"\"\n        else:\n            word = word + c\n    return out\n\ndef whole(s, high):\n    if s == \"\" or len(s) > 4:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    if n < 1 or n > high:\n        return -1\n    return n\n\ndef tenths(s):\n    k = 0\n    while k < len(s) and s[k] != \".\":\n        k = k + 1\n    a = s[0:k]\n    b = \"0\"\n    if k < len(s):\n        b = s[k + 1:len(s)]\n        if len(b) != 1:\n            return -1\n    if a == \"\":\n        a = \"0\"\n    for c in a + b:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n    if len(a) > 3:\n        return -1\n    t = int(a) * 10 + int(b)\n    if t < 1 or t > 5000:\n        return -1\n    return t\n\ndef parsed_line(line):\n    ws = words(line)\n    if len(ws) < 3:\n        return [\"an exercise, sets x reps and a weight, like squat 3x5 100\"]\n    w = ws[len(ws) - 1]\n    sr = ws[len(ws) - 2]\n    name = \"\"\n    for k in range(0, len(ws) - 3+1):\n        if name != \"\":\n            name = name + \" \"\n        name = name + ws[k]\n    if len(name) > 20:\n        return [\"an exercise name of at most 20 characters\"]\n    x = 0\n    while x < len(sr) and sr[x] != \"x\":\n        x = x + 1\n    sets = whole(sr[0:x], 20)\n    reps = -1\n    if x < len(sr):\n        reps = whole(sr[x + 1:len(sr)], 100)\n    if sets == -1 or reps == -1:\n        return [\"sets x reps as two whole numbers like 3x5, at most 20 sets of 100 reps\"]\n    t = tenths(w)\n    if t == -1:\n        return [\"a weight in kilograms from 0.1 to 500, at most one decimal, like 102.5\"]\n    return [name, sets, reps, t]\n\ndef reps_text(n):\n    if n == 1:\n        return \"1 rep\"\n    return str(n) + \" reps\"\n\ndef shown_day(n):\n    d = dates.date_of(n)\n    return dates.shown(d) + \" \" + dates.day_name(d)[0:3]\n\ndef entry_text(e):\n    return e[1] + \" \" + str(e[2]) + \" x \" + str(e[3]) + \" x \" + log.kg(e[4]) + \" kg\"\n\ndef log_workout(state):\n    if len(state[0]) >= 200:\n        print(\"The log holds 200 entries, the most it keeps.\")\n        return\n    last = dates.shown(dates.date_of(state[1]))\n    while True:\n        answer = trim(input(\"date (YYYY-MM-DD, nothing for \" + last + \")> \"))\n        if answer == \"\":\n            day = state[1]\n            break\n        d = dates.parsed(answer)\n        if len(d) > 0:\n            day = dates.day_number(d)\n            break\n        print(\"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\")\n    state[1] = day\n    print(\"Exercises for \" + shown_day(day) + \", one a line, like squat 3x5 100; nothing to stop.\")\n    added = 0\n    while len(state[0]) < 200:\n        line = trim(input(\"exercise> \"))\n        if line == \"\":\n            break\n        p = parsed_line(line)\n        if len(p) == 1:\n            print(\"Type \" + p[0] + \".\")\n        else:\n            e = [day, p[0], p[1], p[2], p[3]]\n            k = len(state[0])\n            while k > 0 and state[0][k - 1][0] > day:\n                k = k - 1\n            state[0] = state[0][0:k] + [e] + state[0][k:len(state[0])]\n            added = added + 1\n            print(\"  \" + entry_text(e) + \": volume \" + log.kg(log.volume(e)) + \" kg\")\n    print(str(added) + \" added, \" + str(len(state[0])) + \" in the log.\")\n\ndef history(state):\n    if len(state[0]) == 0:\n        print(\"The log is empty.\")\n        return\n    day = -1\n    for e in state[0]:\n        if e[0] != day:\n            day = e[0]\n            print(shown_day(day))\n        print(\"  \" + entry_text(e))\n\ndef exercises(state):\n    out = []\n    for e in state[0]:\n        seen = False\n        for x in out:\n            if x == e[1]:\n                seen = True\n        if not seen:\n            out = out + [e[1]]\n    return out\n\ndef show_records(state):\n    if len(state[0]) == 0:\n        print(\"The log is empty.\")\n        return\n    for x in exercises(state):\n        r = log.records(state[0], x)\n        heavy = r[0]\n        best = r[1]\n        print(x + \": heaviest \" + log.kg(heavy[0]) + \" kg (\" + reps_text(heavy[1][3]) + \", \" + dates.shown(dates.date_of(heavy[1][0])) + \"); best estimated 1-rep max \" + log.kg(best[0]) + \" kg (\" + log.kg(best[1][4]) + \" x \" + str(best[1][3]) + \", \" + dates.shown(dates.date_of(best[1][0])) + \")\")\n\ndef weekly(state):\n    if len(state[0]) == 0:\n        print(\"The log is empty.\")\n        return\n    weeks = log.weekly(state[0])\n    b = log.bars(weeks, 30)\n    total = 0\n    for k in range(0, len(weeks)):\n        v = log.kg(weeks[k][1])\n        while len(v) < 9:\n            v = \" \" + v\n        total = total + weeks[k][1]\n        print(\"week of \" + dates.shown(dates.date_of(weeks[k][0])) + v + \" kg  \" + \"#\" * b[k])\n    all = 0\n    for e in state[0]:\n        all = all + log.volume(e)\n    print(\"Total \" + log.kg(total) + \" kg over \" + str(len(weeks)) + \" weeks - the same as all \" + str(len(state[0])) + \" entries added up, \" + log.kg(all) + \" kg.\")\n\ndef sample(state):\n    entries = []\n    start = dates.day_number([2026, 9, 21])\n    plan = [[0, \"squat\", 3, 5, 1000], [0, \"bench press\", 3, 8, 600], [2, \"deadlift\", 1, 5, 1200], [4, \"squat\", 3, 5, 1025], [4, \"bench press\", 3, 8, 625], [7, \"squat\", 3, 5, 1050], [7, \"bench press\", 3, 6, 650], [9, \"deadlift\", 1, 5, 1250], [11, \"squat\", 5, 3, 1100], [11, \"bench press\", 3, 8, 625], [14, \"squat\", 1, 1, 1200], [14, \"bench press\", 2, 3, 700], [16, \"deadlift\", 1, 3, 1350]]\n    for p in plan:\n        entries = entries + [[start + p[0], p[1], p[2], p[3], p[4]]]\n    state[0] = entries\n    state[1] = start + 16\n    print(\"Three weeks of sample workouts, 2026-09-21 to 2026-10-07.\")\n\nprint(\"== Workout log ==\")\nprint(\"Log exercises as sets x reps at a weight; see records and weekly volume.\")\nstate = [[], dates.day_number([2026, 10, 12])]\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        log_workout(state)\n    elif choice == \"2\":\n        history(state)\n    elif choice == \"3\":\n        show_records(state)\n    elif choice == \"4\":\n        weekly(state)\n    elif choice == \"5\":\n        sample(state)\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "log.eml",
      "eml": "# P059 workout log - entries, records and weekly volume. An entry is\n# [day number, exercise, sets, reps, weight in tenths of a kilogram]:\n# weights are kept as whole tenths, so 102.5 kg is 1025 and every sum is\n# exact.\nimport dates\n\ndef kg(tenths):\n    # Tenths of a kilogram as text: 1025 -> \"102.5\", 1000 -> \"100\".\n    str(int(tenths / 10)) => whole\n    tenths % 10 => part\n    if part == 0:\n        return whole\n    return whole + \".\" + str(part)\n\ndef volume(e):\n    # Sets x reps x weight, in tenths.\n    return e[2] * e[3] * e[4]\n\ndef one_rep_max(weight, reps):\n    # Epley's estimate of the most you could lift once: weight x (1 + reps /\n    # 30), to the nearest tenth (halves up), and the weight itself for a\n    # single rep. Whole-number arithmetic: tenths x (30 + reps) / 30.\n    if reps == 1:\n        return weight\n    return int((weight * (30 + reps) * 2 + 30) / 60)\n\ndef records(entries, exercise):\n    # [heaviest weight and its entry, best estimated one-rep max and its\n    # entry]; the earliest entry wins a tie. [] when the exercise has none.\n    [] => heavy\n    [] => best\n    for e in entries:\n        if e[1] == exercise:\n            if len(heavy) == 0 or e[4] > heavy[0]:\n                [e[4], e] => heavy\n            one_rep_max(e[4], e[3]) => m\n            if len(best) == 0 or m > best[0]:\n                [m, e] => best\n    if len(heavy) == 0:\n        return []\n    return [heavy, best]\n\ndef week_start(day):\n    # The Monday on or before a day.\n    return day - dates.weekday(dates.date_of(day))\n\ndef weekly(entries):\n    # [[monday, volume in tenths], ...] for every week from the first entry's\n    # to the last's, empty weeks included, oldest first. Each entry falls in\n    # exactly one week - from its Monday up to, not including, the next - so\n    # the weekly volumes add back up to the total, the check the corpus case\n    # histogram-builder makes of its buckets.\n    if len(entries) == 0:\n        return []\n    entries[0][0] => lo\n    lo => hi\n    for e in entries:\n        if e[0] < lo:\n            e[0] => lo\n        if e[0] > hi:\n            e[0] => hi\n    [] => out\n    week_start(lo) => w\n    while w <= hi:\n        0 => total\n        for e in entries:\n            if e[0] >= w and e[0] < w + 7:\n                total + volume(e) => total\n        out + [[w, total]] => out\n        w + 7 => w\n    return out\n\ndef bars(weeks, width):\n    # A bar length for each week, scaled so the biggest week gets `width`\n    # marks (rounded half up); a week with any volume gets at least one.\n    0 => top\n    for w in weeks:\n        if w[1] > top:\n            w[1] => top\n    [] => out\n    for w in weeks:\n        0 => n\n        if top > 0:\n            int((w[1] * width * 2 + top) / (2 * top)) => n\n            if n == 0 and w[1] > 0:\n                1 => n\n        out + [n] => out\n    return out\n",
      "python": "import dates\n\ndef kg(tenths):\n    whole = str(int(tenths / 10))\n    part = tenths % 10\n    if part == 0:\n        return whole\n    return whole + \".\" + str(part)\n\ndef volume(e):\n    return e[2] * e[3] * e[4]\n\ndef one_rep_max(weight, reps):\n    if reps == 1:\n        return weight\n    return int((weight * (30 + reps) * 2 + 30) / 60)\n\ndef records(entries, exercise):\n    heavy = []\n    best = []\n    for e in entries:\n        if e[1] == exercise:\n            if len(heavy) == 0 or e[4] > heavy[0]:\n                heavy = [e[4], e]\n            m = one_rep_max(e[4], e[3])\n            if len(best) == 0 or m > best[0]:\n                best = [m, e]\n    if len(heavy) == 0:\n        return []\n    return [heavy, best]\n\ndef week_start(day):\n    return day - dates.weekday(dates.date_of(day))\n\ndef weekly(entries):\n    if len(entries) == 0:\n        return []\n    lo = entries[0][0]\n    hi = lo\n    for e in entries:\n        if e[0] < lo:\n            lo = e[0]\n        if e[0] > hi:\n            hi = e[0]\n    out = []\n    w = week_start(lo)\n    while w <= hi:\n        total = 0\n        for e in entries:\n            if e[0] >= w and e[0] < w + 7:\n                total = total + volume(e)\n        out = out + [[w, total]]\n        w = w + 7\n    return out\n\ndef bars(weeks, width):\n    top = 0\n    for w in weeks:\n        if w[1] > top:\n            top = w[1]\n    out = []\n    for w in weeks:\n        n = 0\n        if top > 0:\n            n = int((w[1] * width * 2 + top) / (2 * top))\n            if n == 0 and w[1] > 0:\n                n = 1\n        out = out + [n]\n    return out\n"
    },
    {
      "name": "dates.eml",
      "eml": "# P059 workout log - real calendar dates, from P055 (habit tracker),\n# 1900-01-01 to 2099-12-31. A\n# date is [year, month, day]; a day number counts days from 1900-01-01\n# (day 0), as in the corpus case days-between-dates, so consecutive days\n# have consecutive numbers and a week is seven numbers from a Monday.\n\ndef is_leap(year):\n    if year % 400 == 0:\n        return True\n    if year % 100 == 0:\n        return False\n    return year % 4 == 0\n\ndef days_in_month(year, month):\n    if month == 2:\n        if is_leap(year):\n            return 29\n        return 28\n    if month == 4 or month == 6 or month == 9 or month == 11:\n        return 30\n    return 31\n\ndef day_number(d):\n    0 => total\n    for y in [1900:d[0] - 1]:\n        if is_leap(y):\n            total + 366 => total\n        else:\n            total + 365 => total\n    for m in [1:d[1] - 1]:\n        total + days_in_month(d[0], m) => total\n    return total + d[2] - 1\n\ndef date_of(n):\n    # The date of day number n.\n    1900 => y\n    365 => length\n    while True:\n        365 => length\n        if is_leap(y):\n            366 => length\n        if n < length:\n            break\n        n - length => n\n        y + 1 => y\n    1 => m\n    while n >= days_in_month(y, m):\n        n - days_in_month(y, m) => n\n        m + 1 => m\n    return [y, m, n + 1]\n\ndef weekday(d):\n    # Zeller's congruence, as in the corpus case day-of-week-zeller: January\n    # and February count as months 13 and 14 of the year before. Returns 0\n    # for Monday up to 6 for Sunday (Zeller's 0 is Saturday).\n    d[0] => y\n    d[1] => m\n    if m < 3:\n        m + 12 => m\n        y - 1 => y\n    y % 100 => k\n    int(y / 100) => j\n    (d[2] + int(13 * (m + 1) / 5) + k + int(k / 4) + int(j / 4) + 5 * j) % 7 => h\n    return (h + 5) % 7\n\ndef parsed(s):\n    # \"YYYY-MM-DD\" as a date, or [] when it is not a real date in range.\n    if len(s) != 10 or s[4] != \"-\" or s[7] != \"-\":\n        return []\n    for i in [0, 1, 2, 3, 5, 6, 8, 9]:\n        if not (s[i] >= \"0\" and s[i] <= \"9\"):\n            return []\n    int(s[0:4]) => y\n    int(s[5:7]) => m\n    int(s[8:10]) => d\n    if y < 1900 or y > 2099 or m < 1 or m > 12 or d < 1 or d > days_in_month(y, m):\n        return []\n    return [y, m, d]\n\ndef shown(d):\n    str(d[1]) => m\n    if len(m) == 1:\n        \"0\" + m => m\n    str(d[2]) => dd\n    if len(dd) == 1:\n        \"0\" + dd => dd\n    return str(d[0]) + \"-\" + m + \"-\" + dd\n\ndef day_name(d):\n    return [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"][weekday(d)]\n",
      "python": "def is_leap(year):\n    if year % 400 == 0:\n        return True\n    if year % 100 == 0:\n        return False\n    return year % 4 == 0\n\ndef days_in_month(year, month):\n    if month == 2:\n        if is_leap(year):\n            return 29\n        return 28\n    if month == 4 or month == 6 or month == 9 or month == 11:\n        return 30\n    return 31\n\ndef day_number(d):\n    total = 0\n    for y in range(1900, d[0]):\n        if is_leap(y):\n            total = total + 366\n        else:\n            total = total + 365\n    for m in range(1, d[1]):\n        total = total + days_in_month(d[0], m)\n    return total + d[2] - 1\n\ndef date_of(n):\n    y = 1900\n    length = 365\n    while True:\n        length = 365\n        if is_leap(y):\n            length = 366\n        if n < length:\n            break\n        n = n - length\n        y = y + 1\n    m = 1\n    while n >= days_in_month(y, m):\n        n = n - days_in_month(y, m)\n        m = m + 1\n    return [y, m, n + 1]\n\ndef weekday(d):\n    y = d[0]\n    m = d[1]\n    if m < 3:\n        m = m + 12\n        y = y - 1\n    k = y % 100\n    j = int(y / 100)\n    h = (d[2] + int(13 * (m + 1) / 5) + k + int(k / 4) + int(j / 4) + 5 * j) % 7\n    return (h + 5) % 7\n\ndef parsed(s):\n    if len(s) != 10 or s[4] != \"-\" or s[7] != \"-\":\n        return []\n    for i in [0, 1, 2, 3, 5, 6, 8, 9]:\n        if not (s[i] >= \"0\" and s[i] <= \"9\"):\n            return []\n    y = int(s[0:4])\n    m = int(s[5:7])\n    d = int(s[8:10])\n    if y < 1900 or y > 2099 or m < 1 or m > 12 or d < 1 or d > days_in_month(y, m):\n        return []\n    return [y, m, d]\n\ndef shown(d):\n    m = str(d[1])\n    if len(m) == 1:\n        m = \"0\" + m\n    dd = str(d[2])\n    if len(dd) == 1:\n        dd = \"0\" + dd\n    return str(d[0]) + \"-\" + m + \"-\" + dd\n\ndef day_name(d):\n    return [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"][weekday(d)]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n3\n4\n1\n2026-02-30\n\nsquat\nsquat 3x5\nsquat 3x 100\nsquat 0x5 100\nsquat 21x5 100\nsquat 3x101 100\nsquat 3x5 0\nsquat 3x5 500.5\nsquat 3x5 100.25\nsquat 3x5 abc\na very long exercise name 3x5 100\nsquat 3x5 .5\n\n3\n6\n",
      "screen": "== Workout log ==\nLog exercises as sets x reps at a weight; see records and weekly volume.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 2\nThe log is empty.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 3\nThe log is empty.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 4\nThe log is empty.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 1\ndate (YYYY-MM-DD, nothing for 2026-10-12)> 2026-02-30\nType a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\ndate (YYYY-MM-DD, nothing for 2026-10-12)> \nExercises for 2026-10-12 Mon, one a line, like squat 3x5 100; nothing to stop.\nexercise> squat\nType an exercise, sets x reps and a weight, like squat 3x5 100.\nexercise> squat 3x5\nType an exercise, sets x reps and a weight, like squat 3x5 100.\nexercise> squat 3x 100\nType sets x reps as two whole numbers like 3x5, at most 20 sets of 100 reps.\nexercise> squat 0x5 100\nType sets x reps as two whole numbers like 3x5, at most 20 sets of 100 reps.\nexercise> squat 21x5 100\nType sets x reps as two whole numbers like 3x5, at most 20 sets of 100 reps.\nexercise> squat 3x101 100\nType sets x reps as two whole numbers like 3x5, at most 20 sets of 100 reps.\nexercise> squat 3x5 0\nType a weight in kilograms from 0.1 to 500, at most one decimal, like 102.5.\nexercise> squat 3x5 500.5\nType a weight in kilograms from 0.1 to 500, at most one decimal, like 102.5.\nexercise> squat 3x5 100.25\nType a weight in kilograms from 0.1 to 500, at most one decimal, like 102.5.\nexercise> squat 3x5 abc\nType a weight in kilograms from 0.1 to 500, at most one decimal, like 102.5.\nexercise> a very long exercise name 3x5 100\nType an exercise name of at most 20 characters.\nexercise> squat 3x5 .5\n  squat 3 x 5 x 0.5 kg: volume 7.5 kg\nexercise> \n1 added, 1 in the log.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 3\nsquat: heaviest 0.5 kg (5 reps, 2026-10-12); best estimated 1-rep max 0.6 kg (0.5 x 5, 2026-10-12)\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "5\n2\n3\n4\n1\n2026-10-12\nsquat 3x5 112.5\nbench press 5x5 65\n\n3\n4\n6\n",
      "screen": "== Workout log ==\nLog exercises as sets x reps at a weight; see records and weekly volume.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 5\nThree weeks of sample workouts, 2026-09-21 to 2026-10-07.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 2\n2026-09-21 Mon\n  squat 3 x 5 x 100 kg\n  bench press 3 x 8 x 60 kg\n2026-09-23 Wed\n  deadlift 1 x 5 x 120 kg\n2026-09-25 Fri\n  squat 3 x 5 x 102.5 kg\n  bench press 3 x 8 x 62.5 kg\n2026-09-28 Mon\n  squat 3 x 5 x 105 kg\n  bench press 3 x 6 x 65 kg\n2026-09-30 Wed\n  deadlift 1 x 5 x 125 kg\n2026-10-02 Fri\n  squat 5 x 3 x 110 kg\n  bench press 3 x 8 x 62.5 kg\n2026-10-05 Mon\n  squat 1 x 1 x 120 kg\n  bench press 2 x 3 x 70 kg\n2026-10-07 Wed\n  deadlift 1 x 3 x 135 kg\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 3\nsquat: heaviest 120 kg (1 rep, 2026-10-05); best estimated 1-rep max 122.5 kg (105 x 5, 2026-09-28)\nbench press: heaviest 70 kg (3 reps, 2026-10-05); best estimated 1-rep max 79.2 kg (62.5 x 8, 2026-09-25)\ndeadlift: heaviest 135 kg (3 reps, 2026-10-07); best estimated 1-rep max 148.5 kg (135 x 3, 2026-10-07)\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 4\nweek of 2026-09-21   6577.5 kg  ##############################\nweek of 2026-09-28     6520 kg  ##############################\nweek of 2026-10-05      945 kg  ####\nTotal 14042.5 kg over 3 weeks - the same as all 13 entries added up, 14042.5 kg.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 1\ndate (YYYY-MM-DD, nothing for 2026-10-07)> 2026-10-12\nExercises for 2026-10-12 Mon, one a line, like squat 3x5 100; nothing to stop.\nexercise> squat 3x5 112.5\n  squat 3 x 5 x 112.5 kg: volume 1687.5 kg\nexercise> bench press 5x5 65\n  bench press 5 x 5 x 65 kg: volume 1625 kg\nexercise> \n2 added, 15 in the log.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 3\nsquat: heaviest 120 kg (1 rep, 2026-10-05); best estimated 1-rep max 131.3 kg (112.5 x 5, 2026-10-12)\nbench press: heaviest 70 kg (3 reps, 2026-10-05); best estimated 1-rep max 79.2 kg (62.5 x 8, 2026-09-25)\ndeadlift: heaviest 135 kg (3 reps, 2026-10-07); best estimated 1-rep max 148.5 kg (135 x 3, 2026-10-07)\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 4\nweek of 2026-09-21   6577.5 kg  ##############################\nweek of 2026-09-28     6520 kg  ##############################\nweek of 2026-10-05      945 kg  ####\nweek of 2026-10-12   3312.5 kg  ###############\nTotal 17355 kg over 4 weeks - the same as all 15 entries added up, 17355 kg.\n\n1) log a workout  2) history  3) records  4) weekly volume  5) sample  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "histogram-builder",
      "caseId": "113-histogram-builder",
      "title": "Histogram builder"
    }
  ],
  "updated": "2026-10-10"
}
