Weeks and months
Dates as a day count: a date's ISO week date against the US and simple week numbers, adding months by the clamp and overflow rules, stepping a month at a time against adding all at once, working days, and the years with 53 ISO weeks.
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
Dates as a day count, so that weeks and months can be reckoned exactly. The program gives a date's ISO week date next to two other week numbers. It adds months by the clamp and the overflow rules, compares stepping a month at a time with adding all the months at once, counts working days, and lists the years with 53 ISO weeks.
main.eml- the menu, reading dates and numbers, and the reportsweeks.eml- leap years, day numbers and back, weekdays, the three week numberings, adding months, and working days
How each part works:
- A date becomes the number of days since 1 January 1600, a Saturday: 365 per year plus the leap days (every 4th year, not every 100th, but every 400th). Weekdays and differences are then plain arithmetic.
- Week numbers, as in the corpus case
week-numbering-conventions, are at least three different things: - ISO: weeks start on Monday, and a week belongs to the year holding its Thursday. So 1 January can be in the last week of the year before - 2027-01-01 is in week 53 of 2026 - and 30 December 2024 is in week 1 of 2025. - US: weeks start on Sunday, and week 1 is the one holding 1 January. - Simple: week 1 is 1 to 7 January, whatever days they are. - A year has 53 ISO weeks when it starts on a Thursday, or is a leap year starting on a Wednesday.
- Adding months, as in the corpus case
month-end-arithmetic: 31 January plus one month has no answer. Clamping gives the month's last day, 28 February; overflow runs on into March, 3 March. Clamping cannot be undone: 31 January + 1 - 1 is 28 January. Stepping a month at a time drifts to the 28th and stays there, while adding all the months at once from the start does not. - Working days are Monday to Friday, counted by whole weeks and then day by day.
What is checked: menu choices 1 to 7; dates as YYYY-MM-DD, real ones from 1601-01-01 to 9999-12-31 (1601 is the first year whose weeks all start on or after the day count's first day), and results inside that range; months from -1200 to 1200, steps from 1 to 24, working days from 1 to 10,000, years from 1600 to 9999; a second date no earlier than the first. An empty answer cancels.
Sessions: sessions/basic.in gives: - 2026-10-15 (week 42), 2027-01-01 (2026-W53) and 2024-12-30 (2025-W01); - 31 January 2026 and 2024 plus one month by both rules, and back again; 15 March minus two months, where the rules agree; - twelve monthly steps from 31 January (2027-01-28 against 2027-01-31), and six from the 15th (no difference); - 22 working days in October 2026; 10 working days after 2026-10-15; - the five 53-week years from 2000 to 2030.
sessions/bad-input.in gives: - menu choices 0 and x; - 2026-02-30, slashes, 1600-12-31 and day-first order, then a cancel; - 1201 and x months; 1200 months back from 1601, which lands outside; - an end date before the start; 0 working days; year 1599; an end year before the start; and 2026 alone, which has 53.
Built on the verified corpus cases week-numbering-conventions (three week numberings that disagree at the ends of the year) and month-end-arithmetic (clamp against overflow, and what clamping loses).
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== Weeks and months ==
Week numbers three ways, adding months two ways, and working days.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 0
Pick a number from 1 to 7.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> x
Pick a number from 1 to 7.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 1
date (YYYY-MM-DD)> 2026-02-30
Type a real date from 1601-01-01 to 9999-12-31, like 2026-10-15.
date (YYYY-MM-DD)> 2026/10/15
Type a real date from 1601-01-01 to 9999-12-31, like 2026-10-15.
date (YYYY-MM-DD)> 1600-12-31
Type a real date from 1601-01-01 to 9999-12-31, like 2026-10-15.
date (YYYY-MM-DD)> 15-10-2026
Type a real date from 1601-01-01 to 9999-12-31, like 2026-10-15.
date (YYYY-MM-DD)>
Cancelled.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 2
date (YYYY-MM-DD)> 2026-01-31
months to add, minus to go back (-1200 to 1200)> 1201
Type a whole number from -1200 to 1200.
months to add, minus to go back (-1200 to 1200)> x
Type a whole number from -1200 to 1200.
months to add, minus to go back (-1200 to 1200)>
Cancelled.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 2
date (YYYY-MM-DD)> 1601-03-01
months to add, minus to go back (-1200 to 1200)> -1200
That lands outside 1601 to 9999.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 4
from (YYYY-MM-DD)> 2026-10-15
to (YYYY-MM-DD)> 2026-10-01
The second date has to be the same or later.
to (YYYY-MM-DD)>
Cancelled.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 5
start (YYYY-MM-DD)> 2026-10-15
working days to add (1 to 10000)> 0
Type a whole number from 1 to 10000.
working days to add (1 to 10000)>
Cancelled.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 6
from year (1600 to 9999)> 1599
Type a whole number from 1600 to 9999.
from year (1600 to 9999)> 2026
to year (2026 to 9999)> 2025
Type a whole number from 2026 to 9999.
to year (2026 to 9999)> 2026
2026 has 53 ISO weeks.
A year has 53 when it starts on a Thursday, or is a leap year starting on a Wednesday.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 7
Bye.
What was typed (30 lines)
0
x
1
2026-02-30
2026/10/15
1600-12-31
15-10-2026
2
2026-01-31
1201
x
2
1601-03-01
-1200
4
2026-10-15
2026-10-01
5
2026-10-15
0
6
1599
2026
2025
2026
7
basic
interpreter: byte-equal== Weeks and months ==
Week numbers three ways, adding months two ways, and working days.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 1
date (YYYY-MM-DD)> 2026-10-15
2026-10-15 is a Thursday.
ISO: week 42 of 2026, day 4 - written 2026-W42-4.
US (weeks from Sunday, week 1 holds 1 January): week 42.
Simple (week 1 is 1 to 7 January): week 42.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 1
date (YYYY-MM-DD)> 2027-01-01
2027-01-01 is a Friday.
ISO: week 53 of 2026, day 5 - written 2026-W53-5.
The ISO year is 2026, not 2027: this week's Thursday falls in 2026.
US (weeks from Sunday, week 1 holds 1 January): week 1.
Simple (week 1 is 1 to 7 January): week 1.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 1
date (YYYY-MM-DD)> 2024-12-30
2024-12-30 is a Monday.
ISO: week 1 of 2025, day 1 - written 2025-W01-1.
The ISO year is 2025, not 2024: this week's Thursday falls in 2025.
US (weeks from Sunday, week 1 holds 1 January): week 53.
Simple (week 1 is 1 to 7 January): week 53.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 2
date (YYYY-MM-DD)> 2026-01-31
months to add, minus to go back (-1200 to 1200)> 1
clamp: 2026-02-28 (a day past the month's end becomes its last day)
overflow: 2026-03-03 (the extra days run on into the next month)
And back again with clamp: 2026-01-28 - not where it started: clamping is not reversible.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 2
date (YYYY-MM-DD)> 2024-01-31
months to add, minus to go back (-1200 to 1200)> 1
clamp: 2024-02-29 (a day past the month's end becomes its last day)
overflow: 2024-03-02 (the extra days run on into the next month)
And back again with clamp: 2024-01-29 - not where it started: clamping is not reversible.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 2
date (YYYY-MM-DD)> 2026-03-15
months to add, minus to go back (-1200 to 1200)> -2
clamp: 2026-01-15 (a day past the month's end becomes its last day)
overflow: 2026-01-15 (the extra days run on into the next month)
Both rules agree: the day exists in that month.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 3
date (YYYY-MM-DD)> 2026-01-31
months (1 to 24)> 12
One month at a time: 2026-02-28, 2026-03-28, 2026-04-28, 2026-05-28, 2026-06-28, 2026-07-28, 2026-08-28, 2026-09-28, 2026-10-28, 2026-11-28, 2026-12-28, 2027-01-28
After 12 steps: 2027-01-28; all 12 months at once: 2027-01-31.
Stepping lost days at a short month and never got them back: count from the start date instead.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 3
date (YYYY-MM-DD)> 2026-03-15
months (1 to 24)> 6
One month at a time: 2026-04-15, 2026-05-15, 2026-06-15, 2026-07-15, 2026-08-15, 2026-09-15
After 6 steps: 2026-09-15; all 6 months at once: 2026-09-15.
The same: no step had to clamp.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 4
from (YYYY-MM-DD)> 2026-10-01
to (YYYY-MM-DD)> 2026-11-01
From 2026-10-01 up to 2026-11-01: 31 days, 22 of them Monday to Friday.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 5
start (YYYY-MM-DD)> 2026-10-15
working days to add (1 to 10000)> 10
10 working days after 2026-10-15 is 2026-10-29, a Thursday.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 6
from year (1600 to 9999)> 2000
to year (2000 to 9999)> 2030
5 of the 31 years have 53 ISO weeks: 2004 2009 2015 2020 2026
A year has 53 when it starts on a Thursday, or is a leap year starting on a Wednesday.
1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit
choice> 7
Bye.
What was typed (31 lines)
1
2026-10-15
1
2027-01-01
1
2024-12-30
2
2026-01-31
1
2
2024-01-31
1
2
2026-03-15
-2
3
2026-01-31
12
3
2026-03-15
6
4
2026-10-01
2026-11-01
5
2026-10-15
10
6
2000
2030
7
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# P087 weeks and months: a date's ISO week and two other week numbers,
# adding months by the clamp and overflow rules, stepping a month at a time
# against adding all at once, working days, and years with 53 ISO weeks.
import weeks
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 digits(s):
if len(s) == 0:
return -1
0 => n
for c in s:
if not (c >= "0" and c <= "9"):
return -1
n * 10 + int(c) => n
return n
def two(n):
if n < 10:
return "0" + str(n)
return str(n)
def text(ymd):
return str(ymd[0]) + "-" + two(ymd[1]) + "-" + two(ymd[2])
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] => NAMES
def parse(s):
# "2026-10-15" as [y, m, d] (1601-01-01 to 9999-12-31), or []. 1601 is the
# first year whose ISO weeks all start on or after the day count's
# first day, 1 January 1600.
if len(s) != 10 or s[4] != "-" or s[7] != "-":
return []
digits(s[0:4]) => y
digits(s[5:7]) => m
digits(s[8:10]) => d
if y < 1601 or m < 1 or m > 12 or d < 1:
return []
if d > weeks.days_in(y, m):
return []
return [y, m, d]
def ask_date(prompt):
while True:
trim(input(prompt + " (YYYY-MM-DD)> ")) => s
if s == "":
return []
parse(s) => ymd
if len(ymd) == 3:
return ymd
"Type a real date from 1601-01-01 to 9999-12-31, like 2026-10-15." ^0
def ask(prompt, low, high):
while True:
trim(input(prompt + " (" + str(low) + " to " + str(high) + ")> ")) => a
if a == "":
return -100000
a => t
1 => sg
if len(t) > 1 and t[0] == "-":
t[1:len(t)] => t
-1 => sg
digits(t) => n
if n >= 0 and len(t) <= 5 and sg * n >= low and sg * n <= high:
return sg * n
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
def about():
ask_date("date") => ymd
if len(ymd) == 0:
"Cancelled." ^0
return
weeks.day_number(ymd[0], ymd[1], ymd[2]) => n
weeks.iso_week(n) => w
(text(ymd) + " is a " + NAMES[weeks.weekday(n)] + ".") ^0
("ISO: week " + str(w[1]) + " of " + str(w[0]) + ", day " + str(w[2]) + " - written " + str(w[0]) + "-W" + two(w[1]) + "-" + str(w[2]) + ".") ^0
if w[0] != ymd[0]:
(" The ISO year is " + str(w[0]) + ", not " + str(ymd[0]) + ": this week's Thursday falls in " + str(w[0]) + ".") ^0
("US (weeks from Sunday, week 1 holds 1 January): week " + str(weeks.us_week(n)) + ".") ^0
("Simple (week 1 is 1 to 7 January): week " + str(weeks.simple_week(n)) + ".") ^0
def months():
ask_date("date") => ymd
if len(ymd) == 0:
"Cancelled." ^0
return
ask("months to add, minus to go back", -1200, 1200) => k
if k == -100000:
"Cancelled." ^0
return
weeks.add_months(ymd[0], ymd[1], ymd[2], k, True) => c
if c[0] < 1601 or c[0] > 9999:
"That lands outside 1601 to 9999." ^0
return
weeks.add_months(ymd[0], ymd[1], ymd[2], k, False) => o
("clamp: " + text(c) + " (a day past the month's end becomes its last day)") ^0
("overflow: " + text(o) + " (the extra days run on into the next month)") ^0
if c == o:
"Both rules agree: the day exists in that month." ^0
else:
weeks.add_months(c[0], c[1], c[2], 0 - k, True) => back
("And back again with clamp: " + text(back) + " - " + ["the same day.", "not where it started: clamping is not reversible."][back != ymd]) ^0
def stepping():
ask_date("date") => ymd
if len(ymd) == 0:
"Cancelled." ^0
return
ask("months", 1, 24) => k
if k == -100000:
"Cancelled." ^0
return
weeks.add_months(ymd[0], ymd[1], ymd[2], k, True) => once
if once[0] > 9999:
"That lands after 9999." ^0
return
ymd => cur
"" => line
for i in [1:k]:
weeks.add_months(cur[0], cur[1], cur[2], 1, True) => cur
if i <= 12:
if line != "":
line + ", " => line
line + text(cur) => line
if k > 12:
line + ", ..." => line
("One month at a time: " + line) ^0
("After " + str(k) + " steps: " + text(cur) + "; all " + str(k) + " months at once: " + text(once) + ".") ^0
if cur != once:
"Stepping lost days at a short month and never got them back: count from the start date instead." ^0
else:
"The same: no step had to clamp." ^0
def work():
ask_date("from") => a
if len(a) == 0:
"Cancelled." ^0
return
while True:
ask_date("to") => b
if len(b) == 0:
"Cancelled." ^0
return
weeks.day_number(a[0], a[1], a[2]) => na
weeks.day_number(b[0], b[1], b[2]) => nb
if nb >= na:
weeks.working_days(na, nb) => w
("From " + text(a) + " up to " + text(b) + ": " + str(nb - na) + " days, " + str(w) + " of them Monday to Friday.") ^0
return
"The second date has to be the same or later." ^0
def add_work():
ask_date("start") => a
if len(a) == 0:
"Cancelled." ^0
return
ask("working days to add", 1, 10000) => k
if k == -100000:
"Cancelled." ^0
return
weeks.add_working(weeks.day_number(a[0], a[1], a[2]), k) => n
(str(k) + " working days after " + text(a) + " is " + text(weeks.date_of(n)) + ", a " + NAMES[weeks.weekday(n)] + ".") ^0
def long_years():
ask("from year", 1600, 9999) => y1
if y1 == -100000:
"Cancelled." ^0
return
ask("to year", y1, 9999) => y2
if y2 == -100000:
"Cancelled." ^0
return
if y1 == y2:
(str(y1) + " has " + str(weeks.iso_weeks_in(y1)) + " ISO weeks.") ^0
"A year has 53 when it starts on a Thursday, or is a leap year starting on a Wednesday." ^0
return
"" => line
0 => count
for y in [y1:y2]:
if weeks.iso_weeks_in(y) == 53:
if count < 20:
if line != "":
line + " " => line
line + str(y) => line
count + 1 => count
if count == 0:
("No year from " + str(y1) + " to " + str(y2) + " has 53 ISO weeks.") ^0
return
if count > 20:
line + " ..." => line
(str(count) + " of the " + str(y2 - y1 + 1) + " years have 53 ISO weeks: " + line) ^0
"A year has 53 when it starts on a Thursday, or is a leap year starting on a Wednesday." ^0
"== Weeks and months ==" ^0
"Week numbers three ways, adding months two ways, and working days." ^0
True => running
while running:
"" ^0
"1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit" ^0
trim(input("choice> ")) => choice
if choice == "1":
about()
elif choice == "2":
months()
elif choice == "3":
stepping()
elif choice == "4":
work()
elif choice == "5":
add_work()
elif choice == "6":
long_years()
elif choice == "7":
False => running
else:
"Pick a number from 1 to 7." ^0
"Bye." ^0
Python projection (main.py)
import weeks
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 digits(s):
if len(s) == 0:
return -1
n = 0
for c in s:
if not (c >= "0" and c <= "9"):
return -1
n = n * 10 + int(c)
return n
def two(n):
if n < 10:
return "0" + str(n)
return str(n)
def text(ymd):
return str(ymd[0]) + "-" + two(ymd[1]) + "-" + two(ymd[2])
NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def parse(s):
if len(s) != 10 or s[4] != "-" or s[7] != "-":
return []
y = digits(s[0:4])
m = digits(s[5:7])
d = digits(s[8:10])
if y < 1601 or m < 1 or m > 12 or d < 1:
return []
if d > weeks.days_in(y, m):
return []
return [y, m, d]
def ask_date(prompt):
while True:
s = trim(input(prompt + " (YYYY-MM-DD)> "))
if s == "":
return []
ymd = parse(s)
if len(ymd) == 3:
return ymd
print("Type a real date from 1601-01-01 to 9999-12-31, like 2026-10-15.")
def ask(prompt, low, high):
while True:
a = trim(input(prompt + " (" + str(low) + " to " + str(high) + ")> "))
if a == "":
return -100000
t = a
sg = 1
if len(t) > 1 and t[0] == "-":
t = t[1:len(t)]
sg = -1
n = digits(t)
if n >= 0 and len(t) <= 5 and sg * n >= low and sg * n <= high:
return sg * n
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
def about():
ymd = ask_date("date")
if len(ymd) == 0:
print("Cancelled.")
return
n = weeks.day_number(ymd[0], ymd[1], ymd[2])
w = weeks.iso_week(n)
print(text(ymd) + " is a " + NAMES[weeks.weekday(n)] + ".")
print("ISO: week " + str(w[1]) + " of " + str(w[0]) + ", day " + str(w[2]) + " - written " + str(w[0]) + "-W" + two(w[1]) + "-" + str(w[2]) + ".")
if w[0] != ymd[0]:
print(" The ISO year is " + str(w[0]) + ", not " + str(ymd[0]) + ": this week's Thursday falls in " + str(w[0]) + ".")
print("US (weeks from Sunday, week 1 holds 1 January): week " + str(weeks.us_week(n)) + ".")
print("Simple (week 1 is 1 to 7 January): week " + str(weeks.simple_week(n)) + ".")
def months():
ymd = ask_date("date")
if len(ymd) == 0:
print("Cancelled.")
return
k = ask("months to add, minus to go back", -1200, 1200)
if k == -100000:
print("Cancelled.")
return
c = weeks.add_months(ymd[0], ymd[1], ymd[2], k, True)
if c[0] < 1601 or c[0] > 9999:
print("That lands outside 1601 to 9999.")
return
o = weeks.add_months(ymd[0], ymd[1], ymd[2], k, False)
print("clamp: " + text(c) + " (a day past the month's end becomes its last day)")
print("overflow: " + text(o) + " (the extra days run on into the next month)")
if c == o:
print("Both rules agree: the day exists in that month.")
else:
back = weeks.add_months(c[0], c[1], c[2], 0 - k, True)
print("And back again with clamp: " + text(back) + " - " + ["the same day.", "not where it started: clamping is not reversible."][back != ymd])
def stepping():
ymd = ask_date("date")
if len(ymd) == 0:
print("Cancelled.")
return
k = ask("months", 1, 24)
if k == -100000:
print("Cancelled.")
return
once = weeks.add_months(ymd[0], ymd[1], ymd[2], k, True)
if once[0] > 9999:
print("That lands after 9999.")
return
cur = ymd
line = ""
for i in range(1, k+1):
cur = weeks.add_months(cur[0], cur[1], cur[2], 1, True)
if i <= 12:
if line != "":
line = line + ", "
line = line + text(cur)
if k > 12:
line = line + ", ..."
print("One month at a time: " + line)
print("After " + str(k) + " steps: " + text(cur) + "; all " + str(k) + " months at once: " + text(once) + ".")
if cur != once:
print("Stepping lost days at a short month and never got them back: count from the start date instead.")
else:
print("The same: no step had to clamp.")
def work():
a = ask_date("from")
if len(a) == 0:
print("Cancelled.")
return
while True:
b = ask_date("to")
if len(b) == 0:
print("Cancelled.")
return
na = weeks.day_number(a[0], a[1], a[2])
nb = weeks.day_number(b[0], b[1], b[2])
if nb >= na:
w = weeks.working_days(na, nb)
print("From " + text(a) + " up to " + text(b) + ": " + str(nb - na) + " days, " + str(w) + " of them Monday to Friday.")
return
print("The second date has to be the same or later.")
def add_work():
a = ask_date("start")
if len(a) == 0:
print("Cancelled.")
return
k = ask("working days to add", 1, 10000)
if k == -100000:
print("Cancelled.")
return
n = weeks.add_working(weeks.day_number(a[0], a[1], a[2]), k)
print(str(k) + " working days after " + text(a) + " is " + text(weeks.date_of(n)) + ", a " + NAMES[weeks.weekday(n)] + ".")
def long_years():
y1 = ask("from year", 1600, 9999)
if y1 == -100000:
print("Cancelled.")
return
y2 = ask("to year", y1, 9999)
if y2 == -100000:
print("Cancelled.")
return
if y1 == y2:
print(str(y1) + " has " + str(weeks.iso_weeks_in(y1)) + " ISO weeks.")
print("A year has 53 when it starts on a Thursday, or is a leap year starting on a Wednesday.")
return
line = ""
count = 0
for y in range(y1, y2+1):
if weeks.iso_weeks_in(y) == 53:
if count < 20:
if line != "":
line = line + " "
line = line + str(y)
count = count + 1
if count == 0:
print("No year from " + str(y1) + " to " + str(y2) + " has 53 ISO weeks.")
return
if count > 20:
line = line + " ..."
print(str(count) + " of the " + str(y2 - y1 + 1) + " years have 53 ISO weeks: " + line)
print("A year has 53 when it starts on a Thursday, or is a leap year starting on a Wednesday.")
print("== Weeks and months ==")
print("Week numbers three ways, adding months two ways, and working days.")
running = True
while running:
print("")
print("1) about a date 2) add months 3) a month at a time 4) working days between 5) add working days 6) 53-week years 7) quit")
choice = trim(input("choice> "))
if choice == "1":
about()
elif choice == "2":
months()
elif choice == "3":
stepping()
elif choice == "4":
work()
elif choice == "5":
add_work()
elif choice == "6":
long_years()
elif choice == "7":
running = False
else:
print("Pick a number from 1 to 7.")
print("Bye.")
weeks.eml
eml# P087 weeks and months - dates as a day count, so weeks and months can be
# reckoned exactly: ISO week numbers and two other conventions, adding
# months by two rules, and working days. Days are counted from 1 January
# 1600; dates from 1601 on (whose weeks all start on or after that day) to
# 9999, in the Gregorian calendar throughout.
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] => DAYS
def is_leap(y):
if y % 400 == 0:
return True
if y % 100 == 0:
return False
return y % 4 == 0
def days_in(y, m):
if m == 2 and is_leap(y):
return 29
return DAYS[m - 1]
def quotient(a, b):
# a // b for a >= 0, b > 0. The numbers here stay far below 2^53, so the
# float division is exact; a % b is exact at any size.
return int((a - a % b) / b)
def day_number(y, m, d):
# Days since 1 January 1600, which was a Saturday.
y - 1600 => years
365 * years + quotient(years + 3, 4) - quotient(years + 99, 100) + quotient(years + 399, 400) => n
for k in [1:m - 1]:
n + days_in(y, k) => n
return n + d - 1
def date_of(n):
# The [y, m, d] of day number n (0 = 1 January 1600).
1600 => y
# whole 400-year cycles of 146097 days first, then year by year
while n >= 146097:
n - 146097 => n
y + 400 => y
while True:
365 => len_y
if is_leap(y):
366 => len_y
if n < len_y:
1 => m
while n >= days_in(y, m):
n - days_in(y, m) => n
m + 1 => m
return [y, m, n + 1]
n - len_y => n
y + 1 => y
def weekday(n):
# 0 Monday ... 6 Sunday; day 0 was a Saturday.
return (n + 5) % 7
def iso_week(n):
# The ISO week date [ISO year, week, weekday 1-7]: weeks start Monday,
# and a week belongs to the year that holds its Thursday - so week 1
# holds the first Thursday, and 1 January can be in the last week of the
# year before.
n - weekday(n) + 3 => th
date_of(th)[0] => y
th - day_number(y, 1, 1) => doy
return [y, quotient(doy, 7) + 1, weekday(n) + 1]
def us_week(n):
# Weeks start Sunday, and week 1 is the one holding 1 January.
date_of(n)[0] => y
day_number(y, 1, 1) => jan1
(weekday(jan1) + 1) % 7 => lead
return quotient(n - jan1 + lead, 7) + 1
def simple_week(n):
# Week 1 is 1 to 7 January, whatever day they fall on.
date_of(n)[0] => y
return quotient(n - day_number(y, 1, 1), 7) + 1
def iso_weeks_in(y):
# 53 when the year starts on a Thursday, or is a leap year starting on
# a Wednesday; otherwise 52. Read off 28 December, always in the last week.
return iso_week(day_number(y, 12, 28))[1]
def add_months(y, m, d, k, clamp):
# k months later (k may be negative). With clamp, a day past the end of
# the month becomes its last day (31 January + 1 = 28 or 29 February);
# without, the extra days run on into the next month (3 or 2 March).
m - 1 + k => t
y + quotient(t - t % 12, 12) => ny
t % 12 + 1 => nm
if d <= days_in(ny, nm):
return [ny, nm, d]
if clamp:
return [ny, nm, days_in(ny, nm)]
return date_of(day_number(ny, nm, 1) + d - 1)
def working_days(a, b):
# Monday to Friday from day a up to but not including day b (a <= b),
# by whole weeks and then day by day.
b - a => total
quotient(total, 7) * 5 => count
a + quotient(total, 7) * 7 => n
while n < b:
if weekday(n) < 5:
count + 1 => count
n + 1 => n
return count
def add_working(n, k):
# The day k working days after day n (k >= 1).
n => d
k => left
while left > 0:
d + 1 => d
if weekday(d) < 5:
left - 1 => left
return d
Python projection (weeks.py)
DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def is_leap(y):
if y % 400 == 0:
return True
if y % 100 == 0:
return False
return y % 4 == 0
def days_in(y, m):
if m == 2 and is_leap(y):
return 29
return DAYS[m - 1]
def quotient(a, b):
return int((a - a % b) / b)
def day_number(y, m, d):
years = y - 1600
n = 365 * years + quotient(years + 3, 4) - quotient(years + 99, 100) + quotient(years + 399, 400)
for k in range(1, m):
n = n + days_in(y, k)
return n + d - 1
def date_of(n):
y = 1600
while n >= 146097:
n = n - 146097
y = y + 400
while True:
len_y = 365
if is_leap(y):
len_y = 366
if n < len_y:
m = 1
while n >= days_in(y, m):
n = n - days_in(y, m)
m = m + 1
return [y, m, n + 1]
n = n - len_y
y = y + 1
def weekday(n):
return (n + 5) % 7
def iso_week(n):
th = n - weekday(n) + 3
y = date_of(th)[0]
doy = th - day_number(y, 1, 1)
return [y, quotient(doy, 7) + 1, weekday(n) + 1]
def us_week(n):
y = date_of(n)[0]
jan1 = day_number(y, 1, 1)
lead = (weekday(jan1) + 1) % 7
return quotient(n - jan1 + lead, 7) + 1
def simple_week(n):
y = date_of(n)[0]
return quotient(n - day_number(y, 1, 1), 7) + 1
def iso_weeks_in(y):
return iso_week(day_number(y, 12, 28))[1]
def add_months(y, m, d, k, clamp):
t = m - 1 + k
ny = y + quotient(t - t % 12, 12)
nm = t % 12 + 1
if d <= days_in(ny, nm):
return [ny, nm, d]
if clamp:
return [ny, nm, days_in(ny, nm)]
return date_of(day_number(ny, nm, 1) + d - 1)
def working_days(a, b):
total = b - a
count = quotient(total, 7) * 5
n = a + quotient(total, 7) * 7
while n < b:
if weekday(n) < 5:
count = count + 1
n = n + 1
return count
def add_working(n, k):
d = n
left = k
while left > 0:
d = d + 1
if weekday(d) < 5:
left = left - 1
return d