Rhett Allain, who’s the physics columnist for Wired and a professor of physics at Southeast Louisiana State University, solved a funny problem a couple of weeks ago involving a ladder leaning against a wall. You can see his solution on YouTube or Medium. I think of it as an oddball problem because it’s very different from the “ladder leaning against a wall” problems I’ve been seeing for over 40 years.

It started as a calculus problem in which you are to assume that Point A moves down the wall with a constant speed and then determine the speed of Point B. Prof. Allain thought (rightly) that Point A of a real ladder wouldn’t fall at a constant speed, so he decided to turn it into a physics problem with gravity acting to accelerate the ladder’s downward movement. His goal in this altered problem was to determine the point at which Point A loses contact with the wall. A key feature in Allain’s problem was zero friction between the wall and the ladder and the floor and the ladder. This certainly makes the solution easier.

Both of these problems are different from the ladder/wall problems that are commonly given to engineers in their introductory mechanics courses. Ladders and walls are typically found in the friction section of the study of statics. In these problems, the idea is to determine either

• the lowest angle for which the ladder doesn’t slide down the wall for a given coefficient of friction; or
• the lowest coefficient of friction for which the ladder doesn’t slide down the wall for a given angle.

The key word in these problems is doesn’t. These are statics problems, in which things don’t move, and we look for the conditions under which the ladder stays in place. Here are some images from example problems in textbooks I have:

From Seely & Ensign’s Analytical Mechanics for Engineers (Wiley, 1941):

From Den Hartog’s Mechanics (McGraw-Hill, 1948, but I have the Dover reprint):

From Synge & Griffith’s Principles of Mechanics (McGraw-Hill, 1949):

From McGill & King’s Engineering Mechanics: Statics (PWS-Kent, 1989):

They use different loading and different friction conditions, but basic idea is the same in all of them.

## Static friction

In addition to the laws of statics—Newton’s Second Law with no acceleration—these problems make use of Coulomb’s1 static friction relationship:

$f\le \mu \phantom{\rule{thinmathspace}{0ex}}N$

where $f$ is the friction force, which runs parallel to the contact surface in the direction opposite that of impending motion; $N$ is the normal force; and $\mu$ is the friction coefficient. Let’s talk about each of these.

In common speech, “impending” means “about to happen,” but that isn’t quite what the authors of engineering textbooks mean when they use it in this context. These are, after all, statics problems, so there is no movement about to happen. The meaning here has been stretched to “what would happen if there were no friction.”

The “normal” in “normal force” means “perpendicular.” The normal force is perpendicular to the contact surface.

The friction coefficient, $\mu$, is a marvel of engineering simplification. The magnitude of the friction force depends on many things: which two materials are in contact, their surface roughness, any lubrication that might be present, even the temperature. But to get practical solutions, we simplify these many conditions down to a single number. Well, maybe not a single number, because when you look up the friction coefficient for a particular set of materials, you’ll often find a range of values. Still, boiling friction down to a number, even if we don’t know the number exactly, is a great way to think about friction and get reasonable answers.

In the simplest version of the ladder problem, we assume there is friction between the ladder and the floor (Point B), but no friction between the ladder and the wall (Point A). If you look carefully, you’ll see that this is the problem Synge & Griffith were exploring. Here’s a free-body diagram of the ladder and all the forces acting on it:

We’ve taken the mass center of the ladder to be at its geometric center.

The three equations of statics are

$∑{F}_{x}={N}_{A}-{f}_{B}=0$ $∑{F}_{y}={N}_{B}-\mathrm{mg}=0$ $∑{M}_{B}=\mathrm{mg}\left(\frac{L}{2}\mathrm{cos}\theta \right)-{N}_{A}\left(L\mathrm{sin}\theta \right)=0$

Here, the x and y directions are horizontal and vertical as usual, and the positive direction for moments about Point B is counterclockwise.

