Habit tracker
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.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Up to five habits, checked in on real calendar days. For each habit the tracker shows the streak still running, the longest streak and the days done this month, and draws any month as a calendar with the days done marked. Check-ins can be taken back, or made for an earlier day you forgot. The program has no clock: "today" starts as 2026-10-12 and can be set to any date.
main.eml- the menu, choosing a habit and a day, and the messagesdates.eml- calendar dates from 1900-01-01 to 2099-12-31: day numbers, leap years, weekdays and reading a datehabits.eml- check-ins, streaks and the month calendar
How each part works:
- A date becomes a day number - days since 1900-01-01 - by adding up the years and months before it, as in the corpus case
days-between-dates. Consecutive days then have consecutive numbers, so a streak is a run of numbers with no gap, and a habit's check-ins are a sorted list of them. - The weekday comes from Zeller's congruence, as in the corpus case
day-of-week-zeller: January and February count as months 13 and 14 of the year before, which puts the leap day at the end of the year. It places the first of a month in its column of the calendar, which starts on Monday. - The current streak is the run ending today. If today is not checked in yet, it is the run ending yesterday, because the day is not over. The longest streak is the longest run ever, the earliest if two are equally long.
- A day after today cannot be checked in. Setting today to an earlier date keeps the check-ins after it, and they still count in the longest streak.
What is checked: menu choices 1 to 8; a habit by its number or its name; a day as YYYY-MM-DD that is a real date between 1900-01-01 and 2099-12-31 and not after today; a month as YYYY-MM; habit names up to 30 characters, each used once, at most five habits. An empty answer cancels, except for a day, where it means today and x cancels.
Sessions: sessions/basic.in loads two sample habits covering the six weeks up to Monday 2026-10-12. - Reading has three days missed: an 11-day streak since 2026-10-02, and a longest of 19 days from 2026-09-12. - Running is on Mondays, Wednesdays and Fridays, so its streaks are single days. - Calendars: October for reading, then September for running (picked by name), with 13 runs. - Running is checked in again for today (it is already in), then for Sunday, making a 2-day streak. - 2026-10-02 is taken back from reading, so the streak is 10 days. - A third habit is added and checked in. - After today moves to Wednesday 2026-10-14, no streak is running, since nothing was checked in on the 13th.
sessions/bad-input.in gives: - menu choices 0 and x; - every action before there is a habit; - an empty name, a name over 30 characters, and a name used twice; - days 2026-13-01, 2026-02-30, the day after today, 1899-12-31, and x; - a check-in for today, then again; - taking back a day that was not checked in; - months 2026-13 and the coming November; - a sixth habit; - habit 9; - today set to 2026-02-29 (not a leap year), then to 2024-02-29.
Built on the verified corpus cases days-between-dates (counting the days between two dates through a day number) and day-of-week-zeller (the weekday of a date by Zeller's congruence).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Habit tracker ==
Check in a habit each day it is done; streaks count the days in a row.
Today is 2026-10-12, a Monday. Change it with 7) today.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 0
Pick a number from 1 to 8.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> x
Pick a number from 1 to 8.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
There is no habit yet - add one first.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 2
There is no habit yet - add one first.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 3
There is no habit yet - add one first.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 4
There is no habit yet - add one first.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name>
Cancelled.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> A name that is far too long to keep
Keep the name to 30 characters.
name> Walk
Added Walk.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> Walk
There is a habit called Walk already.
name>
Cancelled.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
day (YYYY-MM-DD, nothing for today, x to cancel)> 2026-13-01
Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.
day (YYYY-MM-DD, nothing for today, x to cancel)> 2026-02-30
Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.
day (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-13
That is after today, 2026-10-12.
day (YYYY-MM-DD, nothing for today, x to cancel)> 1899-12-31
Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.
day (YYYY-MM-DD, nothing for today, x to cancel)> x
Cancelled.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
day (YYYY-MM-DD, nothing for today, x to cancel)>
Checked in Walk on 2026-10-12, a Monday.
Walk: streak 1 day (since 2026-10-12), longest 1 day (from 2026-10-12), 1 day this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
day (YYYY-MM-DD, nothing for today, x to cancel)>
Walk is already checked in on 2026-10-12, a Monday.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 2
day (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-10
Walk was not checked in on 2026-10-10, a Saturday.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 4
month (YYYY-MM, nothing for this month)> 2026-13
Type a month like 2026-09.
month (YYYY-MM, nothing for this month)> 2026-11
Walk, November 2026:
Mo Tu We Th Fr Sa Su
1.
2. 3. 4. 5. 6. 7. 8.
9. 10. 11. 12. 13. 14. 15.
16. 17. 18. 19. 20. 21. 22.
23. 24. 25. 26. 27. 28. 29.
30.
That month has not started yet.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> A
Added A.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> B
Added B.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> C
Added C.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> D
Added D.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
Five habits is the most - this tracker keeps them few.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
1) Walk
2) A
3) B
4) C
5) D
habit> 9
Type a habit's number or its name.
1) Walk
2) A
3) B
4) C
5) D
habit>
Cancelled.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 7
today (YYYY-MM-DD)> 2026-02-29
Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.
today (YYYY-MM-DD)> 2024-02-29
Today is now 2024-02-29, a Thursday.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 3
Today is 2024-02-29, a Thursday.
Walk: no streak running, longest 1 day (from 2026-10-12), 0 days this month.
A: no streak running, 0 days this month.
B: no streak running, 0 days this month.
C: no streak running, 0 days this month.
D: no streak running, 0 days this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 8
Bye.
What was typed (46 lines)
0
x
1
2
3
4
5
5
A name that is far too long to keep
Walk
5
Walk
1
2026-13-01
2026-02-30
2026-10-13
1899-12-31
x
1
1
2
2026-10-10
4
2026-13
2026-11
5
A
5
B
5
C
5
D
5
1
9
7
2026-02-29
2024-02-29
3
8
basic
interpreter: byte-equal== Habit tracker ==
Check in a habit each day it is done; streaks count the days in a row.
Today is 2026-10-12, a Monday. Change it with 7) today.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 6
Two sample habits over the last six weeks: Read 20 pages and Run.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 3
Today is 2026-10-12, a Monday.
Read 20 pages: streak 11 days (since 2026-10-02), longest 19 days (from 2026-09-12), 11 days this month.
Run: streak 1 day (since 2026-10-12), longest 1 day (from 2026-09-02), 5 days this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 4
1) Read 20 pages
2) Run
habit> 1
month (YYYY-MM, nothing for this month)>
Read 20 pages, October 2026:
Mo Tu We Th Fr Sa Su
1 2* 3* 4*
5* 6* 7* 8* 9* 10* 11*
12* 13. 14. 15. 16. 17. 18.
19. 20. 21. 22. 23. 24. 25.
26. 27. 28. 29. 30. 31.
Done on 11 of 12 days so far.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 4
1) Read 20 pages
2) Run
habit> Run
month (YYYY-MM, nothing for this month)> 2026-09
Run, September 2026:
Mo Tu We Th Fr Sa Su
1 2* 3 4* 5 6
7* 8 9* 10 11* 12 13
14* 15 16* 17 18* 19 20
21* 22 23* 24 25* 26 27
28* 29 30*
Done on 13 of 30 days so far.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
1) Read 20 pages
2) Run
habit> 2
day (YYYY-MM-DD, nothing for today, x to cancel)>
Run is already checked in on 2026-10-12, a Monday.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
1) Read 20 pages
2) Run
habit> 2
day (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-11
Checked in Run on 2026-10-11, a Sunday.
Run: streak 2 days (since 2026-10-11), longest 2 days (from 2026-10-11), 6 days this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 2
1) Read 20 pages
2) Run
habit> 1
day (YYYY-MM-DD, nothing for today, x to cancel)> 2026-10-02
Took back Read 20 pages on 2026-10-02, a Friday.
Read 20 pages: streak 10 days (since 2026-10-03), longest 19 days (from 2026-09-12), 10 days this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 5
name> Stretch
Added Stretch.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 1
1) Read 20 pages
2) Run
3) Stretch
habit> 3
day (YYYY-MM-DD, nothing for today, x to cancel)>
Checked in Stretch on 2026-10-12, a Monday.
Stretch: streak 1 day (since 2026-10-12), longest 1 day (from 2026-10-12), 1 day this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 7
today (YYYY-MM-DD)> 2026-10-14
Today is now 2026-10-14, a Wednesday.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 3
Today is 2026-10-14, a Wednesday.
Read 20 pages: no streak running, longest 19 days (from 2026-09-12), 10 days this month.
Run: no streak running, longest 2 days (from 2026-10-11), 6 days this month.
Stretch: no streak running, longest 1 day (from 2026-10-12), 1 day this month.
1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit
choice> 8
Bye.
What was typed (26 lines)
6
3
4
1
4
Run
2026-09
1
2
1
2
2026-10-11
2
1
2026-10-02
5
Stretch
1
3
7
2026-10-14
3
8
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P055 habit tracker: up to five habits, checked in on real calendar days.
# For each habit: the streak still running, the longest streak, and a month
# drawn as a calendar with the days it was done.
import dates
import habits
def trim(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def counted(n, one, many):
if n == 1:
return "1 " + one
return str(n) + " " + many
def described(h, today):
habits.current_streak(h[1], today) => now
habits.longest_streak(h[1]) => best
dates.date_of(today) => t
h[0] + ": " => s
if now > 0:
today => end
if not habits.has_day(h[1], today):
today - 1 => end
s + "streak " + counted(now, "day", "days") + " (since " + dates.shown(dates.date_of(end - now + 1)) + ")" => s
else:
s + "no streak running" => s
if best[0] > 0:
s + ", longest " + counted(best[0], "day", "days") + " (from " + dates.shown(dates.date_of(best[1])) + ")" => s
s + ", " + counted(habits.done_in_month(h[1], t[0], t[1]), "day", "days") + " this month." => s
return s
def ask_habit(state):
# The index of a habit chosen by number or name, or -1 when the answer
# is empty.
state[1] => hs
if len(hs) == 1:
return 0
while True:
for k in [0:len(hs) - 1]:
(" " + str(k + 1) + ") " + hs[k][0]) ^0
trim(input("habit> ")) => answer
if answer == "":
return -1
for k in [0:len(hs) - 1]:
if answer == str(k + 1) or answer == hs[k][0]:
return k
"Type a habit's number or its name." ^0
def ask_day(state, prompt):
# A day number not after today: an empty answer is today itself, or -1
# when the answer is "x" (cancel).
while True:
trim(input(prompt + " (YYYY-MM-DD, nothing for today, x to cancel)> ")) => answer
if answer == "":
return state[0]
if answer == "x":
return -1
dates.parsed(answer) => d
if len(d) == 0:
"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12." ^0
elif dates.day_number(d) > state[0]:
("That is after today, " + dates.shown(dates.date_of(state[0])) + ".") ^0
else:
return dates.day_number(d)
def check_in(state, undo):
if len(state[1]) == 0:
"There is no habit yet - add one first." ^0
return
ask_habit(state) => k
if k == -1:
"Cancelled." ^0
return
state[1][k] => h
ask_day(state, "day") => n
if n == -1:
"Cancelled." ^0
return
dates.date_of(n) => d
dates.shown(d) + ", a " + dates.day_name(d) => when
if undo:
if not habits.has_day(h[1], n):
(h[0] + " was not checked in on " + when + ".") ^0
return
habits.without_day(h[1], n) => h[1]
("Took back " + h[0] + " on " + when + ".") ^0
else:
if habits.has_day(h[1], n):
(h[0] + " is already checked in on " + when + ".") ^0
return
habits.with_day(h[1], n) => h[1]
("Checked in " + h[0] + " on " + when + ".") ^0
described(h, state[0]) ^0
def list_habits(state):
if len(state[1]) == 0:
"There is no habit yet - add one first." ^0
return
dates.date_of(state[0]) => t
("Today is " + dates.shown(t) + ", a " + dates.day_name(t) + ".") ^0
for h in state[1]:
described(h, state[0]) ^0
def month(state):
if len(state[1]) == 0:
"There is no habit yet - add one first." ^0
return
ask_habit(state) => k
if k == -1:
"Cancelled." ^0
return
state[1][k] => h
dates.date_of(state[0]) => t
while True:
trim(input("month (YYYY-MM, nothing for this month)> ")) => answer
if answer == "":
[t[0], t[1]] => ym
break
dates.parsed(answer + "-01") => d
if len(d) > 0:
[d[0], d[1]] => ym
break
"Type a month like 2026-09." ^0
["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][ym[1]] => name
(h[0] + ", " + name + " " + str(ym[0]) + ":") ^0
for line in habits.month_grid(h[1], ym[0], ym[1], state[0]):
line ^0
dates.day_number([ym[0], ym[1], 1]) => first
first + dates.days_in_month(ym[0], ym[1]) - 1 => last
if last > state[0]:
state[0] => last
if last < first:
"That month has not started yet." ^0
else:
("Done on " + str(habits.done_in_month(h[1], ym[0], ym[1])) + " of " + counted(last - first + 1, "day", "days") + " so far.") ^0
def new_habit(state):
if len(state[1]) == 5:
"Five habits is the most - this tracker keeps them few." ^0
return
while True:
trim(input("name> ")) => name
if name == "":
"Cancelled." ^0
return
False => taken
for h in state[1]:
if h[0] == name:
True => taken
if taken:
("There is a habit called " + name + " already.") ^0
elif len(name) > 30:
"Keep the name to 30 characters." ^0
else:
state[1] + [[name, []]] => state[1]
("Added " + name + ".") ^0
return
def set_today(state):
while True:
trim(input("today (YYYY-MM-DD)> ")) => answer
if answer == "":
"Cancelled." ^0
return
dates.parsed(answer) => d
if len(d) > 0:
dates.day_number(d) => state[0]
("Today is now " + dates.shown(d) + ", a " + dates.day_name(d) + ".") ^0
return
"Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12." ^0
def sample(state):
# Two habits over the last six weeks: reading on most days, with two
# breaks, and running on Mondays, Wednesdays and Fridays.
state[0] => t
[] => reading
[] => running
for back in [0:41]:
t - 41 + back => n
if not (back == 9 or back == 10 or back == 30):
reading + [n] => reading
dates.weekday(dates.date_of(n)) => w
if w == 0 or w == 2 or w == 4:
running + [n] => running
[["Read 20 pages", reading], ["Run", running]] => state[1]
"Two sample habits over the last six weeks: Read 20 pages and Run." ^0
"== Habit tracker ==" ^0
"Check in a habit each day it is done; streaks count the days in a row." ^0
# [today as a day number, the habits]
[dates.day_number([2026, 10, 12]), []] => state
("Today is 2026-10-12, a " + dates.day_name([2026, 10, 12]) + ". Change it with 7) today.") ^0
True => looping
while looping:
"" ^0
"1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit" ^0
trim(input("choice> ")) => choice
if choice == "1":
check_in(state, False)
elif choice == "2":
check_in(state, True)
elif choice == "3":
list_habits(state)
elif choice == "4":
month(state)
elif choice == "5":
new_habit(state)
elif choice == "6":
sample(state)
elif choice == "7":
set_today(state)
elif choice == "8":
False => looping
else:
"Pick a number from 1 to 8." ^0
"Bye." ^0
Python projection (main.py)
import dates
import habits
def trim(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def counted(n, one, many):
if n == 1:
return "1 " + one
return str(n) + " " + many
def described(h, today):
now = habits.current_streak(h[1], today)
best = habits.longest_streak(h[1])
t = dates.date_of(today)
s = h[0] + ": "
if now > 0:
end = today
if not habits.has_day(h[1], today):
end = today - 1
s = s + "streak " + counted(now, "day", "days") + " (since " + dates.shown(dates.date_of(end - now + 1)) + ")"
else:
s = s + "no streak running"
if best[0] > 0:
s = s + ", longest " + counted(best[0], "day", "days") + " (from " + dates.shown(dates.date_of(best[1])) + ")"
s = s + ", " + counted(habits.done_in_month(h[1], t[0], t[1]), "day", "days") + " this month."
return s
def ask_habit(state):
hs = state[1]
if len(hs) == 1:
return 0
while True:
for k in range(0, len(hs)):
print(" " + str(k + 1) + ") " + hs[k][0])
answer = trim(input("habit> "))
if answer == "":
return -1
for k in range(0, len(hs)):
if answer == str(k + 1) or answer == hs[k][0]:
return k
print("Type a habit's number or its name.")
def ask_day(state, prompt):
while True:
answer = trim(input(prompt + " (YYYY-MM-DD, nothing for today, x to cancel)> "))
if answer == "":
return state[0]
if answer == "x":
return -1
d = dates.parsed(answer)
if len(d) == 0:
print("Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.")
elif dates.day_number(d) > state[0]:
print("That is after today, " + dates.shown(dates.date_of(state[0])) + ".")
else:
return dates.day_number(d)
def check_in(state, undo):
if len(state[1]) == 0:
print("There is no habit yet - add one first.")
return
k = ask_habit(state)
if k == -1:
print("Cancelled.")
return
h = state[1][k]
n = ask_day(state, "day")
if n == -1:
print("Cancelled.")
return
d = dates.date_of(n)
when = dates.shown(d) + ", a " + dates.day_name(d)
if undo:
if not habits.has_day(h[1], n):
print(h[0] + " was not checked in on " + when + ".")
return
h[1] = habits.without_day(h[1], n)
print("Took back " + h[0] + " on " + when + ".")
else:
if habits.has_day(h[1], n):
print(h[0] + " is already checked in on " + when + ".")
return
h[1] = habits.with_day(h[1], n)
print("Checked in " + h[0] + " on " + when + ".")
print(described(h, state[0]))
def list_habits(state):
if len(state[1]) == 0:
print("There is no habit yet - add one first.")
return
t = dates.date_of(state[0])
print("Today is " + dates.shown(t) + ", a " + dates.day_name(t) + ".")
for h in state[1]:
print(described(h, state[0]))
def month(state):
if len(state[1]) == 0:
print("There is no habit yet - add one first.")
return
k = ask_habit(state)
if k == -1:
print("Cancelled.")
return
h = state[1][k]
t = dates.date_of(state[0])
while True:
answer = trim(input("month (YYYY-MM, nothing for this month)> "))
if answer == "":
ym = [t[0], t[1]]
break
d = dates.parsed(answer + "-01")
if len(d) > 0:
ym = [d[0], d[1]]
break
print("Type a month like 2026-09.")
name = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][ym[1]]
print(h[0] + ", " + name + " " + str(ym[0]) + ":")
for line in habits.month_grid(h[1], ym[0], ym[1], state[0]):
print(line)
first = dates.day_number([ym[0], ym[1], 1])
last = first + dates.days_in_month(ym[0], ym[1]) - 1
if last > state[0]:
last = state[0]
if last < first:
print("That month has not started yet.")
else:
print("Done on " + str(habits.done_in_month(h[1], ym[0], ym[1])) + " of " + counted(last - first + 1, "day", "days") + " so far.")
def new_habit(state):
if len(state[1]) == 5:
print("Five habits is the most - this tracker keeps them few.")
return
while True:
name = trim(input("name> "))
if name == "":
print("Cancelled.")
return
taken = False
for h in state[1]:
if h[0] == name:
taken = True
if taken:
print("There is a habit called " + name + " already.")
elif len(name) > 30:
print("Keep the name to 30 characters.")
else:
state[1] = state[1] + [[name, []]]
print("Added " + name + ".")
return
def set_today(state):
while True:
answer = trim(input("today (YYYY-MM-DD)> "))
if answer == "":
print("Cancelled.")
return
d = dates.parsed(answer)
if len(d) > 0:
state[0] = dates.day_number(d)
print("Today is now " + dates.shown(d) + ", a " + dates.day_name(d) + ".")
return
print("Type a real date from 1900-01-01 to 2099-12-31, like 2026-10-12.")
def sample(state):
t = state[0]
reading = []
running = []
for back in range(0, 42):
n = t - 41 + back
if not (back == 9 or back == 10 or back == 30):
reading = reading + [n]
w = dates.weekday(dates.date_of(n))
if w == 0 or w == 2 or w == 4:
running = running + [n]
state[1] = [["Read 20 pages", reading], ["Run", running]]
print("Two sample habits over the last six weeks: Read 20 pages and Run.")
print("== Habit tracker ==")
print("Check in a habit each day it is done; streaks count the days in a row.")
state = [dates.day_number([2026, 10, 12]), []]
print("Today is 2026-10-12, a " + dates.day_name([2026, 10, 12]) + ". Change it with 7) today.")
looping = True
while looping:
print("")
print("1) check in 2) take back 3) habits 4) month 5) new habit 6) sample 7) today 8) quit")
choice = trim(input("choice> "))
if choice == "1":
check_in(state, False)
elif choice == "2":
check_in(state, True)
elif choice == "3":
list_habits(state)
elif choice == "4":
month(state)
elif choice == "5":
new_habit(state)
elif choice == "6":
sample(state)
elif choice == "7":
set_today(state)
elif choice == "8":
looping = False
else:
print("Pick a number from 1 to 8.")
print("Bye.")
dates.eml
eml# P055 habit tracker - real calendar dates, 1900-01-01 to 2099-12-31. A
# date is [year, month, day]; a day number counts days from 1900-01-01
# (day 0), as in the corpus case days-between-dates, so consecutive days
# have consecutive numbers and a streak is a run of numbers.
def is_leap(year):
if year % 400 == 0:
return True
if year % 100 == 0:
return False
return year % 4 == 0
def days_in_month(year, month):
if month == 2:
if is_leap(year):
return 29
return 28
if month == 4 or month == 6 or month == 9 or month == 11:
return 30
return 31
def day_number(d):
0 => total
for y in [1900:d[0] - 1]:
if is_leap(y):
total + 366 => total
else:
total + 365 => total
for m in [1:d[1] - 1]:
total + days_in_month(d[0], m) => total
return total + d[2] - 1
def date_of(n):
# The date of day number n.
1900 => y
365 => length
while True:
365 => length
if is_leap(y):
366 => length
if n < length:
break
n - length => n
y + 1 => y
1 => m
while n >= days_in_month(y, m):
n - days_in_month(y, m) => n
m + 1 => m
return [y, m, n + 1]
def weekday(d):
# Zeller's congruence, as in the corpus case day-of-week-zeller: January
# and February count as months 13 and 14 of the year before. Returns 0
# for Monday up to 6 for Sunday (Zeller's 0 is Saturday).
d[0] => y
d[1] => m
if m < 3:
m + 12 => m
y - 1 => y
y % 100 => k
int(y / 100) => j
(d[2] + int(13 * (m + 1) / 5) + k + int(k / 4) + int(j / 4) + 5 * j) % 7 => h
return (h + 5) % 7
def parsed(s):
# "YYYY-MM-DD" as a date, or [] when it is not a real date in range.
if len(s) != 10 or s[4] != "-" or s[7] != "-":
return []
for i in [0, 1, 2, 3, 5, 6, 8, 9]:
if not (s[i] >= "0" and s[i] <= "9"):
return []
int(s[0:4]) => y
int(s[5:7]) => m
int(s[8:10]) => d
if y < 1900 or y > 2099 or m < 1 or m > 12 or d < 1 or d > days_in_month(y, m):
return []
return [y, m, d]
def shown(d):
str(d[1]) => m
if len(m) == 1:
"0" + m => m
str(d[2]) => dd
if len(dd) == 1:
"0" + dd => dd
return str(d[0]) + "-" + m + "-" + dd
def day_name(d):
return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][weekday(d)]
Python projection (dates.py)
def is_leap(year):
if year % 400 == 0:
return True
if year % 100 == 0:
return False
return year % 4 == 0
def days_in_month(year, month):
if month == 2:
if is_leap(year):
return 29
return 28
if month == 4 or month == 6 or month == 9 or month == 11:
return 30
return 31
def day_number(d):
total = 0
for y in range(1900, d[0]):
if is_leap(y):
total = total + 366
else:
total = total + 365
for m in range(1, d[1]):
total = total + days_in_month(d[0], m)
return total + d[2] - 1
def date_of(n):
y = 1900
length = 365
while True:
length = 365
if is_leap(y):
length = 366
if n < length:
break
n = n - length
y = y + 1
m = 1
while n >= days_in_month(y, m):
n = n - days_in_month(y, m)
m = m + 1
return [y, m, n + 1]
def weekday(d):
y = d[0]
m = d[1]
if m < 3:
m = m + 12
y = y - 1
k = y % 100
j = int(y / 100)
h = (d[2] + int(13 * (m + 1) / 5) + k + int(k / 4) + int(j / 4) + 5 * j) % 7
return (h + 5) % 7
def parsed(s):
if len(s) != 10 or s[4] != "-" or s[7] != "-":
return []
for i in [0, 1, 2, 3, 5, 6, 8, 9]:
if not (s[i] >= "0" and s[i] <= "9"):
return []
y = int(s[0:4])
m = int(s[5:7])
d = int(s[8:10])
if y < 1900 or y > 2099 or m < 1 or m > 12 or d < 1 or d > days_in_month(y, m):
return []
return [y, m, d]
def shown(d):
m = str(d[1])
if len(m) == 1:
m = "0" + m
dd = str(d[2])
if len(dd) == 1:
dd = "0" + dd
return str(d[0]) + "-" + m + "-" + dd
def day_name(d):
return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"][weekday(d)]
habits.eml
eml# P055 habit tracker - check-ins. A habit is [name, days]: days is the
# sorted list of day numbers it was done on, each at most once.
import dates
def has_day(days, n):
for x in days:
if x == n:
return True
return False
def with_day(days, n):
# days with n added in its place, keeping the list sorted.
[] => out
False => placed
for x in days:
if not placed and n < x:
out + [n] => out
True => placed
out + [x] => out
if not placed:
out + [n] => out
return out
def without_day(days, n):
[] => out
for x in days:
if x != n:
out + [x] => out
return out
def run_ending(days, n):
# How many days in a row, ending with day n, the habit was done.
0 => run
while has_day(days, n - run):
run + 1 => run
return run
def current_streak(days, today):
# The run that is still alive: ending today, or ending yesterday when
# today is not checked in yet - the day is not over.
if has_day(days, today):
return run_ending(days, today)
return run_ending(days, today - 1)
def longest_streak(days):
# [length, first day] of the longest run; the earliest when two are
# equally long.
0 => best
-1 => best_start
0 => run
-1 => start
-10 => prev
for x in days:
if x == prev + 1:
run + 1 => run
else:
1 => run
x => start
if run > best:
run => best
start => best_start
x => prev
return [best, best_start]
def rtrimmed(s):
len(s) => j
while j > 0 and s[j - 1] == " ":
j - 1 => j
return s[0:j]
def month_grid(days, year, month, today):
# The month as a calendar, Monday first: each day with * when the habit
# was done, and . after the days still to come.
[" Mo Tu We Th Fr Sa Su"] => lines
dates.weekday([year, month, 1]) => first
dates.day_number([year, month, 1]) => n0
" " * first => line
first => col
for d in [1:dates.days_in_month(year, month)]:
n0 + d - 1 => n
str(d) => cell
while len(cell) < 3:
" " + cell => cell
if n > today:
cell + "." => cell
elif has_day(days, n):
cell + "*" => cell
else:
cell + " " => cell
line + cell => line
col + 1 => col
if col == 7:
lines + [rtrimmed(line)] => lines
"" => line
0 => col
if line != "":
lines + [rtrimmed(line)] => lines
return lines
def done_in_month(days, year, month):
dates.day_number([year, month, 1]) => a
a + dates.days_in_month(year, month) - 1 => b
0 => count
for x in days:
if x >= a and x <= b:
count + 1 => count
return count
Python projection (habits.py)
import dates
def has_day(days, n):
for x in days:
if x == n:
return True
return False
def with_day(days, n):
out = []
placed = False
for x in days:
if not placed and n < x:
out = out + [n]
placed = True
out = out + [x]
if not placed:
out = out + [n]
return out
def without_day(days, n):
out = []
for x in days:
if x != n:
out = out + [x]
return out
def run_ending(days, n):
run = 0
while has_day(days, n - run):
run = run + 1
return run
def current_streak(days, today):
if has_day(days, today):
return run_ending(days, today)
return run_ending(days, today - 1)
def longest_streak(days):
best = 0
best_start = -1
run = 0
start = -1
prev = -10
for x in days:
if x == prev + 1:
run = run + 1
else:
run = 1
start = x
if run > best:
best = run
best_start = start
prev = x
return [best, best_start]
def rtrimmed(s):
j = len(s)
while j > 0 and s[j - 1] == " ":
j = j - 1
return s[0:j]
def month_grid(days, year, month, today):
lines = [" Mo Tu We Th Fr Sa Su"]
first = dates.weekday([year, month, 1])
n0 = dates.day_number([year, month, 1])
line = " " * first
col = first
for d in range(1, dates.days_in_month(year, month)+1):
n = n0 + d - 1
cell = str(d)
while len(cell) < 3:
cell = " " + cell
if n > today:
cell = cell + "."
elif has_day(days, n):
cell = cell + "*"
else:
cell = cell + " "
line = line + cell
col = col + 1
if col == 7:
lines = lines + [rtrimmed(line)]
line = ""
col = 0
if line != "":
lines = lines + [rtrimmed(line)]
return lines
def done_in_month(days, year, month):
a = dates.day_number([year, month, 1])
b = a + dates.days_in_month(year, month) - 1
count = 0
for x in days:
if x >= a and x <= b:
count = count + 1
return count