Dates, triangles, and Python
December 1, 2023 at 12:31 PM by Dr. Drang
Tuesday morning, which was November 28, John D. Cook started a post with
The numbers in today’s date—11, 28, and 23—make up the sides of a triangle. This doesn’t always happen; the two smaller numbers have to add up to more than the larger number.
He went on to figure out the angles of the plane triangle with side lengths of 11, 28, and 23 and then extended his analysis to triangles on a sphere and a pseudosphere. But I got hung up on the quoted paragraph. Which days can and can’t be the sides of a triangle? And how does the number of such “triangle days” change from year to year?
So I wrote a little Python to answer these questions.
python:
1: from datetime import date, timedelta
2:
3: def isTriangleDay(dt):
4: "Can the the year, month, and day of the given date be the sides of a triangle?"
5: y = dt.year % 100
6: m = dt.month
7: d = dt.day
8: sides = sorted(int(x) for x in (y, m, d))
9: return sides[0] + sides[1] > sides[2]
10:
11: def allDays(y):
12: "Return a list of all days in the given year."
13: start = date(y, 1, 1)
14: end = date(y, 12, 31)
15: numDays = (end  start).days + 1
16: return [ start + timedelta(days=n) for n in range(numDays) ]
17:
18: def triangleDays(y):
19: "Return a list of all the triangle days in the given year."
20: return [x for x in allDays(y) if isTriangleDay(x) ]
isTriangleDay
is a Boolean function that implements the test Cook described for a datetime.date
object. Note that Line 5 extracts just the last two digits of the year, which is what Cook intends. You could, I suppose, change Line 9 to
python:
9: return sides[0] + sides[1] >= sides[2]
if you want to accept degenerate triangles, where the three sides collapse onto a single line. I don’t.
The allDays
function uses a list comprehension to return a list of all the days in a given year, and triangleDays
calls isTriangleDay
to filter the results of allDays
down to just triangle days. I think both of these functions are selfexplanatory.
With these functions defined, I got all the triangle days for 2023 via
python:
print('\n'.join(x.strftime('%Y%m%d') for x in triangleDays(2023)))
which returned this list of dates (after reshaping into four columns):
20230123 20230627 20230919 20231117
20230222 20230628 20230920 20231118
20230223 20230717 20230921 20231119
20230224 20230718 20230922 20231120
20230321 20230719 20230923 20231121
20230322 20230720 20230924 20231122
20230323 20230721 20230925 20231123
20230324 20230722 20230926 20231124
20230325 20230723 20230927 20231125
20230420 20230724 20230928 20231126
20230421 20230725 20230929 20231127
20230422 20230726 20230930 20231128
20230423 20230727 20231014 20231129
20230424 20230728 20231015 20231130
20230425 20230729 20231016 20231212
20230426 20230816 20231017 20231213
20230519 20230817 20231018 20231214
20230520 20230818 20231019 20231215
20230521 20230819 20231020 20231216
20230522 20230820 20231021 20231217
20230523 20230821 20231022 20231218
20230524 20230822 20231023 20231219
20230525 20230823 20231024 20231220
20230526 20230824 20231025 20231221
20230527 20230825 20231026 20231222
20230618 20230826 20231027 20231223
20230619 20230827 20231028 20231224
20230620 20230828 20231029 20231225
20230621 20230829 20231030 20231226
20230622 20230830 20231031 20231227
20230623 20230915 20231113 20231228
20230624 20230916 20231114 20231229
20230625 20230917 20231115 20231230
20230626 20230918 20231116 20231231
That’s 136 triangle days for this year. To see how this count changes from year to year, I ran
python:
for y in range(2000, 2051):
print(f'{y} {len(triangleDays(y)):3d}')
which returned
2000 0
2001 12
2002 34
2003 54
2004 72
2005 88
2006 102
2007 114
2008 124
2009 132
2010 138
2011 142
2012 144
2013 144
2014 144
2015 144
2016 144
2017 144
2018 144
2019 144
2020 144
2021 142
2022 140
2023 136
2024 132
2025 127
2026 120
2027 113
2028 104
2029 93
2030 82
2031 72
2032 61
2033 51
2034 41
2035 33
2036 25
2037 19
2038 13
2039 8
2040 5
2041 2
2042 1
2043 0
2044 0
2045 0
2046 0
2047 0
2048 0
2049 0
2050 0
I knew there was no point in checking on years later in the century—it was obvious that every year after 2042 would have no triangle days. As you can see, the 2010s were the peak decade for triangle days. We’re now in the early stages of a 20year decline.
After doing this, I looked back at my code and decided that most serious Python programmers wouldn’t have done it the way I did. Instead of functions that returned lists, they would build allDays
and triangleDays
as iterators.^{1} Not because there’s any need to save space—the space used by 366 datetime.date
objects is hardly even noticeable—but because that’s more the current style.
So to make myself feel more like a real Pythonista, I rewrote the code like this:
python:
1: from datetime import date, timedelta
2:
3: def isTriangleDay(dt):
4: "Can the the year, month, and day of the given date be the sides of a triangle?"
5: y = dt.year % 100
6: m = dt.month
7: d = dt.day
8: sides = sorted(int(x) for x in (y, m, d))
9: return sides[0] + sides[1] > sides[2]
10:
11: def allDays(y):
12: "Iterator for all days in the given year."
13: d = date(y, 1, 1)
14: end = date(y, 12, 31)
15: while d <= end:
16: yield d
17: d = d + timedelta(days=1)
18:
19: def triangleDays(y):
20: "Iterator for all the triangle days in the given year."
21: return filter(isTriangleDay, allDays(y))
isTriangleDay
is unchanged, but allDays
now works its way through the days of the year with a while
loop and the yield
statement, and triangleDays
uses the filter
function to iterate through just the triangle days.
Using these functions is basically the same as using the listbased versions, except that you can’t pass an iterator to len
. So determining the number of triangle days over a range of years can be done by either by converting the iterator to a list before passing it to len
,
python:
for y in range(2000, 2051):
print(f'{y} {len(list(triangleDays(y))):3d}')
or by using the sum
command with an argument that produces a one for each element of triangleDays
,
python:
for y in range(2000, 2051):
print(f'{y} {sum(1 for x in triangleDays(y))}')
The former sort of defeats the purpose of using an iterator, so I guess it’s better practice to use the latter, even though I find it weird looking.
It may well be that my perception of “real” Python programmers is wrong and they wouldn’t bother with yield
and filter
in such a piddly little problem as this. But at least I got some practice with them.

A confession: I find it hard to distinguish between between the proper use of the terms generator and iterator. My sense is that generators provide a way of creating iterators. So once the function is written, do you have a generator, an iterator, or both? ↩