Because we were smart in choosing Point B to take moments about, the second and third equations have only one unknown variable each and can be solved directly:

${N}_{B}=\mathrm{mg}$ ${N}_{A}=\frac{\mathrm{mg}}{2}\mathrm{cot}\theta$

We substitute the second of these solutions into the first statics equation to get

${f}_{B}=\frac{\mathrm{mg}}{2}\mathrm{cot}\theta$

Here’s where the friction coefficient comes in. We know that ${f}_{B}\le \mu {N}_{B}$, so substituting in the solutions from statics, this inequality turns into

${f}_{B}=\frac{\mathrm{mg}}{2}\mathrm{cot}\theta \le \mu {N}_{B}=\mu \mathrm{mg}$

or, after rearranging,

$\mu \ge \frac{1}{2}\mathrm{cot}\theta$

So this gives us the minimum friction coefficient for the ladder to stay up at a given angle. A little algebra tells us the lowest at which the ladder will stay up for a given coefficient of friction:

$\theta \ge {\mathrm{tan}}^{-1}\frac{1}{2\mu }$

If you’re worried about trig functions changing signs and messing up these inequalities, recall that the ladder angle is always between 0° and 90°, so there are no sign changes for tangent or cotangent.

## A slightly more complicated ladder problem

Now let’s add friction between the ladder and the wall. To keep the number of variables to a minimum, we’ll assume the ladder/wall friction coefficient is the same as the ladder/floor friction coefficient.

Here’s the free-body diagram:

The addition of ${f}_{A}$ to the system means we no longer have a statically determinant system. Now we have four unknowns to go with the three equations of statics:

$∑{F}_{y}={f}_{A}+{N}_{B}-\mathrm{mg}=0$ $∑{F}_{x}={N}_{A}-{f}_{B}=0$ $∑{M}_{B}=\mathrm{mg}\left(\frac{L}{2}\mathrm{cos}\theta \right)-{N}_{A}\left(L\mathrm{sin}\theta \right)-{f}_{A}\left(L\mathrm{cos}\theta \right)=0$

And we have two inequalities of Coulomb friction:

${f}_{A}\le \mu {N}_{A}\phantom{\rule{mediummathspace}{0ex}},\phantom{\rule{1em}{0ex}}{f}_{B}\le \mu {N}_{B}$

We can combine these five relationships, and if we’re very careful in keeping the inequalities pointed in the right directions, we’ll come up with an inequality that relates the friction coefficient to the angle of the ladder. But most engineers wouldn’t solve the problem that way. Instead, they’d use the following physical reasoning to simplify the algebra:

1. When two surfaces are on the verge of slipping, the inequality in Coulomb’s friction relationship turns into an equality. This makes the algebraic manipulations easier, because we don’t have to worry about the inequality signs flipping on us.
2. The ladder can’t slip at just one of its ends. If it slips at Point A, it must also slip at Point B, and vice versa. And the same is true when the two points are on the verge of slipping.
3. The larger the friction coefficient, the lower the angle at which the ladder will be on the verge of slipping. So when we solve for the ladder angle or friction coefficient on the verge of slipping, it will be the minimum angle or minimum friction coefficient.

Using these three bits of reasoning, we can say

${f}_{A}=\mu {N}_{A}\phantom{\rule{mediummathspace}{0ex}},\phantom{\rule{1em}{0ex}}{f}_{B}=\mu {N}_{B}$

and combine these equations with the statics equations as follows:

The second equation of statics tells us that

${f}_{B}={N}_{A}$

so

${N}_{A}=\mu {N}_{B}$

The first equation of statics tells us

${f}_{A}=\mathrm{mg}-{N}_{B}$

so

$\mathrm{mg}-{N}_{B}=\mu {N}_{A}={\mu }^{2}{N}_{B}$

Therefore,

${N}_{B}=\frac{\mathrm{mg}}{1+{\mu }^{2}}$ ${N}_{A}=\frac{\mu \mathrm{mg}}{1+{\mu }^{2}}$

