{
  "id": "P015",
  "slug": "date-calculator",
  "key": "P015-date-calculator",
  "title": "Date calculator",
  "summary": "Days between two dates, the day of the week, a date plus or minus some days, whether a year is a leap year and why, and someone's age on a date - including the two rules for a 29 February birthday - on the Gregorian calendar from 1583 to 9999, from a text menu.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P015 - Date calculator\n\nFive things people ask a calendar: how many days lie between two dates, what\nday of the week a date is (and which day of its year), what date comes some\ndays before or after another, whether a year is a leap year - and why - and\nhow old someone is on a given date. Everything is on the Gregorian calendar,\nfrom 1583 (the first full year after the calendar began in October 1582) to\n9999. A text menu.\n\n- `main.eml` - the menu and its questions, with their checks, and what the\n  screen shows\n- `dates.eml` - the calendar: the leap-year rule and its reason, the days in\n  a month, counting days, turning a count back into a date, the day of the\n  week, and reading and writing YYYY-MM-DD\n- `text.eml` - trimming, whole numbers with or without a sign, grouped\n  digits and counts\n\nHow it counts: a date becomes the number of days since 0001-01-01 by a\nclosed formula - 365 a year, plus one for every leap year before it, which is\nevery 4th, less every 100th, plus every 400th - so the days between two dates\nare a subtraction however far apart they are (3,074,245 days from 1583-01-01\nto 9999-12-31), with no loop over the years. A count turns back into a date by\ntaking off whole blocks of 400 years (146,097 days), 100 years (36,524), 4\nyears (1,461) and single years; the last day of a long block would count as\na fifth short block, so that count is held at 3. The day of the week is\nZeller's congruence, which treats January and February as months 13 and 14 of\nthe year before so that the leap day falls at the end of the year.\n\nAge is a comparison, not a division: the difference in years, less one if\nthe birthday has not come yet. A 29 February birthday has no date in other\nyears; here it falls on 1 March, so on 28 February 2025 someone born on\n2004-02-29 is 20 - and the screen adds that where the birthday is kept on 28\nFebruary instead, they are 21. Both rules are used in real law and software.\n\nWhat is checked: a date is written YYYY-MM-DD with a year from 1583 to 9999,\na month that exists and a day that month has (2023-02-29 is refused,\n2024-02-29 is not); a number of days is whole, may start with + or -, is at\nmost 3,000,000 either way, and must not lead outside the calendar's range; a\nyear is one from 1583 to 9999; a birth date may not come after the date of\nthe age. Anything else asks again; nothing cancels.\n\nSessions: `sessions/basic.in` counts the days across the end of February in\na leap year, an ordinary year and 1900 (divisible by 100, so not a leap year),\nand back from 2026-10-03 to 2025-01-01; names the weekday of 2024-02-29 and of\n1969-07-20; adds 30 days to 2024-01-31 and takes 1,000 from 2026-10-03; asks\nabout 2000, 1900, 2024 and 2023; and gives three ages, two of them for a 29\nFebruary birthday, on 28 February and on 1 March; `sessions/bad-input.in`\ntypes dates in the wrong shape, out of range or that do not exist, numbers of\ndays that are words, doubly signed, too large or that lead out of range, years\nout of range, a birth date after the age date, and then counts the whole range.\n\nBuilt on the verified corpus cases `days-between-dates` (days between two\ndates, with the full leap-year rule), `day-of-week-zeller` (Zeller's\ncongruence) and `age-at-date-leap-day` (age as a comparison, and the two\nrules for a 29 February birthday).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P015 date calculator: days between two dates, the day of the week, a date\n# plus or minus some days, whether a year is a leap year and why, and\n# someone's age on a date - all on the Gregorian calendar from 1583 to 9999.\nimport dates\nimport text\n\n3000000 => most_days\n\ndef ask_date(prompt):\n    # A date typed as YYYY-MM-DD, asked again until one is typed; None if the\n    # answer is empty, which cancels.\n    while True:\n        text.trim(input(prompt)) => answer\n        if answer == \"\":\n            return None\n        dates.parse(answer) => r\n        if r[0] == None:\n            r[1] ^0\n        else:\n            return r[0]\n\ndef ask_number(prompt, low, high, message):\n    # A whole number from low to high, asked again until one is typed; -1 if\n    # the answer is empty, which cancels.\n    while True:\n        text.trim(input(prompt)) => answer\n        if answer == \"\":\n            return 0 - 1\n        text.number(answer) => n\n        if n >= low and n <= high:\n            return n\n        message ^0\n\ndef gap(n):\n    # \"2 days\", \"400 days (57 weeks and 1 day)\".\n    text.count(n, \"day\") => s\n    if n >= 7:\n        s + \" (\" + text.count(dates.div(n, 7), \"week\") => s\n        if n % 7 > 0:\n            s + \" and \" + text.count(n % 7, \"day\") => s\n        s + \")\" => s\n    return s\n\ndates.day_number([dates.first_year, 1, 1]) => first_day\ndates.day_number([dates.last_year, 12, 31]) => last_day\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Date calculator ==\" ^0\n    \"1) days between  2) day of the week  3) add or take away days\" ^0\n    \"4) leap year  5) age on a date  6) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        ask_date(\"first date> \") => a\n        None => b\n        if a != None:\n            ask_date(\"second date> \") => b\n        if b == None:\n            \"Cancelled.\" ^0\n        else:\n            dates.day_number(b) - dates.day_number(a) => d\n            if d == 0:\n                (dates.show(b) + \" is the same day as \" + dates.show(a) + \".\") ^0\n            elif d > 0:\n                (dates.show(b) + \" is \" + gap(d) + \" after \" + dates.show(a) + \".\") ^0\n            else:\n                (dates.show(b) + \" is \" + gap(0 - d) + \" before \" + dates.show(a) + \".\") ^0\n    elif choice == \"2\":\n        ask_date(\"date> \") => a\n        if a == None:\n            \"Cancelled.\" ^0\n        else:\n            dates.day_number(a) - dates.day_number([a[0], 1, 1]) + 1 => day_of_year\n            365 => year_length\n            if dates.is_leap(a[0]):\n                366 => year_length\n            (dates.show(a) + \" is a \" + dates.weekday(a) + \", day \" + str(day_of_year) + \" of \" + str(year_length) + \".\") ^0\n    elif choice == \"3\":\n        ask_date(\"date> \") => a\n        None => result\n        while a != None and result == None:\n            text.trim(input(\"days (like 30 or -30)> \")) => answer\n            text.signed(answer) => s\n            if answer == \"\":\n                None => a\n            elif not s[1]:\n                \"Type a whole number of days like 30 or -30, or nothing to cancel.\" ^0\n            elif s[0] > most_days or s[0] < 0 - most_days:\n                \"At most 3,000,000 days either way.\" ^0\n            elif dates.day_number(a) + s[0] < first_day:\n                \"That goes before 1583-01-01.\" ^0\n            elif dates.day_number(a) + s[0] > last_day:\n                \"That goes past 9999-12-31.\" ^0\n            else:\n                dates.from_day_number(dates.day_number(a) + s[0]) => result\n        if a == None:\n            \"Cancelled.\" ^0\n        else:\n            \" + \" + text.count(s[0], \"day\") => step\n            if s[0] < 0:\n                \" - \" + text.count(0 - s[0], \"day\") => step\n            (dates.show(a) + step + \" = \" + dates.show(result) + \", a \" + dates.weekday(result) + \".\") ^0\n    elif choice == \"4\":\n        ask_number(\"year> \", dates.first_year, dates.last_year, \"Type a year from 1583 to 9999, or nothing to cancel.\") => y\n        if y == 0 - 1:\n            \"Cancelled.\" ^0\n        elif dates.is_leap(y):\n            (str(y) + \" is a leap year: \" + dates.leap_reason(y) + \".\") ^0\n        else:\n            (str(y) + \" is not a leap year: \" + dates.leap_reason(y) + \".\") ^0\n    elif choice == \"5\":\n        ask_date(\"born on> \") => born\n        None => on\n        if born != None:\n            ask_date(\"age on> \") => on\n        if on == None:\n            \"Cancelled.\" ^0\n        elif dates.day_number(on) < dates.day_number(born):\n            \"The birth date comes after that date.\" ^0\n        else:\n            on[0] - born[0] => years\n            if on[1] < born[1] or (on[1] == born[1] and on[2] < born[2]):\n                years - 1 => years\n            dates.day_number(on) - dates.day_number(born) => lived\n            (\"On \" + dates.show(on) + \" someone born \" + dates.show(born) + \" is \" + text.count(years, \"year\") + \" old (\" + text.count(lived, \"day\") + \").\") ^0\n            if born[1] == 2 and born[2] == 29 and on[1] == 2 and on[2] == 28 and on[0] > born[0] and not dates.is_leap(on[0]):\n                (\"Where a 29 February birthday is kept on 28 February in other years, they are \" + str(years + 1) + \".\") ^0\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 text\nmost_days = 3000000\n\ndef ask_date(prompt):\n    while True:\n        answer = text.trim(input(prompt))\n        if answer == \"\":\n            return None\n        r = dates.parse(answer)\n        if r[0] == None:\n            print(r[1])\n        else:\n            return r[0]\n\ndef ask_number(prompt, low, high, message):\n    while True:\n        answer = text.trim(input(prompt))\n        if answer == \"\":\n            return 0 - 1\n        n = text.number(answer)\n        if n >= low and n <= high:\n            return n\n        print(message)\n\ndef gap(n):\n    s = text.count(n, \"day\")\n    if n >= 7:\n        s = s + \" (\" + text.count(dates.div(n, 7), \"week\")\n        if n % 7 > 0:\n            s = s + \" and \" + text.count(n % 7, \"day\")\n        s = s + \")\"\n    return s\n\nfirst_day = dates.day_number([dates.first_year, 1, 1])\nlast_day = dates.day_number([dates.last_year, 12, 31])\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Date calculator ==\")\n    print(\"1) days between  2) day of the week  3) add or take away days\")\n    print(\"4) leap year  5) age on a date  6) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\":\n        a = ask_date(\"first date> \")\n        b = None\n        if a != None:\n            b = ask_date(\"second date> \")\n        if b == None:\n            print(\"Cancelled.\")\n        else:\n            d = dates.day_number(b) - dates.day_number(a)\n            if d == 0:\n                print(dates.show(b) + \" is the same day as \" + dates.show(a) + \".\")\n            elif d > 0:\n                print(dates.show(b) + \" is \" + gap(d) + \" after \" + dates.show(a) + \".\")\n            else:\n                print(dates.show(b) + \" is \" + gap(0 - d) + \" before \" + dates.show(a) + \".\")\n    elif choice == \"2\":\n        a = ask_date(\"date> \")\n        if a == None:\n            print(\"Cancelled.\")\n        else:\n            day_of_year = dates.day_number(a) - dates.day_number([a[0], 1, 1]) + 1\n            year_length = 365\n            if dates.is_leap(a[0]):\n                year_length = 366\n            print(dates.show(a) + \" is a \" + dates.weekday(a) + \", day \" + str(day_of_year) + \" of \" + str(year_length) + \".\")\n    elif choice == \"3\":\n        a = ask_date(\"date> \")\n        result = None\n        while a != None and result == None:\n            answer = text.trim(input(\"days (like 30 or -30)> \"))\n            s = text.signed(answer)\n            if answer == \"\":\n                a = None\n            elif not s[1]:\n                print(\"Type a whole number of days like 30 or -30, or nothing to cancel.\")\n            elif s[0] > most_days or s[0] < 0 - most_days:\n                print(\"At most 3,000,000 days either way.\")\n            elif dates.day_number(a) + s[0] < first_day:\n                print(\"That goes before 1583-01-01.\")\n            elif dates.day_number(a) + s[0] > last_day:\n                print(\"That goes past 9999-12-31.\")\n            else:\n                result = dates.from_day_number(dates.day_number(a) + s[0])\n        if a == None:\n            print(\"Cancelled.\")\n        else:\n            step = \" + \" + text.count(s[0], \"day\")\n            if s[0] < 0:\n                step = \" - \" + text.count(0 - s[0], \"day\")\n            print(dates.show(a) + step + \" = \" + dates.show(result) + \", a \" + dates.weekday(result) + \".\")\n    elif choice == \"4\":\n        y = ask_number(\"year> \", dates.first_year, dates.last_year, \"Type a year from 1583 to 9999, or nothing to cancel.\")\n        if y == 0 - 1:\n            print(\"Cancelled.\")\n        elif dates.is_leap(y):\n            print(str(y) + \" is a leap year: \" + dates.leap_reason(y) + \".\")\n        else:\n            print(str(y) + \" is not a leap year: \" + dates.leap_reason(y) + \".\")\n    elif choice == \"5\":\n        born = ask_date(\"born on> \")\n        on = None\n        if born != None:\n            on = ask_date(\"age on> \")\n        if on == None:\n            print(\"Cancelled.\")\n        elif dates.day_number(on) < dates.day_number(born):\n            print(\"The birth date comes after that date.\")\n        else:\n            years = on[0] - born[0]\n            if on[1] < born[1] or on[1] == born[1] and on[2] < born[2]:\n                years = years - 1\n            lived = dates.day_number(on) - dates.day_number(born)\n            print(\"On \" + dates.show(on) + \" someone born \" + dates.show(born) + \" is \" + text.count(years, \"year\") + \" old (\" + text.count(lived, \"day\") + \").\")\n            if born[1] == 2 and born[2] == 29 and on[1] == 2 and on[2] == 28 and on[0] > born[0] and not dates.is_leap(on[0]):\n                print(\"Where a 29 February birthday is kept on 28 February in other years, they are \" + str(years + 1) + \".\")\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "dates.eml",
      "eml": "# P015 date calculator - dates on the Gregorian calendar. A date is [year, month, day]\n# for the years 1583 to 9999 (the calendar began in October 1582). Days are\n# counted from 0001-01-01 by a closed formula, so the gap between two dates is\n# a subtraction however far apart they are, and the count turns back into a\n# date by taking off whole 400-, 100-, 4- and 1-year blocks.\n\n[\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"] => month_names\n[\"Saturday\", \"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"] => day_names\n1583 => first_year\n9999 => last_year\n\ndef div(a, b):\n    # a // b for whole numbers a >= 0 and b > 0 (small enough to stay exact).\n    return int((a - a % b) / b)\n\ndef is_leap(year):\n    # Divisible by 4 is a leap year, except divisible by 100, except\n    # divisible by 400 after all.\n    if year % 400 == 0:\n        return True\n    if year % 100 == 0:\n        return False\n    return year % 4 == 0\n\ndef leap_reason(year):\n    # Why year is or is not a leap year, naming the rule that decides it.\n    if year % 400 == 0:\n        return \"divisible by 400\"\n    if year % 100 == 0:\n        return \"divisible by 100 but not by 400\"\n    if year % 4 == 0:\n        return \"divisible by 4 and not by 100\"\n    return \"not divisible by 4\"\n\ndef days_in_month(year, month):\n    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] => lengths\n    if month == 2 and is_leap(year):\n        return 29\n    return lengths[month - 1]\n\ndef day_number(d):\n    # Days from 0001-01-01 (day 0) to d: 365 a year, plus one for every leap\n    # year before it - every 4th, less every 100th, plus every 400th.\n    d[0] - 1 => p\n    365 * p + div(p, 4) - div(p, 100) + div(p, 400) => n\n    for m in [1:d[1] - 1]:\n        n + days_in_month(d[0], m) => n\n    return n + d[2] - 1\n\ndef from_day_number(n):\n    # The date n days after 0001-01-01. A 400-year block has 146097 days, a\n    # 100-year block 36524, a 4-year block 1461; the last day of a long block\n    # would count as a fifth short one, so that count is held at 3.\n    div(n, 146097) => n400\n    n % 146097 => n\n    div(n, 36524) => n100\n    if n100 == 4:\n        3 => n100\n    n - 36524 * n100 => n\n    div(n, 1461) => n4\n    n % 1461 => n\n    div(n, 365) => n1\n    if n1 == 4:\n        3 => n1\n    n - 365 * n1 => n\n    400 * n400 + 100 * n100 + 4 * n4 + n1 + 1 => year\n    1 => month\n    while n >= days_in_month(year, month):\n        n - days_in_month(year, month) => n\n        month + 1 => month\n    return [year, month, n + 1]\n\ndef weekday(d):\n    # The day of the week by Zeller's congruence: January and February count\n    # as months 13 and 14 of the year before, which puts the leap day at the\n    # end of the year.\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    div(y, 100) => j\n    return day_names[(d[2] + div(13 * (m + 1), 5) + k + div(k, 4) + div(j, 4) + 5 * j) % 7]\n\ndef show(d):\n    return str(d[0]) + \"-\" + (\"%02d\" % d[1]) + \"-\" + (\"%02d\" % d[2])\n\ndef parse(s):\n    # [date, \"\"] if s is a date written YYYY-MM-DD, otherwise [None, what is\n    # wrong with it].\n    \"Write the date as YYYY-MM-DD, for example 2024-02-29.\" => shape\n    if len(s) != 10 or s[4] != \"-\" or s[7] != \"-\":\n        return [None, shape]\n    for i in [0, 1, 2, 3, 5, 6, 8, 9]:\n        if not (s[i] in \"0123456789\"):\n            return [None, shape]\n    int(s[0:4]) => y\n    int(s[5:7]) => m\n    int(s[8:10]) => day\n    if y < first_year or y > last_year:\n        return [None, \"The year must be from 1583 to 9999 (the Gregorian calendar).\"]\n    if m < 1 or m > 12:\n        return [None, \"There is no month \" + str(m) + \".\"]\n    if day < 1 or day > days_in_month(y, m):\n        return [None, s + \" is not a date: \" + month_names[m - 1] + \" \" + str(y) + \" has \" + str(days_in_month(y, m)) + \" days.\"]\n    return [[y, m, day], \"\"]\n",
      "python": "month_names = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"]\nday_names = [\"Saturday\", \"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"]\nfirst_year = 1583\nlast_year = 9999\n\ndef div(a, b):\n    return int((a - a % b) / b)\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 leap_reason(year):\n    if year % 400 == 0:\n        return \"divisible by 400\"\n    if year % 100 == 0:\n        return \"divisible by 100 but not by 400\"\n    if year % 4 == 0:\n        return \"divisible by 4 and not by 100\"\n    return \"not divisible by 4\"\n\ndef days_in_month(year, month):\n    lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n    if month == 2 and is_leap(year):\n        return 29\n    return lengths[month - 1]\n\ndef day_number(d):\n    p = d[0] - 1\n    n = 365 * p + div(p, 4) - div(p, 100) + div(p, 400)\n    for m in range(1, d[1]):\n        n = n + days_in_month(d[0], m)\n    return n + d[2] - 1\n\ndef from_day_number(n):\n    n400 = div(n, 146097)\n    n = n % 146097\n    n100 = div(n, 36524)\n    if n100 == 4:\n        n100 = 3\n    n = n - 36524 * n100\n    n4 = div(n, 1461)\n    n = n % 1461\n    n1 = div(n, 365)\n    if n1 == 4:\n        n1 = 3\n    n = n - 365 * n1\n    year = 400 * n400 + 100 * n100 + 4 * n4 + n1 + 1\n    month = 1\n    while n >= days_in_month(year, month):\n        n = n - days_in_month(year, month)\n        month = month + 1\n    return [year, month, 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 = div(y, 100)\n    return day_names[(d[2] + div(13 * (m + 1), 5) + k + div(k, 4) + div(j, 4) + 5 * j) % 7]\n\ndef show(d):\n    return str(d[0]) + \"-\" + \"%02d\" % d[1] + \"-\" + \"%02d\" % d[2]\n\ndef parse(s):\n    shape = \"Write the date as YYYY-MM-DD, for example 2024-02-29.\"\n    if len(s) != 10 or s[4] != \"-\" or s[7] != \"-\":\n        return [None, shape]\n    for i in [0, 1, 2, 3, 5, 6, 8, 9]:\n        if not s[i] in \"0123456789\":\n            return [None, shape]\n    y = int(s[0:4])\n    m = int(s[5:7])\n    day = int(s[8:10])\n    if y < first_year or y > last_year:\n        return [None, \"The year must be from 1583 to 9999 (the Gregorian calendar).\"]\n    if m < 1 or m > 12:\n        return [None, \"There is no month \" + str(m) + \".\"]\n    if day < 1 or day > days_in_month(y, m):\n        return [None, s + \" is not a date: \" + month_names[m - 1] + \" \" + str(y) + \" has \" + str(days_in_month(y, m)) + \" days.\"]\n    return [[y, m, day], \"\"]\n"
    },
    {
      "name": "text.eml",
      "eml": "# P015 date calculator - reading what is typed and writing counts. The\n# interpreter that checks every session does not run string methods yet, so\n# the text handling is written out here.\n\ndef trim(s):\n    # s without the spaces at either end.\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 number(s):\n    # The value of s if it is digits only (at least one), otherwise -1.\n    if s == \"\":\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in \"0123456789\"):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\n\ndef signed(s):\n    # [value, True] for digits with an optional + or - in front, otherwise\n    # [0, False].\n    1 => sign\n    s => rest\n    if len(s) > 0 and (s[0] == \"+\" or s[0] == \"-\"):\n        if s[0] == \"-\":\n            0 - 1 => sign\n        s[1:len(s)] => rest\n    number(rest) => n\n    if n == 0 - 1:\n        return [0, False]\n    return [sign * n, True]\n\ndef grouped(n):\n    # A whole number n >= 0 as 1,234,567.\n    str(n) => digits\n    \"\" => out\n    len(digits) => i\n    while i > 3:\n        \",\" + digits[i - 3:i] + out => out\n        i - 3 => i\n    return digits[0:i] + out\n\ndef count(n, word):\n    # \"1 day\", \"1,000 days\".\n    if n == 1:\n        return \"1 \" + word\n    return grouped(n) + \" \" + word + \"s\"\n",
      "python": "def 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 number(s):\n    if s == \"\":\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in \"0123456789\":\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n\ndef signed(s):\n    sign = 1\n    rest = s\n    if len(s) > 0 and (s[0] == \"+\" or s[0] == \"-\"):\n        if s[0] == \"-\":\n            sign = 0 - 1\n        rest = s[1:len(s)]\n    n = number(rest)\n    if n == 0 - 1:\n        return [0, False]\n    return [sign * n, True]\n\ndef grouped(n):\n    digits = str(n)\n    out = \"\"\n    i = len(digits)\n    while i > 3:\n        out = \",\" + digits[i - 3:i] + out\n        i = i - 3\n    return digits[0:i] + out\n\ndef count(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return grouped(n) + \" \" + word + \"s\"\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\n1\n2024/02/28\n2023-02-29\n1582-12-31\n2024-13-01\n\n3\n2024-01-31\nabc\n+-5\n9999999\n-200000\n\n3\n9999-12-31\n1\n+0\n4\nabc\n1582\n10000\n\n5\n2030-01-01\n2026-10-03\n2\n\n1\n1583-01-01\n9999-12-31\n6\n",
      "screen": "\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 1\nfirst date> 2024/02/28\nWrite the date as YYYY-MM-DD, for example 2024-02-29.\nfirst date> 2023-02-29\n2023-02-29 is not a date: February 2023 has 28 days.\nfirst date> 1582-12-31\nThe year must be from 1583 to 9999 (the Gregorian calendar).\nfirst date> 2024-13-01\nThere is no month 13.\nfirst date> \nCancelled.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 3\ndate> 2024-01-31\ndays (like 30 or -30)> abc\nType a whole number of days like 30 or -30, or nothing to cancel.\ndays (like 30 or -30)> +-5\nType a whole number of days like 30 or -30, or nothing to cancel.\ndays (like 30 or -30)> 9999999\nAt most 3,000,000 days either way.\ndays (like 30 or -30)> -200000\nThat goes before 1583-01-01.\ndays (like 30 or -30)> \nCancelled.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 3\ndate> 9999-12-31\ndays (like 30 or -30)> 1\nThat goes past 9999-12-31.\ndays (like 30 or -30)> +0\n9999-12-31 + 0 days = 9999-12-31, a Friday.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 4\nyear> abc\nType a year from 1583 to 9999, or nothing to cancel.\nyear> 1582\nType a year from 1583 to 9999, or nothing to cancel.\nyear> 10000\nType a year from 1583 to 9999, or nothing to cancel.\nyear> \nCancelled.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 5\nborn on> 2030-01-01\nage on> 2026-10-03\nThe birth date comes after that date.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 2\ndate> \nCancelled.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 1\nfirst date> 1583-01-01\nsecond date> 9999-12-31\n9999-12-31 is 3,074,245 days (439,177 weeks and 6 days) after 1583-01-01.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2024-02-28\n2024-03-01\n1\n2023-02-28\n2023-03-01\n1\n1900-02-28\n1900-03-01\n1\n2026-10-03\n2025-01-01\n2\n2024-02-29\n2\n1969-07-20\n3\n2024-01-31\n30\n3\n2026-10-03\n-1000\n4\n2000\n4\n1900\n4\n2024\n4\n2023\n5\n2004-02-29\n2025-02-28\n5\n2004-02-29\n2025-03-01\n5\n1990-05-17\n2026-10-03\n6\n",
      "screen": "\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 1\nfirst date> 2024-02-28\nsecond date> 2024-03-01\n2024-03-01 is 2 days after 2024-02-28.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 1\nfirst date> 2023-02-28\nsecond date> 2023-03-01\n2023-03-01 is 1 day after 2023-02-28.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 1\nfirst date> 1900-02-28\nsecond date> 1900-03-01\n1900-03-01 is 1 day after 1900-02-28.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 1\nfirst date> 2026-10-03\nsecond date> 2025-01-01\n2025-01-01 is 640 days (91 weeks and 3 days) before 2026-10-03.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 2\ndate> 2024-02-29\n2024-02-29 is a Thursday, day 60 of 366.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 2\ndate> 1969-07-20\n1969-07-20 is a Sunday, day 201 of 365.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 3\ndate> 2024-01-31\ndays (like 30 or -30)> 30\n2024-01-31 + 30 days = 2024-03-01, a Friday.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 3\ndate> 2026-10-03\ndays (like 30 or -30)> -1000\n2026-10-03 - 1,000 days = 2024-01-07, a Sunday.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 4\nyear> 2000\n2000 is a leap year: divisible by 400.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 4\nyear> 1900\n1900 is not a leap year: divisible by 100 but not by 400.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 4\nyear> 2024\n2024 is a leap year: divisible by 4 and not by 100.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 4\nyear> 2023\n2023 is not a leap year: not divisible by 4.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 5\nborn on> 2004-02-29\nage on> 2025-02-28\nOn 2025-02-28 someone born 2004-02-29 is 20 years old (7,670 days).\nWhere a 29 February birthday is kept on 28 February in other years, they are 21.\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 5\nborn on> 2004-02-29\nage on> 2025-03-01\nOn 2025-03-01 someone born 2004-02-29 is 21 years old (7,671 days).\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 5\nborn on> 1990-05-17\nage on> 2026-10-03\nOn 2026-10-03 someone born 1990-05-17 is 36 years old (13,288 days).\n\n== Date calculator ==\n1) days between  2) day of the week  3) add or take away days\n4) leap year  5) age on a date  6) quit\nchoice> 6\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)"
    },
    {
      "slug": "age-at-date-leap-day",
      "caseId": "246-age-at-date-leap-day",
      "title": "Age at a date — three defensible answers on 29 February"
    }
  ],
  "updated": "2026-10-03"
}
