{
  "id": "P055",
  "slug": "habit-tracker",
  "key": "P055-habit-tracker",
  "title": "Habit tracker",
  "summary": "Up to five habits checked in on real calendar days: the streak still running, the longest streak, check-ins taken back, and any month drawn as a Monday-first calendar with the days done - dates counted from 1900 and weekdays by Zeller's congruence.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P055 - Habit tracker\n\nUp to five habits, checked in on real calendar days. For each habit the\ntracker shows the streak still running, the longest streak and the days\ndone this month, and draws any month as a calendar with the days done\nmarked. Check-ins can be taken back, or made for an earlier day you forgot.\nThe program has no clock: \"today\" starts as 2026-10-12 and can be set to\nany date.\n\n- `main.eml` - the menu, choosing a habit and a day, and the messages\n- `dates.eml` - calendar dates from 1900-01-01 to 2099-12-31: day\n  numbers, leap years, weekdays and reading a date\n- `habits.eml` - check-ins, streaks and the month calendar\n\nHow each part works:\n\n- A date becomes a day number - days since 1900-01-01 - by adding up the\n  years and months before it, as in the corpus case `days-between-dates`.\n  Consecutive days then have consecutive numbers, so a streak is a run of\n  numbers with no gap, and a habit's check-ins are a sorted list of them.\n- The weekday comes from Zeller's congruence, as in the corpus case\n  `day-of-week-zeller`: January and February count as months 13 and 14 of\n  the year before, which puts the leap day at the end of the year. It\n  places the first of a month in its column of the calendar, which starts\n  on Monday.\n- The current streak is the run ending today. If today is not checked in\n  yet, it is the run ending yesterday, because the day is not over. The\n  longest streak is the longest run ever, the earliest if two are equally\n  long.\n- A day after today cannot be checked in. Setting today to an earlier date\n  keeps the check-ins after it, and they still count in the longest\n  streak.\n\nWhat is checked: menu choices 1 to 8; a habit by its number or its name; a\nday as YYYY-MM-DD that is a real date between 1900-01-01 and 2099-12-31 and\nnot after today; a month as YYYY-MM; habit names up to 30 characters, each\nused once, at most five habits. An empty answer cancels, except for a day,\nwhere it means today and x cancels.\n\nSessions: `sessions/basic.in` loads two sample habits covering the six\nweeks up to Monday 2026-10-12.\n- Reading has three days missed: an 11-day streak since 2026-10-02, and a\n  longest of 19 days from 2026-09-12.\n- Running is on Mondays, Wednesdays and Fridays, so its streaks are single\n  days.\n- Calendars: October for reading, then September for running (picked by\n  name), with 13 runs.\n- Running is checked in again for today (it is already in), then for\n  Sunday, making a 2-day streak.\n- 2026-10-02 is taken back from reading, so the streak is 10 days.\n- A third habit is added and checked in.\n- After today moves to Wednesday 2026-10-14, no streak is running, since\n  nothing was checked in on the 13th.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- every action before there is a habit;\n- an empty name, a name over 30 characters, and a name used twice;\n- days 2026-13-01, 2026-02-30, the day after today, 1899-12-31, and x;\n- a check-in for today, then again;\n- taking back a day that was not checked in;\n- months 2026-13 and the coming November;\n- a sixth habit;\n- habit 9;\n- today set to 2026-02-29 (not a leap year), then to 2024-02-29.\n\nBuilt on the verified corpus cases `days-between-dates` (counting the days\nbetween two dates through a day number) and `day-of-week-zeller` (the\nweekday of a date by Zeller's congruence).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P055 habit tracker: up to five habits, checked in on real calendar days.\n# For each habit: the streak still running, the longest streak, and a month\n# drawn as a calendar with the days it was done.\nimport dates\nimport habits\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 counted(n, one, many):\n    if n == 1:\n        return \"1 \" + one\n    return str(n) + \" \" + many\n\ndef described(h, today):\n    habits.current_streak(h[1], today) => now\n    habits.longest_streak(h[1]) => best\n    dates.date_of(today) => t\n    h[0] + \": \" => s\n    if now > 0:\n        today => end\n        if not habits.has_day(h[1], today):\n            today - 1 => end\n        s + \"streak \" + counted(now, \"day\", \"days\") + \" (since \" + dates.shown(dates.date_of(end - now + 1)) + \")\" => s\n    else:\n        s + \"no streak running\" => s\n    if best[0] > 0:\n        s + \", longest \" + counted(best[0], \"day\", \"days\") + \" (from \" + dates.shown(dates.date_of(best[1])) + \")\" => s\n    s + \", \" + counted(habits.done_in_month(h[1], t[0], t[1]), \"day\", \"days\") + \" this month.\" => s\n    return s\n\ndef ask_habit(state):\n    # The index of a habit chosen by number or name, or -1 when the answer\n    # is empty.\n    state[1] => hs\n    if len(hs) == 1:\n        return 0\n    while True:\n        for k in [0:len(hs) - 1]:\n            (\"  \" + str(k + 1) + \") \" + hs[k][0]) ^0\n        trim(input(\"habit> \")) => answer\n        if answer == \"\":\n            return -1\n        for k in [0:len(hs) - 1]:\n            if answer == str(k + 1) or answer == hs[k][0]:\n                return k\n        \"Type a habit's number or its name.\" ^0\n\ndef ask_day(state, prompt):\n    # A day number not after today: an empty answer is today itself, or -1\n    # when the answer is \"x\" (cancel).\n    while True:\n        trim(input(prompt + \" (YYYY-MM-DD, nothing for today, x to cancel)> \")) => answer\n        if answer == \"\":\n            return state[0]\n        if answer == \"x\":\n            return -1\n        dates.parsed(answer) => d\n        if len(d) == 0:\n            \"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\" ^0\n        elif dates.day_number(d) > state[0]:\n            (\"That is after today, \" + dates.shown(dates.date_of(state[0])) + \".\") ^0\n        else:\n            return dates.day_number(d)\n\ndef check_in(state, undo):\n    if len(state[1]) == 0:\n        \"There is no habit yet - add one first.\" ^0\n        return\n    ask_habit(state) => k\n    if k == -1:\n        \"Cancelled.\" ^0\n        return\n    state[1][k] => h\n    ask_day(state, \"day\") => n\n    if n == -1:\n        \"Cancelled.\" ^0\n        return\n    dates.date_of(n) => d\n    dates.shown(d) + \", a \" + dates.day_name(d) => when\n    if undo:\n        if not habits.has_day(h[1], n):\n            (h[0] + \" was not checked in on \" + when + \".\") ^0\n            return\n        habits.without_day(h[1], n) => h[1]\n        (\"Took back \" + h[0] + \" on \" + when + \".\") ^0\n    else:\n        if habits.has_day(h[1], n):\n            (h[0] + \" is already checked in on \" + when + \".\") ^0\n            return\n        habits.with_day(h[1], n) => h[1]\n        (\"Checked in \" + h[0] + \" on \" + when + \".\") ^0\n    described(h, state[0]) ^0\n\ndef list_habits(state):\n    if len(state[1]) == 0:\n        \"There is no habit yet - add one first.\" ^0\n        return\n    dates.date_of(state[0]) => t\n    (\"Today is \" + dates.shown(t) + \", a \" + dates.day_name(t) + \".\") ^0\n    for h in state[1]:\n        described(h, state[0]) ^0\n\ndef month(state):\n    if len(state[1]) == 0:\n        \"There is no habit yet - add one first.\" ^0\n        return\n    ask_habit(state) => k\n    if k == -1:\n        \"Cancelled.\" ^0\n        return\n    state[1][k] => h\n    dates.date_of(state[0]) => t\n    while True:\n        trim(input(\"month (YYYY-MM, nothing for this month)> \")) => answer\n        if answer == \"\":\n            [t[0], t[1]] => ym\n            break\n        dates.parsed(answer + \"-01\") => d\n        if len(d) > 0:\n            [d[0], d[1]] => ym\n            break\n        \"Type a month like 2026-09.\" ^0\n    [\"\", \"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"][ym[1]] => name\n    (h[0] + \", \" + name + \" \" + str(ym[0]) + \":\") ^0\n    for line in habits.month_grid(h[1], ym[0], ym[1], state[0]):\n        line ^0\n    dates.day_number([ym[0], ym[1], 1]) => first\n    first + dates.days_in_month(ym[0], ym[1]) - 1 => last\n    if last > state[0]:\n        state[0] => last\n    if last < first:\n        \"That month has not started yet.\" ^0\n    else:\n        (\"Done on \" + str(habits.done_in_month(h[1], ym[0], ym[1])) + \" of \" + counted(last - first + 1, \"day\", \"days\") + \" so far.\") ^0\n\ndef new_habit(state):\n    if len(state[1]) == 5:\n        \"Five habits is the most - this tracker keeps them few.\" ^0\n        return\n    while True:\n        trim(input(\"name> \")) => name\n        if name == \"\":\n            \"Cancelled.\" ^0\n            return\n        False => taken\n        for h in state[1]:\n            if h[0] == name:\n                True => taken\n        if taken:\n            (\"There is a habit called \" + name + \" already.\") ^0\n        elif len(name) > 30:\n            \"Keep the name to 30 characters.\" ^0\n        else:\n            state[1] + [[name, []]] => state[1]\n            (\"Added \" + name + \".\") ^0\n            return\n\ndef set_today(state):\n    while True:\n        trim(input(\"today (YYYY-MM-DD)> \")) => answer\n        if answer == \"\":\n            \"Cancelled.\" ^0\n            return\n        dates.parsed(answer) => d\n        if len(d) > 0:\n            dates.day_number(d) => state[0]\n            (\"Today is now \" + dates.shown(d) + \", a \" + dates.day_name(d) + \".\") ^0\n            return\n        \"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\" ^0\n\ndef sample(state):\n    # Two habits over the last six weeks: reading on most days, with two\n    # breaks, and running on Mondays, Wednesdays and Fridays.\n    state[0] => t\n    [] => reading\n    [] => running\n    for back in [0:41]:\n        t - 41 + back => n\n        if not (back == 9 or back == 10 or back == 30):\n            reading + [n] => reading\n        dates.weekday(dates.date_of(n)) => w\n        if w == 0 or w == 2 or w == 4:\n            running + [n] => running\n    [[\"Read 20 pages\", reading], [\"Run\", running]] => state[1]\n    \"Two sample habits over the last six weeks: Read 20 pages and Run.\" ^0\n\n\"== Habit tracker ==\" ^0\n\"Check in a habit each day it is done; streaks count the days in a row.\" ^0\n# [today as a day number, the habits]\n[dates.day_number([2026, 10, 12]), []] => state\n(\"Today is 2026-10-12, a \" + dates.day_name([2026, 10, 12]) + \". Change it with 7) today.\") ^0\nTrue => looping\nwhile looping:\n    \"\" ^0\n    \"1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        check_in(state, False)\n    elif choice == \"2\":\n        check_in(state, True)\n    elif choice == \"3\":\n        list_habits(state)\n    elif choice == \"4\":\n        month(state)\n    elif choice == \"5\":\n        new_habit(state)\n    elif choice == \"6\":\n        sample(state)\n    elif choice == \"7\":\n        set_today(state)\n    elif choice == \"8\":\n        False => looping\n    else:\n        \"Pick a number from 1 to 8.\" ^0\n\"Bye.\" ^0\n",
      "python": "import dates\nimport habits\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 counted(n, one, many):\n    if n == 1:\n        return \"1 \" + one\n    return str(n) + \" \" + many\n\ndef described(h, today):\n    now = habits.current_streak(h[1], today)\n    best = habits.longest_streak(h[1])\n    t = dates.date_of(today)\n    s = h[0] + \": \"\n    if now > 0:\n        end = today\n        if not habits.has_day(h[1], today):\n            end = today - 1\n        s = s + \"streak \" + counted(now, \"day\", \"days\") + \" (since \" + dates.shown(dates.date_of(end - now + 1)) + \")\"\n    else:\n        s = s + \"no streak running\"\n    if best[0] > 0:\n        s = s + \", longest \" + counted(best[0], \"day\", \"days\") + \" (from \" + dates.shown(dates.date_of(best[1])) + \")\"\n    s = s + \", \" + counted(habits.done_in_month(h[1], t[0], t[1]), \"day\", \"days\") + \" this month.\"\n    return s\n\ndef ask_habit(state):\n    hs = state[1]\n    if len(hs) == 1:\n        return 0\n    while True:\n        for k in range(0, len(hs)):\n            print(\"  \" + str(k + 1) + \") \" + hs[k][0])\n        answer = trim(input(\"habit> \"))\n        if answer == \"\":\n            return -1\n        for k in range(0, len(hs)):\n            if answer == str(k + 1) or answer == hs[k][0]:\n                return k\n        print(\"Type a habit's number or its name.\")\n\ndef ask_day(state, prompt):\n    while True:\n        answer = trim(input(prompt + \" (YYYY-MM-DD, nothing for today, x to cancel)> \"))\n        if answer == \"\":\n            return state[0]\n        if answer == \"x\":\n            return -1\n        d = dates.parsed(answer)\n        if len(d) == 0:\n            print(\"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\")\n        elif dates.day_number(d) > state[0]:\n            print(\"That is after today, \" + dates.shown(dates.date_of(state[0])) + \".\")\n        else:\n            return dates.day_number(d)\n\ndef check_in(state, undo):\n    if len(state[1]) == 0:\n        print(\"There is no habit yet - add one first.\")\n        return\n    k = ask_habit(state)\n    if k == -1:\n        print(\"Cancelled.\")\n        return\n    h = state[1][k]\n    n = ask_day(state, \"day\")\n    if n == -1:\n        print(\"Cancelled.\")\n        return\n    d = dates.date_of(n)\n    when = dates.shown(d) + \", a \" + dates.day_name(d)\n    if undo:\n        if not habits.has_day(h[1], n):\n            print(h[0] + \" was not checked in on \" + when + \".\")\n            return\n        h[1] = habits.without_day(h[1], n)\n        print(\"Took back \" + h[0] + \" on \" + when + \".\")\n    else:\n        if habits.has_day(h[1], n):\n            print(h[0] + \" is already checked in on \" + when + \".\")\n            return\n        h[1] = habits.with_day(h[1], n)\n        print(\"Checked in \" + h[0] + \" on \" + when + \".\")\n    print(described(h, state[0]))\n\ndef list_habits(state):\n    if len(state[1]) == 0:\n        print(\"There is no habit yet - add one first.\")\n        return\n    t = dates.date_of(state[0])\n    print(\"Today is \" + dates.shown(t) + \", a \" + dates.day_name(t) + \".\")\n    for h in state[1]:\n        print(described(h, state[0]))\n\ndef month(state):\n    if len(state[1]) == 0:\n        print(\"There is no habit yet - add one first.\")\n        return\n    k = ask_habit(state)\n    if k == -1:\n        print(\"Cancelled.\")\n        return\n    h = state[1][k]\n    t = dates.date_of(state[0])\n    while True:\n        answer = trim(input(\"month (YYYY-MM, nothing for this month)> \"))\n        if answer == \"\":\n            ym = [t[0], t[1]]\n            break\n        d = dates.parsed(answer + \"-01\")\n        if len(d) > 0:\n            ym = [d[0], d[1]]\n            break\n        print(\"Type a month like 2026-09.\")\n    name = [\"\", \"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"][ym[1]]\n    print(h[0] + \", \" + name + \" \" + str(ym[0]) + \":\")\n    for line in habits.month_grid(h[1], ym[0], ym[1], state[0]):\n        print(line)\n    first = dates.day_number([ym[0], ym[1], 1])\n    last = first + dates.days_in_month(ym[0], ym[1]) - 1\n    if last > state[0]:\n        last = state[0]\n    if last < first:\n        print(\"That month has not started yet.\")\n    else:\n        print(\"Done on \" + str(habits.done_in_month(h[1], ym[0], ym[1])) + \" of \" + counted(last - first + 1, \"day\", \"days\") + \" so far.\")\n\ndef new_habit(state):\n    if len(state[1]) == 5:\n        print(\"Five habits is the most - this tracker keeps them few.\")\n        return\n    while True:\n        name = trim(input(\"name> \"))\n        if name == \"\":\n            print(\"Cancelled.\")\n            return\n        taken = False\n        for h in state[1]:\n            if h[0] == name:\n                taken = True\n        if taken:\n            print(\"There is a habit called \" + name + \" already.\")\n        elif len(name) > 30:\n            print(\"Keep the name to 30 characters.\")\n        else:\n            state[1] = state[1] + [[name, []]]\n            print(\"Added \" + name + \".\")\n            return\n\ndef set_today(state):\n    while True:\n        answer = trim(input(\"today (YYYY-MM-DD)> \"))\n        if answer == \"\":\n            print(\"Cancelled.\")\n            return\n        d = dates.parsed(answer)\n        if len(d) > 0:\n            state[0] = dates.day_number(d)\n            print(\"Today is now \" + dates.shown(d) + \", a \" + dates.day_name(d) + \".\")\n            return\n        print(\"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\")\n\ndef sample(state):\n    t = state[0]\n    reading = []\n    running = []\n    for back in range(0, 42):\n        n = t - 41 + back\n        if not (back == 9 or back == 10 or back == 30):\n            reading = reading + [n]\n        w = dates.weekday(dates.date_of(n))\n        if w == 0 or w == 2 or w == 4:\n            running = running + [n]\n    state[1] = [[\"Read 20 pages\", reading], [\"Run\", running]]\n    print(\"Two sample habits over the last six weeks: Read 20 pages and Run.\")\n\nprint(\"== Habit tracker ==\")\nprint(\"Check in a habit each day it is done; streaks count the days in a row.\")\nstate = [dates.day_number([2026, 10, 12]), []]\nprint(\"Today is 2026-10-12, a \" + dates.day_name([2026, 10, 12]) + \". Change it with 7) today.\")\nlooping = True\nwhile looping:\n    print(\"\")\n    print(\"1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        check_in(state, False)\n    elif choice == \"2\":\n        check_in(state, True)\n    elif choice == \"3\":\n        list_habits(state)\n    elif choice == \"4\":\n        month(state)\n    elif choice == \"5\":\n        new_habit(state)\n    elif choice == \"6\":\n        sample(state)\n    elif choice == \"7\":\n        set_today(state)\n    elif choice == \"8\":\n        looping = False\n    else:\n        print(\"Pick a number from 1 to 8.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "dates.eml",
      "eml": "# P055 habit tracker - real calendar dates, 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 streak is a run of numbers.\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"
    },
    {
      "name": "habits.eml",
      "eml": "# P055 habit tracker - check-ins. A habit is [name, days]: days is the\n# sorted list of day numbers it was done on, each at most once.\nimport dates\n\ndef has_day(days, n):\n    for x in days:\n        if x == n:\n            return True\n    return False\n\ndef with_day(days, n):\n    # days with n added in its place, keeping the list sorted.\n    [] => out\n    False => placed\n    for x in days:\n        if not placed and n < x:\n            out + [n] => out\n            True => placed\n        out + [x] => out\n    if not placed:\n        out + [n] => out\n    return out\n\ndef without_day(days, n):\n    [] => out\n    for x in days:\n        if x != n:\n            out + [x] => out\n    return out\n\ndef run_ending(days, n):\n    # How many days in a row, ending with day n, the habit was done.\n    0 => run\n    while has_day(days, n - run):\n        run + 1 => run\n    return run\n\ndef current_streak(days, today):\n    # The run that is still alive: ending today, or ending yesterday when\n    # today is not checked in yet - the day is not over.\n    if has_day(days, today):\n        return run_ending(days, today)\n    return run_ending(days, today - 1)\n\ndef longest_streak(days):\n    # [length, first day] of the longest run; the earliest when two are\n    # equally long.\n    0 => best\n    -1 => best_start\n    0 => run\n    -1 => start\n    -10 => prev\n    for x in days:\n        if x == prev + 1:\n            run + 1 => run\n        else:\n            1 => run\n            x => start\n        if run > best:\n            run => best\n            start => best_start\n        x => prev\n    return [best, best_start]\n\ndef rtrimmed(s):\n    len(s) => j\n    while j > 0 and s[j - 1] == \" \":\n        j - 1 => j\n    return s[0:j]\n\ndef month_grid(days, year, month, today):\n    # The month as a calendar, Monday first: each day with * when the habit\n    # was done, and . after the days still to come.\n    [\"  Mo  Tu  We  Th  Fr  Sa  Su\"] => lines\n    dates.weekday([year, month, 1]) => first\n    dates.day_number([year, month, 1]) => n0\n    \"    \" * first => line\n    first => col\n    for d in [1:dates.days_in_month(year, month)]:\n        n0 + d - 1 => n\n        str(d) => cell\n        while len(cell) < 3:\n            \" \" + cell => cell\n        if n > today:\n            cell + \".\" => cell\n        elif has_day(days, n):\n            cell + \"*\" => cell\n        else:\n            cell + \" \" => cell\n        line + cell => line\n        col + 1 => col\n        if col == 7:\n            lines + [rtrimmed(line)] => lines\n            \"\" => line\n            0 => col\n    if line != \"\":\n        lines + [rtrimmed(line)] => lines\n    return lines\n\ndef done_in_month(days, year, month):\n    dates.day_number([year, month, 1]) => a\n    a + dates.days_in_month(year, month) - 1 => b\n    0 => count\n    for x in days:\n        if x >= a and x <= b:\n            count + 1 => count\n    return count\n",
      "python": "import dates\n\ndef has_day(days, n):\n    for x in days:\n        if x == n:\n            return True\n    return False\n\ndef with_day(days, n):\n    out = []\n    placed = False\n    for x in days:\n        if not placed and n < x:\n            out = out + [n]\n            placed = True\n        out = out + [x]\n    if not placed:\n        out = out + [n]\n    return out\n\ndef without_day(days, n):\n    out = []\n    for x in days:\n        if x != n:\n            out = out + [x]\n    return out\n\ndef run_ending(days, n):\n    run = 0\n    while has_day(days, n - run):\n        run = run + 1\n    return run\n\ndef current_streak(days, today):\n    if has_day(days, today):\n        return run_ending(days, today)\n    return run_ending(days, today - 1)\n\ndef longest_streak(days):\n    best = 0\n    best_start = -1\n    run = 0\n    start = -1\n    prev = -10\n    for x in days:\n        if x == prev + 1:\n            run = run + 1\n        else:\n            run = 1\n            start = x\n        if run > best:\n            best = run\n            best_start = start\n        prev = x\n    return [best, best_start]\n\ndef rtrimmed(s):\n    j = len(s)\n    while j > 0 and s[j - 1] == \" \":\n        j = j - 1\n    return s[0:j]\n\ndef month_grid(days, year, month, today):\n    lines = [\"  Mo  Tu  We  Th  Fr  Sa  Su\"]\n    first = dates.weekday([year, month, 1])\n    n0 = dates.day_number([year, month, 1])\n    line = \"    \" * first\n    col = first\n    for d in range(1, dates.days_in_month(year, month)+1):\n        n = n0 + d - 1\n        cell = str(d)\n        while len(cell) < 3:\n            cell = \" \" + cell\n        if n > today:\n            cell = cell + \".\"\n        elif has_day(days, n):\n            cell = cell + \"*\"\n        else:\n            cell = cell + \" \"\n        line = line + cell\n        col = col + 1\n        if col == 7:\n            lines = lines + [rtrimmed(line)]\n            line = \"\"\n            col = 0\n    if line != \"\":\n        lines = lines + [rtrimmed(line)]\n    return lines\n\ndef done_in_month(days, year, month):\n    a = dates.day_number([year, month, 1])\n    b = a + dates.days_in_month(year, month) - 1\n    count = 0\n    for x in days:\n        if x >= a and x <= b:\n            count = count + 1\n    return count\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\n2\n3\n4\n5\n\n5\nA name that is far too long to keep\nWalk\n5\nWalk\n\n1\n2026-13-01\n2026-02-30\n2026-10-13\n1899-12-31\nx\n1\n\n1\n\n2\n2026-10-10\n4\n2026-13\n2026-11\n5\nA\n5\nB\n5\nC\n5\nD\n5\n1\n9\n\n7\n2026-02-29\n2024-02-29\n3\n8\n",
      "screen": "== Habit tracker ==\nCheck in a habit each day it is done; streaks count the days in a row.\nToday is 2026-10-12, a Monday. Change it with 7) today.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 0\nPick a number from 1 to 8.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> x\nPick a number from 1 to 8.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\nThere is no habit yet - add one first.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 2\nThere is no habit yet - add one first.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 3\nThere is no habit yet - add one first.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 4\nThere is no habit yet - add one first.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> \nCancelled.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> A name that is far too long to keep\nKeep the name to 30 characters.\nname> Walk\nAdded Walk.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> Walk\nThere is a habit called Walk already.\nname> \nCancelled.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\nday (YYYY-MM-DD, nothing for today, x to cancel)> 2026-13-01\nType a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\nday (YYYY-MM-DD, nothing for today, x to cancel)> 2026-02-30\nType a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\nday (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-13\nThat is after today, 2026-10-12.\nday (YYYY-MM-DD, nothing for today, x to cancel)> 1899-12-31\nType a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\nday (YYYY-MM-DD, nothing for today, x to cancel)> x\nCancelled.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\nday (YYYY-MM-DD, nothing for today, x to cancel)> \nChecked in Walk on 2026-10-12, a Monday.\nWalk: streak 1 day (since 2026-10-12), longest 1 day (from 2026-10-12), 1 day this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\nday (YYYY-MM-DD, nothing for today, x to cancel)> \nWalk is already checked in on 2026-10-12, a Monday.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 2\nday (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-10\nWalk was not checked in on 2026-10-10, a Saturday.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 4\nmonth (YYYY-MM, nothing for this month)> 2026-13\nType a month like 2026-09.\nmonth (YYYY-MM, nothing for this month)> 2026-11\nWalk, November 2026:\n  Mo  Tu  We  Th  Fr  Sa  Su\n                          1.\n  2.  3.  4.  5.  6.  7.  8.\n  9. 10. 11. 12. 13. 14. 15.\n 16. 17. 18. 19. 20. 21. 22.\n 23. 24. 25. 26. 27. 28. 29.\n 30.\nThat month has not started yet.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> A\nAdded A.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> B\nAdded B.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> C\nAdded C.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> D\nAdded D.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nFive habits is the most - this tracker keeps them few.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\n  1) Walk\n  2) A\n  3) B\n  4) C\n  5) D\nhabit> 9\nType a habit's number or its name.\n  1) Walk\n  2) A\n  3) B\n  4) C\n  5) D\nhabit> \nCancelled.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 7\ntoday (YYYY-MM-DD)> 2026-02-29\nType a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.\ntoday (YYYY-MM-DD)> 2024-02-29\nToday is now 2024-02-29, a Thursday.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 3\nToday is 2024-02-29, a Thursday.\nWalk: no streak running, longest 1 day (from 2026-10-12), 0 days this month.\nA: no streak running, 0 days this month.\nB: no streak running, 0 days this month.\nC: no streak running, 0 days this month.\nD: no streak running, 0 days this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "6\n3\n4\n1\n\n4\nRun\n2026-09\n1\n2\n\n1\n2\n2026-10-11\n2\n1\n2026-10-02\n5\nStretch\n1\n3\n\n7\n2026-10-14\n3\n8\n",
      "screen": "== Habit tracker ==\nCheck in a habit each day it is done; streaks count the days in a row.\nToday is 2026-10-12, a Monday. Change it with 7) today.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 6\nTwo sample habits over the last six weeks: Read 20 pages and Run.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 3\nToday is 2026-10-12, a Monday.\nRead 20 pages: streak 11 days (since 2026-10-02), longest 19 days (from 2026-09-12), 11 days this month.\nRun: streak 1 day (since 2026-10-12), longest 1 day (from 2026-09-02), 5 days this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 4\n  1) Read 20 pages\n  2) Run\nhabit> 1\nmonth (YYYY-MM, nothing for this month)> \nRead 20 pages, October 2026:\n  Mo  Tu  We  Th  Fr  Sa  Su\n              1   2*  3*  4*\n  5*  6*  7*  8*  9* 10* 11*\n 12* 13. 14. 15. 16. 17. 18.\n 19. 20. 21. 22. 23. 24. 25.\n 26. 27. 28. 29. 30. 31.\nDone on 11 of 12 days so far.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 4\n  1) Read 20 pages\n  2) Run\nhabit> Run\nmonth (YYYY-MM, nothing for this month)> 2026-09\nRun, September 2026:\n  Mo  Tu  We  Th  Fr  Sa  Su\n      1   2*  3   4*  5   6\n  7*  8   9* 10  11* 12  13\n 14* 15  16* 17  18* 19  20\n 21* 22  23* 24  25* 26  27\n 28* 29  30*\nDone on 13 of 30 days so far.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\n  1) Read 20 pages\n  2) Run\nhabit> 2\nday (YYYY-MM-DD, nothing for today, x to cancel)> \nRun is already checked in on 2026-10-12, a Monday.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\n  1) Read 20 pages\n  2) Run\nhabit> 2\nday (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-11\nChecked in Run on 2026-10-11, a Sunday.\nRun: streak 2 days (since 2026-10-11), longest 2 days (from 2026-10-11), 6 days this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 2\n  1) Read 20 pages\n  2) Run\nhabit> 1\nday (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-02\nTook back Read 20 pages on 2026-10-02, a Friday.\nRead 20 pages: streak 10 days (since 2026-10-03), longest 19 days (from 2026-09-12), 10 days this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 5\nname> Stretch\nAdded Stretch.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 1\n  1) Read 20 pages\n  2) Run\n  3) Stretch\nhabit> 3\nday (YYYY-MM-DD, nothing for today, x to cancel)> \nChecked in Stretch on 2026-10-12, a Monday.\nStretch: streak 1 day (since 2026-10-12), longest 1 day (from 2026-10-12), 1 day this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 7\ntoday (YYYY-MM-DD)> 2026-10-14\nToday is now 2026-10-14, a Wednesday.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 3\nToday is 2026-10-14, a Wednesday.\nRead 20 pages: no streak running, longest 19 days (from 2026-09-12), 10 days this month.\nRun: no streak running, longest 2 days (from 2026-10-11), 6 days this month.\nStretch: no streak running, longest 1 day (from 2026-10-12), 1 day this month.\n\n1) check in  2) take back  3) habits  4) month  5) new habit  6) sample  7) today  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "days-between-dates",
      "caseId": "111-days-between-dates",
      "title": "Days between dates"
    },
    {
      "slug": "day-of-week-zeller",
      "caseId": "098-day-of-week-zeller",
      "title": "Day of week (Zeller's congruence)"
    }
  ],
  "updated": "2026-10-10"
}