and

${f}_{A}=\frac{{\mu }^{2}\mathrm{mg}}{1+{\mu }^{2}}$

Plugging the expressions for ${N}_{A}$ and ${f}_{A}$ into the third equation of statics gives

$\mathrm{mg}\left(\frac{L}{2}\mathrm{cos}\theta \right)-\frac{\mu \mathrm{mg}}{1+{\mu }^{2}}\left(L\mathrm{sin}\theta \right)-\frac{{\mu }^{2}\mathrm{mg}}{1+{\mu }^{2}}\left(L\mathrm{cos}\theta \right)=0$

which can be rearranged to

$\left(\frac{1}{2}-\frac{{\mu }^{2}}{1+{\mu }^{2}}\right)\mathrm{cos}\theta =\frac{\mu }{1+{\mu }^{2}}\phantom{\rule{thinmathspace}{0ex}}\mathrm{sin}\theta$

This can be simplified in the following steps,

$\frac{1+{\mu }^{2}-2{\mu }^{2}}{2\left(1+{\mu }^{2}\right)}\mathrm{cos}\theta =\frac{\mu }{1+{\mu }^{2}}\phantom{\rule{thinmathspace}{0ex}}\mathrm{sin}\theta$ $\frac{1-{\mu }^{2}}{2}\mathrm{cos}\theta =\mu \mathrm{sin}\theta$

to

$\mathrm{tan}\theta =\frac{1-{\mu }^{2}}{2\mu }$

So if we’re given $\mu$, the minimum angle for which the ladder won’t slip is

$\theta ={\mathrm{tan}}^{-1}\left(\frac{1-{\mu }^{2}}{2\mu }\right)$

If $\mu >1$, this gives us a negative $\theta$, which is outside the range of ladder angles (0° to 90°) for which the statics equations were written. So a friction coefficient greater than one means the ladder will stay up at any angle.

If we’re given the ladder angle, the minumum friction coefficient to prevent slipping is

$\mu =\sqrt{1+{\mathrm{tan}}^{2}\theta }-\mathrm{tan}\theta$

This one of the two solutions to a quadratic equation. The other gives a negative friction coefficient, which we don’t care about. As the ladder angle approaches 0°, the minimum friction coefficient approaches one. This is consistent with the finding above that a friction coefficient above one means the ladder will stay up at any angle.

Note that our claims that these are the minimum angle and friction coefficient don’t come out of the algebra, they come out of Item 3 of our physical reasoning.

By the way, just because we went through this solution using physical reasoning to simplify the math, that doesn’t mean we couldn’t get the same result using the inequalities and carrying out the algebra. I have a few pages in my notebook in which I did that (including a couple of mistakes along the way) just to prove to myself that I could.

## Comparison

One last thing we can do is plot the results of the two problems and see how they compare. Here are the miniumum friction coefficients necessary to keep the ladder from slipping over the full range of angles.

There’s not much difference between the two problems when the ladder angle is high. That’s because the ladder isn’t pressing hard against the wall at those angles, so most of the work of keeping the ladder up in the second problem is being done by the floor/ladder friction. On the other hand, at low ladder angles, the difference between the two problems is huge; the wall/ladder friction in the second problem is doing a lot of work.

Simple problems like this aren’t especially important in the everyday working life of an engineer (unless you work for Werner), but the principles they teach are applicable across many disciplines. That’s why they appear in so many textbooks.

1. This is Charles-Augustin Coloumb, the guy who’s most famous for his law of electrical charges. In accordance with Stiger’s Law of Eponymy, the friction relationship that bears his name was written about by others before him

# Again with man pages and BBEdit

Julia Evans has been posting on Mastodon recently about the GNU Project’s insistence on documenting its commands through info pages instead of man pages and (the following may be biased by my own thoughts) how absolutely awful that is. The posts reminded me that although I wrote about how I open and read man pages in BBEdit last year, I never showed how I use references to related man pages as links. Time to change that.

So you don’t have to go through the terrible burden of reading an earlier blog post, here’s the source code of the command I use from the Terminal (or iTerm) to open man pages in a new BBEdit window:

bash:
1:  #!/bin/bash
2:
3:  # Interpret the arguments as command name and section. As with man,
4:  # the section is optional and comes first if present.
5:  if [[ $# -lt 2 ]]; then 6: cmd=${1}
7:    sec=''
8:  else
9:    cmd=${2} 10: sec=${1}
11:  fi
12:
13:  # Get the formatted man page, filter out backspaces and convert tabs
14:  # to spaces, and open the text in a new BBEdit document. Set the title
15:  # of the window and scroll to the top.
16:  man $sec$cmd | col -bx | bbedit --view-top --clean -t $cmd  This is slightly different from the code I originally posted. It incorporates options suggested by readers to the bbedit command in the pipeline of Line 16: 1. --view-top puts the cursor and the scroll position at the top of the document. 2. --clean sets the state of the document to unmodified so you can close it without getting the “do you want to save this?” warning. 3. -t$cmd sets the title of the document to the command name.

I’ve also changed the name of the command from bman to bbman to better fit the naming pattern set by bbedit, bbfind, and bbdiff. So if I type

bbman ls


at the command line, a new BBEdit window will open1 with the text of the ls man page. The bold characters that I’d see if I ran

man ls


don’t appear in the BBEdit window, but I’ve never gotten any value out of that limited sort of text formatting, so I don’t miss it.

Although most of what I do in a man page is search and read, sometimes I like to use the hints within the text to open a new, related man page. So I wrote an AppleScript (BBEdit has great AppleScript support) that uses the cursor postion or the selected text to open a new page. Here’s how it works:

Say I’m in SEE ALSO section of the ls man page, and I want to open the chmod man page that’s referred to there. I can either double-click to select chmod or just single-click to put the cursor within the word.

I then select from the Scripts menu—or, more likely, use the ⌃⇧M shortcut I’ve assigned to it—and up will pop a new window with the chmod man page.

Here’s the Man Page AppleScript:

 1:  use AppleScript version "2.4" -- Yosemite (10.10) or later
3:
4:  -- This script is expected to be run either with the command name selected (as if
5:  -- by double-clicking) or with the cursor within the command name. The section
6:  -- (in parentheses) may be immediately after the command name.
7:
8:  -- Function for getting the man page section from between parenthesis.
9:  -- Input is the character position of the opening parenthesis (if present).
10:  -- Returns the section or an empty string.
11:  on getSection(parenPos)
12:    tell application "BBEdit"
13:      if (character parenPos of front document as text) is "(" then
14:        set secStart to parenPos + 1
15:        set secEnd to find ")" searching in front document
16:        set secEnd to (characterOffset of secEnd's found object) - 1
17:        return characters secStart through secEnd of front document as text
18:      else
19:        return ""
20:      end if
21:    end tell
22:  end getSection
23:
24:  -- Start by selecting the word the cursor is in (via ⌥←, ⌥⇧→) if there isn't already a selection.
25:  tell application "BBEdit"
26:    if length of selection is 0 then
27:      tell application "System Events"
28:        key code 123 using option down
29:        delay 0.125
30:        key code 124 using {option down, shift down}
31:        delay 0.125
32:      end tell
33:    end if
34:  end tell
35:
36:  -- Set the command name according to the selection and the section according to
37:  -- whatever may be in parentheses immediately after the command name. This is
38:  -- in a new tell block to ensure that the selection has been updated by the
39:  -- previous tell block.
40:  tell application "BBEdit"
41:    set cmdName to selection as text
42:    set parenPos to (characterOffset of selection) + (length of selection)
43:    set manSection to my getSection(parenPos)
44:  end tell
45:
46:  -- Get the man page and pipe it through col to delete backspaces and expand tabs. Then
47:  -- pipe that to bbedit with appropriate options. The --clean option means the new
48:  -- document is treated as unmodified so it can be closed without a confirmation dialog.
49:  set manCmd to "man " & manSection & " " & cmdName & " | col -bx "
50:  set manCmd to manCmd & "| /usr/local/bin/bbedit --view-top --clean -t " & cmdName
51:  do shell script manCmd


I think it’s pretty well commented. You can see that Lines 48–51 invoke the same shell pipeline used in the bbman script. The main features that make this different from the shell script are:

1. Handling the case in which no text is selected but the cursor is within the name of the command. Lines 25–34 check for this condition and select the enclosing word by simulating ⌥← followed by ⌥⇧→.
2. Figuring out the man page section by looking inside the parentheses that may follow the name of the command. That’s handled by the getSection function in Lines 11–22, which uses the character position of the end of the command name to start its search.

Over the years, I’ve seen lots of ways to turn man pages into HTML, which would make the linking more natural. But they’ve all seemed more trouble than they’re worth. The trick of passing the man output through col -bx to turn it into plain text is something I’ve always come back to, whether my preferred text editor has been BBEdit, TextMate, or (on Linux) NEdit.

1. On my computer, BBEdit is always running.

# Holes in the Wolfram Knowledgebase

Wolfram touts its Knowledgebase as “the world’s largest and broadest repository of computable knowledge” and “carefully curated expert knowledge directly derived from primary sources.” There’s certainly a lot in there, but there are some inexplicable holes that could be filled with little effort.

I used the Knowledgebase last year in my post about orbital curvature. Things like

Entity["Planet", "Earth"]["AverageOrbitDistance"]


and

Entity["Planet", "Earth"]["Mass"]


pulled information out of the Knowledgebase so I didn’t have to look it up outside of Mathematica and paste it into my code. Very convenient.

But I learned a few months ago that another part of the Knowledgebase was missing data, which could get in the way of other types of calculation. I was testing out the kind of state- and county-level information I could access, and my initial explorations focused on where I live: DuPage County, Illinois.

To be sure, the Knowledgebase has lots of info on DuPage County. It knows, for example, the area, the population, the per capita income, and the number of annual births and deaths. But it doesn’t know the county seat, which I would think is easier to determine and enter into the Knowledgebase than most of the other stuff—not to mention more stable than transient figures like population and income.

Broadening my exploration to all the counties in Illinois, I learned that of our 102 counties, Wolfram knew the capitals of all of them except DuPage and DeKalb counties. So this command

AdministrativeDivisionData[
"UnitedStates"}], "CapitalName"]


returns

Chicago


as expected, while both of these commands,

AdministrativeDivisionData[
"UnitedStates"}], "CapitalName"]


and

AdministrativeDivisionData[
"UnitedStates"}], "CapitalName"]


return

Missing["NotAvailable"]


This is not exactly obscure information, and there are reliable sources from which to get it. Here, for example is a map from the Illinois Blue Book, an official publication of the state.

As you (and the folks at Wolfram) can see, the DuPage and DeKalb county seats are Wheaton and Sycamore, respectively.

I sent an email to Wolfram about the missing county seat data and got a boilerplate reply saying their development team would review it. That was in August; the Knowledgebase still returns Missing["NotAvailable"].

Recently, I decided to look for missing county seats in every state. Here are all the counties—or administrative divisions that Wolfram treats like counties— that are missing their capitals in the Knowledgebase:

County w/o seat State
DeKalb County Alabama
Mono County California
Sierra County California
Wakulla County Florida
Columbia County Georgia
Crawford County Georgia
DeKalb County Georgia
Echols County Georgia
Kalawao County Hawaii
Owyhee County Idaho
DeKalb County Illinois
DuPage County Illinois
DeKalb County Indiana
LaPorte County Indiana
Plaquemines Parish Louisiana
St. James Parish Louisiana
Keweenaw County Michigan
Lake of the Woods County Minnesota
DeSoto County Mississippi
Franklin County Mississippi
DeKalb County Missouri
Burlington County New Jersey
Mora County New Mexico
Rio Arriba County New Mexico
Bronx County (The Bronx) New York
Broome County New York
Kings County (Brooklyn) New York
New York County (Manhattan) New York
Queens County (Queens) New York
Richmond County (Staten Island) New York
Camden County North Carolina
Currituck County North Carolina
Hyde County North Carolina
Dunn County North Dakota
Bristol County Rhode Island
Kent County Rhode Island
Buffalo County South Dakota
DeKalb County Tennessee
Borden County Texas
Glasscock County Texas
Kenedy County Texas
King County Texas
Loving County Texas
McMullen County Texas
Montague County Texas
Palo Pinto County Texas
Young County Texas
Rich County Utah
Alexandria (independent city) Virginia
Amelia County Virginia
Bath County Virginia
Bland County Virginia
Bristol (independent city) Virginia
Buckingham County Virginia
Buena Vista (independent city) Virginia
Charles City County Virginia
Charlottesville (independent city) Virginia
Chesapeake (independent city) Virginia
Colonial Heights (independent city) Virginia
Covington (independent city) Virginia
Cumberland County Virginia
Danville (independent city) Virginia
Dinwiddie County Virginia
Emporia (independent city) Virginia
Fairfax (independent city) Virginia
Falls Church (independent city) Virginia
Fluvanna County Virginia
Franklin (independent city) Virginia
Fredericksburg (independent city) Virginia
Galax (independent city) Virginia
Goochland County Virginia
Hampton (independent city) Virginia
Hanover County Virginia
Harrisonburg (independent city) Virginia
Hopewell (independent city) Virginia
Isle of Wight County Virginia
King and Queen County Virginia
King George County Virginia
King William County Virginia
Lancaster County Virginia
Lexington (independent city) Virginia
Lunenburg County Virginia
Lynchburg (independent city) Virginia
Manassas (independent city) Virginia
Manassas Park (independent city) Virginia
Martinsville (independent city) Virginia
Mathews County Virginia
Middlesex County Virginia
Nelson County Virginia
New Kent County Virginia
Newport News (independent city) Virginia
Norfolk (independent city) Virginia
Northumberland County Virginia
Norton (independent city) Virginia
Nottoway County Virginia
Petersburg (independent city) Virginia
Poquoson (independent city) Virginia
Portsmouth (independent city) Virginia
Powhatan County Virginia
Prince George County Virginia
Richmond County Virginia
Roanoke County Virginia
Salem (independent city) Virginia
Stafford County Virginia
Staunton (independent city) Virginia
Suffolk (independent city) Virginia
Sussex County Virginia
Virginia Beach (independent city) Virginia
Waynesboro (independent city) Virginia
Williamsburg (independent city) Virginia
Winchester (independent city) Virginia

Quite a list. Now there are legitimate (or at least arguable) reasons some of these counties are missing their county seat:

1. Some counties really don’t have a county seat. Kalawao County in Hawaii, for example.
2. Some administrative districts in Alaska don’t have a capital. Alaska has boroughs rather than counties, and one of them, called the Unorganized Borough,1 is further subdivided into “census areas,” none of which have a capital.
3. Virginia has, in addition to counties, “independent cities,” which appear to be at the same level as counties.2 While I think Wolfram would be justified in saying these independent cities are their own capitals, it’s decided to say the capitals are missing, which is a legitimate choice, too.
4. Five counties in New York match up with the boroughs of New York City. Pretty hard to choose the capital of a portion of a city, so these counties also have missing capitals.

But most of the counties with missing county seats are like DuPage and DeKalb counties—regular counties with regular county seats that are just not included in the Knowledgebase, despite them being easy to look up and verify. There are fewer than 100 of them. I don’t know why they’re missing, but filling in missing values like this is a pretty standard data cleaning operation. And as I said earlier, this is pretty much a one-time operation; counties just don’t change seats very often.

I haven’t sent this list off to Wolfram. If the people on its development team can’t be bothered to clean the data in their own home state, how likely is it that they’ll fill in all the other states’ data? But they should.

Update 4 Dec 2023 11:45 AM
Chon Torres on Mastodon informed me that the two California counties, Mono and Sierra, do have county seats, but they’re unincorporated, and that might explain why they’re missing from the Knowledgebase. That’s a good explanation, but I would argue with Wolfram that it’s a poor reason for excluding a capital. A county seat should have county government offices—Chon mentioned that he’s been at the Sierra County Courthouse in Downieville—but I don’t see why it needs a municipal government.

Adding to the madness of Virginia government, Sam Davies told me that an independent city can also be the county seat of a county that it’s been carved out of. The example he gave was Charlottesville, which is both an independent city and the capital of Albermarle County. To me, a more disturbing example is Fairfax, which is an independent city but also the county seat of—yes, that’s right—Fairfax County.

Thanks to Chon and Sam for the local government expertise.

1. Which is apparently not actually a borough itself, despite its name. This is more than I wanted to know about Alaska’s government.

2. As with the Unorganized Borough in Alaska, this is more than I wanted to know about Virginia’s government.

# Dates, triangles, and Python

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 self-explanatory.

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):

2023-01-23     2023-06-27     2023-09-19     2023-11-17
2023-02-22     2023-06-28     2023-09-20     2023-11-18
2023-02-23     2023-07-17     2023-09-21     2023-11-19
2023-02-24     2023-07-18     2023-09-22     2023-11-20
2023-03-21     2023-07-19     2023-09-23     2023-11-21
2023-03-22     2023-07-20     2023-09-24     2023-11-22
2023-03-23     2023-07-21     2023-09-25     2023-11-23
2023-03-24     2023-07-22     2023-09-26     2023-11-24
2023-03-25     2023-07-23     2023-09-27     2023-11-25
2023-04-20     2023-07-24     2023-09-28     2023-11-26
2023-04-21     2023-07-25     2023-09-29     2023-11-27
2023-04-22     2023-07-26     2023-09-30     2023-11-28
2023-04-23     2023-07-27     2023-10-14     2023-11-29
2023-04-24     2023-07-28     2023-10-15     2023-11-30
2023-04-25     2023-07-29     2023-10-16     2023-12-12
2023-04-26     2023-08-16     2023-10-17     2023-12-13
2023-05-19     2023-08-17     2023-10-18     2023-12-14
2023-05-20     2023-08-18     2023-10-19     2023-12-15
2023-05-21     2023-08-19     2023-10-20     2023-12-16
2023-05-22     2023-08-20     2023-10-21     2023-12-17
2023-05-23     2023-08-21     2023-10-22     2023-12-18
2023-05-24     2023-08-22     2023-10-23     2023-12-19
2023-05-25     2023-08-23     2023-10-24     2023-12-20
2023-05-26     2023-08-24     2023-10-25     2023-12-21
2023-05-27     2023-08-25     2023-10-26     2023-12-22
2023-06-18     2023-08-26     2023-10-27     2023-12-23
2023-06-19     2023-08-27     2023-10-28     2023-12-24
2023-06-20     2023-08-28     2023-10-29     2023-12-25
2023-06-21     2023-08-29     2023-10-30     2023-12-26
2023-06-22     2023-08-30     2023-10-31     2023-12-27
2023-06-23     2023-09-15     2023-11-13     2023-12-28
2023-06-24     2023-09-16     2023-11-14     2023-12-29
2023-06-25     2023-09-17     2023-11-15     2023-12-30
2023-06-26     2023-09-18     2023-11-16     2023-12-31


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 20-year 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 list-based 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.

1. 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?