I assume that most of you have already listened—at least in part—to the latest episode of *Upgrade*, the 40th Anniversary of the Mac Draft. Any podcast with both John Gruber and John Siracusa is going be longer than your average *Upgrade*; I’m impressed they brought it in at 2½ hours. The whole show is good, but the Hall of Shame section is just hilarious.

I don’t have the sort of encyclopedic Mac knowledge the draft participants—Shelly Brisbin, Stephen Hackett, Dan Moren, Jason Snell, and the aforementioned Johns—have, but I do want to mention what my picks would be for two of the categories: first Mac and best/favorite Mac software.

\nMy first Mac slips in between Siracusa’s original 128k Mac and Shelly’s Mac Plus.^{1} It was the 512k Mac, popularly known as the Fat Mac because it had *so much* memory. I bought it in early 1985, when I was was in grad school, and wrote my thesis on it. I took advantage of a discount through the University of Illinois, and added on an external diskette drive, an ImageWriter printer, and the cool canvas carrying bag, in which I hauled the Mac between my apartment and office—just like in Apple’s marketing literature of the time.

I tend to think of the Fat Mac as the first truly useable Mac, mainly because it had enough RAM for apps to have some breathing room. Even better, Switcher, which allowed you to run multiple apps at once before MultiFinder, gave the Fat Mac a giant advantage over the original. I was teaching a course in the spring semester of ’85 and made up my tests on the Mac, making the illustrations in MacPaint and then switching over instantly to paste them into a MacWrite document.

\nAs for my best/favorite Mac software, I’m going to do the traditional podcast draft thing and talk about how I was torn between two apps, thus giving me effectively two picks. But before that, I need to point out that the choices made by those on the podcast are all objectively better than mine. I’m deliberately choosing apps that are weird but were very useful to me in the early years. They also showed off the Mac’s user interface to great advantage.

\nThe app I nearly chose was Macintosh Pascal, written by THINK Technologies (who went on to publish Lightspeed Pascal and Lightspeed C, which had the greatest programmer slogan ever: “Make mistakes faster”) and distributed by Apple.

\n\nThis was an interpreted Pascal, so the code ran relatively slowly, but the lack of a compilation step made the edit/debug/edit/debug cycle go quickly. On the editing side, Macintosh Pascal was my introduction to syntax highlighting, which you can see (sort of) in the screenshot above. This was the Mac’s font styles used to great effect. Debugging was also done visually by dragging a stop sign to the line of code that needed a breakpoint. These are things we take for granted now, but they were a revelation to me in 1985.

\nSo with my not-quite-choice out of the way, here’s my fave: \nClaris CAD.

\n\nAm I kidding? No. This was a fantastic program for the kind of drawing I was doing back in the early ’90s and am still doing today. It wasn’t drafting, per se, but it did involve the kinds of construction typically done on a drafting table: lines tangent to circles, circles tangent to lines, lines perpendicular to other lines, and so on. The cursor would snap to features of the drawing and show a preview of how the next item would be drawn. It nearly always did exactly what I wanted.

\nAnd this is not just hazy nostalgia; I thought Claris CAD was an amazing program at the time. It was simply more in tune with the drawing of manufactured objects than were (and are) apps more obviously based on Bezier curves.

\nThese apps were not only great, they are perfect for a draft. No one would snipe them.

\n\n

\n

"}, {"title": "A Typinator snippet for plotting", "url": "https://leancrew.com/all-this/2024/01/a-typinator-snippet-for-plotting/", "author": {"name": "Dr. Drang"}, "summary": "A bunch of Python/Matplotlib code that's easy to customize for the kinds of graphs I typically make.", "date_published": "2024-01-14T18:02:59+00:00", "id": "https://leancrew.com/all-this/2024/01/a-typinator-snippet-for-plotting/", "content_html": "\n

- \n
- \n
I also had a Plus, but it was my second Mac and was bought for me by my employer. ↩

\n \n

My last post ended with this graph:

\n\nIt was made, as most of my graphs are, using Python and Matplotlib. Here’s the code that did it:

\n`python:\n 1: #!/usr/bin/env python\n 2: \n 3: import matplotlib.pyplot as plt\n 4: from matplotlib.ticker import MultipleLocator, AutoMinorLocator\n 5: import numpy as np\n 6: import sys\n 7: \n 8: # Array of angles in degrees\n 9: theta = np.arange(0.5, 90.0, 0.5)\n10: \n11: # Arrays of friction coefficients\n12: muFloor = 1/(2*np.tan(theta*np.pi/180))\n13: muBoth = np.sqrt(1 + np.tan(theta*np.pi/180)**2) - np.tan(theta*np.pi/180)\n14: \n15: # Create the plot with a given size in inches\n16: fig, ax = plt.subplots(figsize=(6, 4))\n17: \n18: # Add the lines\n19: ax.plot(theta, muFloor, '-', color='#1b9e77', lw=2, label=\"Floor only\")\n20: ax.plot(theta, muBoth, '-', color='#d95f02', lw=2, label=\"Wall and floor\")\n21: \n22: # Set the limits\n23: plt.xlim(xmin=0, xmax=90)\n24: plt.ylim(ymin=0, ymax=2)\n25: \n26: # Set the major and minor ticks and add a grid\n27: ax.xaxis.set_major_locator(MultipleLocator(15))\n28: ax.xaxis.set_minor_locator(AutoMinorLocator(3))\n29: ax.yaxis.set_major_locator(MultipleLocator(.5))\n30: ax.yaxis.set_minor_locator(AutoMinorLocator(2))\n31: # ax.grid(linewidth=.5, axis='x', which='major', color='#dddddd', linestyle='-')\n32: # ax.grid(linewidth=.5, axis='y', which='major', color='#dddddd', linestyle='-')\n33: \n34: # Title and axis labels\n35: plt.title('Leaning ladder problem')\n36: plt.xlabel('Ladder angle (degrees)')\n37: plt.ylabel('Friction coefficient')\n38: \n39: # Make the border and tick marks 0.5 points wide\n40: [ i.set_linewidth(0.5) for i in ax.spines.values() ]\n41: ax.tick_params(which='both', width=.5)\n42: \n43: # Add the legend\n44: ax.legend(loc=(.58, .62), frameon=False)\n45: \n46: # Save as SVG\n47: plt.savefig('20240110-Friction comparison graph.svg', format='svg')\n`

\nIn broad outline, this is how nearly all of my Matplotlib graphs are made, because I have a Typinator snippet that inserts generic plot-making code that I modify to set the limits, tick marks, legend, etc. that are appropriate for the graph.

\nBy the way, I don’t think I’ve mentioned here that I’ve switched to Typinator. Ergonis was having a sale at the tail end of 2022, and I decided to give it a go. I had been using Keyboard Maestro for my snippets for several years, but KM isn’t truly meant for text expansion, and I wanted to move to something more built-for-purpose. TextExpander would, I suppose, be the obvious choice, but I didn’t want another subscription. Typinator looked to have all the features I needed, and I haven’t regretted the choice in the year I’ve been using it.

\nAnyway, here’s the Typinator definition of my plotting snippet:

\n\nAs you can see, the abbreviation is `;plot`

. As you can’t see, at least not fully, the expansion is this:

`import matplotlib.pyplot as plt\nfrom matplotlib.ticker import MultipleLocator, AutoMinorLocator\n\n# Create the plot with a given size in inches\nfig, ax = plt.subplots(figsize=(6, 4))\n\n# Add a line\nax.plot(x, y, '-', color='blue', lw=2, label='Item one')\n\n# Set the limits\n# plt.xlim(xmin=0, xmax=100)\n# plt.ylim(ymin=0, ymax=50)\n\n# Set the major and minor ticks and add a grid\n# ax.xaxis.set_major_locator(MultipleLocator(20))\n# ax.xaxis.set_minor_locator(AutoMinorLocator(2))\n# ax.yaxis.set_major_locator(MultipleLocator(10))\n# ax.yaxis.set_minor_locator(AutoMinorLocator(5))\n# ax.grid(linewidth=.5, axis='x', which='both', color='#dddddd', linestyle='-')\n# ax.grid(linewidth=.5, axis='y', which='major', color='#dddddd', linestyle='-')\n\n# Title and axis labels\nplt.title('{{?Plot title}}')\nplt.xlabel('{{?X label}}')\nplt.ylabel('{{?Y label}}')\n\n# Make the border and tick marks 0.5 points wide\n[ i.set_linewidth(0.5) for i in ax.spines.values() ]\nax.tick_params(which='both', width=.5)\n\n# Add the legend\n# ax.legend()\n\n# Save as PDF\nplt.savefig('{{?File name}}.pdf', format='pdf')\n`

\nTypinator uses special syntax for placeholder text. In this case, the placeholders are

\n- \n
- The plot’s title:
`{{?Plot title}}`

\n - The x-axis label:
`{{?X label}}`

\n - The y-axis label:
`{{?Y label}}`

\n - The file name:
`{{?File name}}`

\n

I don’t have to remember this syntax. The {…} popup menu has a bunch of placeholder options and will insert the correct code when you define the placeholder.

\n\nWhen I type `;plot`

, this dialog box appears to enter the text for each placeholder:

Typinator remembers the last text I entered for each placeholder, which makes things go faster if I’m doing a series of similar graphs.

\nIf you go through the snippet, you’ll see that many of the lines of Matplotlib code are commented out, in particular those that set the formatting of the graph. That’s because I usually like to see what Matplotlib comes up with by default. If I don’t like the defaults, I uncomment the appropriate lines and start tinkering. You’ll also notice that this produces PDFs by default; that’s because the plots I make for work have always been PDFs. As I expect to be writing very few work reports from now on, I’ll probably change the file format in the last line from PDF to SVG or PNG.

"}, {"title": "Ladders and friction", "url": "https://leancrew.com/all-this/2024/01/ladders-and-friction/", "author": {"name": "Dr. Drang"}, "summary": "A classic statics problem.", "date_published": "2024-01-11T13:54:33+00:00", "id": "https://leancrew.com/all-this/2024/01/ladders-and-friction/", "content_html": "[Equations in this post may not look right (or appear at all) in your RSS reader. Go to the original article to see them rendered properly.]

\n\n

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.

\nBoth 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

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

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.

\nIn addition to the laws of statics—Newton’s Second Law with no acceleration—these problems make use of Coulomb’s^{1} static friction relationship:

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.

\nIn 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.”

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

\nThe 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.

\nIn 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:

\n\nWe’ve taken the mass center of the ladder to be at its geometric center.

\nThe three equations of statics are

\nSum;{F}_{x}={N}_{A}-{f}_{B}=0\nSum;{F}_{y}={N}_{B}-\mathrm{mg}=0\nSum;{M}_{B}=\mathrm{mg}\left(\frac{L}{2}\mathrm{cos}\theta \right)-{N}_{A}\left(L\mathrm{sin}\theta \right)=0\nHere, 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{N}_{B}=\mathrm{mg}\n{N}_{A}=\frac{\mathrm{mg}}{2}\mathrm{cot}\theta\nWe substitute the second of these solutions into the first statics equation to get

\n{f}_{B}=\frac{\mathrm{mg}}{2}\mathrm{cot}\theta\nHere’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

\n{f}_{B}=\frac{\mathrm{mg}}{2}\mathrm{cot}\theta \le \mu {N}_{B}=\mu \mathrm{mg}\nor, after rearranging,

\n\mu \ge \frac{1}{2}\mathrm{cot}\theta\nSo 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:

\n\theta \ge {\mathrm{tan}}^{-1}\frac{1}{2\mu}\nIf 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.

\nNow 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.

\nHere’s the free-body diagram:

\n\nThe 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:

\nSum;{F}_{y}={f}_{A}+{N}_{B}-\mathrm{mg}=0\nSum;{F}_{x}={N}_{A}-{f}_{B}=0\nSum;{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\nAnd we have two inequalities of Coulomb friction:

\n{f}_{A}\le \mu {N}_{A}\phantom{\rule{\"mediummathspace\"}{0ex}},\phantom{\rule{\"1em\"}{0ex}}{f}_{B}\le \mu {N}_{B}\nWe 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:

\n- \n
- 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. \n
- 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. \n
- 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. \n

Using these three bits of reasoning, we can say

\n{f}_{A}=\mu {N}_{A}\phantom{\rule{\"mediummathspace\"}{0ex}},\phantom{\rule{\"1em\"}{0ex}}{f}_{B}=\mu {N}_{B}\nand combine these equations with the statics equations as follows:

\nThe second equation of statics tells us that

\n{f}_{B}={N}_{A}\nso

\n{N}_{A}=\mu {N}_{B}\nThe first equation of statics tells us

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

\n\mathrm{mg}-{N}_{B}=\mu {N}_{A}={\mu}^{2}{N}_{B}\nTherefore,

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

\n{f}_{A}=\frac{{\mu}^{2}\mathrm{mg}}{1+{\mu}^{2}}\nPlugging the expressions for {N}_{A} and {f}_{A} into the third equation of statics gives

\n\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\nwhich can be rearranged to

\n(\frac{1}{2}-\frac{{\mu}^{2}}{1+{\mu}^{2}})\mathrm{cos}\theta =\frac{\mu}{1+{\mu}^{2}}\phantom{\rule{\"thinmathspace\"}{0ex}}\mathrm{sin}\theta\nThis can be simplified in the following steps,

\n\frac{1+{\mu}^{2}-2{\mu}^{2}}{2(1+{\mu}^{2})}\mathrm{cos}\theta =\frac{\mu}{1+{\mu}^{2}}\phantom{\rule{\"thinmathspace\"}{0ex}}\mathrm{sin}\theta\n\frac{1-{\mu}^{2}}{2}\mathrm{cos}\theta =\mu \mathrm{sin}\theta\nto

\n\mathrm{tan}\theta =\frac{1-{\mu}^{2}}{2\mu}\nSo if we’re given \mu, the minimum angle for which the ladder won’t slip is

\n\theta ={\mathrm{tan}}^{-1}\left(\frac{1-{\mu}^{2}}{2\mu}\right)\nIf \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.

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

\n\mu =\sqrt{1+{\mathrm{tan}}^{2}\theta}-\mathrm{tan}\theta\nThis 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.

\nNote 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.

\nBy 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.

\nOne 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.

\n\nThere’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.

\nSimple 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.

\n\n

\n

"}, {"title": "Again with man pages and BBEdit", "url": "https://leancrew.com/all-this/2023/12/again-with-man-pages-and-bbedit/", "author": {"name": "Dr. Drang"}, "summary": "A shell script (which we’ve seen before) and an AppleScript to help with viewing.", "date_published": "2023-12-21T21:50:23+00:00", "id": "https://leancrew.com/all-this/2023/12/again-with-man-pages-and-bbedit/", "content_html": "\n

- \n
- \n
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. ↩

\n \n

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.

\nSo 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:

\n`bash:\n 1: #!/bin/bash\n 2: \n 3: # Interpret the arguments as command name and section. As with `man`,\n 4: # the section is optional and comes first if present.\n 5: if [[ $# -lt 2 ]]; then\n 6: cmd=${1}\n 7: sec=''\n 8: else\n 9: cmd=${2}\n10: sec=${1}\n11: fi\n12: \n13: # Get the formatted man page, filter out backspaces and convert tabs\n14: # to spaces, and open the text in a new BBEdit document. Set the title\n15: # of the window and scroll to the top.\n16: man $sec $cmd | col -bx | bbedit --view-top --clean -t $cmd\n`

\nThis 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:

- \n
`--view-top`

puts the cursor and the scroll position at the top of the document. \n`--clean`

sets the state of the document to unmodified so you can close it without getting the “do you want to save this?” warning. \n`-t $cmd`

sets the title of the document to the command name. \n

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\n`

\nat the command line, a new BBEdit window will open^{1} with the text of the `ls`

man page. The bold characters that I’d see if I ran

`man ls\n`

\ndon’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.

\nAlthough 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:

\nSay 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 `chmod`

man page.

Here’s the Man Page AppleScript:

\n` 1: use AppleScript version \"2.4\" -- Yosemite (10.10) or later\n 2: use scripting additions\n 3: \n 4: -- This script is expected to be run either with the command name selected (as if\n 5: -- by double-clicking) or with the cursor within the command name. The section\n 6: -- (in parentheses) may be immediately after the command name.\n 7: \n 8: -- Function for getting the man page section from between parenthesis.\n 9: -- Input is the character position of the opening parenthesis (if present).\n10: -- Returns the section or an empty string.\n11: on getSection(parenPos)\n12: tell application \"BBEdit\"\n13: if (character parenPos of front document as text) is \"(\" then\n14: set secStart to parenPos + 1\n15: set secEnd to find \")\" searching in front document\n16: set secEnd to (characterOffset of secEnd's found object) - 1\n17: return characters secStart through secEnd of front document as text\n18: else\n19: return \"\"\n20: end if\n21: end tell\n22: end getSection\n23: \n24: -- Start by selecting the word the cursor is in (via ⌥←, ⌥⇧→) if there isn't already a selection.\n25: tell application \"BBEdit\"\n26: if length of selection is 0 then\n27: tell application \"System Events\"\n28: key code 123 using option down\n29: delay 0.125\n30: key code 124 using {option down, shift down}\n31: delay 0.125\n32: end tell\n33: end if\n34: end tell\n35: \n36: -- Set the command name according to the selection and the section according to\n37: -- whatever may be in parentheses immediately after the command name. This is\n38: -- in a new tell block to ensure that the selection has been updated by the\n39: -- previous tell block.\n40: tell application \"BBEdit\"\n41: set cmdName to selection as text\n42: set parenPos to (characterOffset of selection) + (length of selection)\n43: set manSection to my getSection(parenPos)\n44: end tell\n45: \n46: -- Get the man page and pipe it through col to delete backspaces and expand tabs. Then\n47: -- pipe that to bbedit with appropriate options. The --clean option means the new\n48: -- document is treated as unmodified so it can be closed without a confirmation dialog.\n49: set manCmd to \"man \" & manSection & \" \" & cmdName & \" | col -bx \"\n50: set manCmd to manCmd & \"| /usr/local/bin/bbedit --view-top --clean -t \" & cmdName\n51: do shell script manCmd\n`

\nI 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:

- \n
- 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 ⌥⇧→. \n
- 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. \n

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.

\n

\n

"}, {"title": "Holes in the Wolfram Knowledgebase", "url": "https://leancrew.com/all-this/2023/12/holes-in-the-wolfram-knowledgebase/", "author": {"name": "Dr. Drang"}, "summary": "There's a lot of missing county-level data for no good reason.", "date_published": "2023-12-03T21:02:42+00:00", "id": "https://leancrew.com/all-this/2023/12/holes-in-the-wolfram-knowledgebase/", "content_html": "\n

- \n
- \n
On my computer, BBEdit is always running. ↩

\n \n

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.

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

\n`Entity[\"Planet\", \"Earth\"][\"AverageOrbitDistance\"]\n`

\nand

\n`Entity[\"Planet\", \"Earth\"][\"Mass\"]\n`

\npulled 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.

\nBut 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.

\nTo 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.

\nBroadening 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

\n`AdministrativeDivisionData[\n Entity[\"AdministrativeDivision\", {\"CookCounty\", \"Illinois\", \n \"UnitedStates\"}], \"CapitalName\"]\n`

\nreturns

\n`Chicago\n`

\nas expected, while both of these commands,

\n`AdministrativeDivisionData[\n Entity[\"AdministrativeDivision\", {\"DuPageCounty\", \"Illinois\", \n \"UnitedStates\"}], \"CapitalName\"]\n`

\nand

\n`AdministrativeDivisionData[\n Entity[\"AdministrativeDivision\", {\"DeKalbCounty\", \"Illinois\", \n \"UnitedStates\"}], \"CapitalName\"]\n`

\nreturn

\n`Missing[\"NotAvailable\"]\n`

\nThis 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.

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

\nI 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:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nCounty w/o seat | State |
---|---|

DeKalb County | Alabama |

Aleutians West | Alaska |

Bethel | Alaska |

Chugach | Alaska |

Copper River | Alaska |

Dillingham | Alaska |

Hoonah-Angoon | Alaska |

Nome | Alaska |

Prince of Wales-Hyder | Alaska |

Petersburg | Alaska |

Skagway | Alaska |

Southeast Fairbanks | Alaska |

Kusilvak Census Area | Alaska |

Wrangell | Alaska |

Yukon-Koyukuk | Alaska |

Mono County | California |

Sierra County | California |

Conejos County | Colorado |

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 |

McPherson County | Nebraska |

Esmeralda County | Nevada |

Eureka County | Nevada |

Lincoln County | Nevada |

Storey County | Nevada |

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 |

Radford (independent city) | 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:

\n- \n
- Some counties really don’t have a county seat. Kalawao County in Hawaii, for example. \n
- 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. \n - 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. \n - 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. \n

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.

\nI 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.

\n\n

\n**Update 4 Dec 2023 11:45 AM**

\nChon 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.

\nThanks to Chon and Sam for the local government expertise.

\n\n

\n

"}, {"title": "Dates, triangles, and Python", "url": "https://leancrew.com/all-this/2023/12/dates-triangles-and-python/", "author": {"name": "Dr. Drang"}, "summary": "A short piece of code to followup on a John D. Cook post.", "date_published": "2023-12-01T18:31:12+00:00", "id": "https://leancrew.com/all-this/2023/12/dates-triangles-and-python/", "content_html": "\n

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

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

\n \n

Tuesday morning, which was November 28, John D. Cook started a post with

\n\n\nThe 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.

\n

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?

\nSo I wrote a little Python to answer these questions.

\n`python:\n 1: from datetime import date, timedelta\n 2: \n 3: def isTriangleDay(dt):\n 4: \"Can the the year, month, and day of the given date be the sides of a triangle?\"\n 5: y = dt.year % 100\n 6: m = dt.month\n 7: d = dt.day\n 8: sides = sorted(int(x) for x in (y, m, d))\n 9: return sides[0] + sides[1] > sides[2]\n10: \n11: def allDays(y):\n12: \"Return a list of all days in the given year.\"\n13: start = date(y, 1, 1)\n14: end = date(y, 12, 31)\n15: numDays = (end - start).days + 1\n16: return [ start + timedelta(days=n) for n in range(numDays) ]\n17: \n18: def triangleDays(y):\n19: \"Return a list of all the triangle days in the given year.\"\n20: return [x for x in allDays(y) if isTriangleDay(x) ]\n`

\n`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:\n 9: return sides[0] + sides[1] >= sides[2]\n`

\nif you want to accept degenerate triangles, where the three sides collapse onto a single line. I don’t.

\nThe `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

\n`python:\nprint('\\n'.join(x.strftime('%Y-%m-%d') for x in triangleDays(2023)))\n`

\nwhich returned this list of dates (after reshaping into four columns):

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

\nThat’s 136 triangle days for this year. To see how this count changes from year to year, I ran

\n`python:\nfor y in range(2000, 2051):\n print(f'{y} {len(triangleDays(y)):3d}')\n`

\nwhich returned

\n`2000 0\n2001 12\n2002 34\n2003 54\n2004 72\n2005 88\n2006 102\n2007 114\n2008 124\n2009 132\n2010 138\n2011 142\n2012 144\n2013 144\n2014 144\n2015 144\n2016 144\n2017 144\n2018 144\n2019 144\n2020 144\n2021 142\n2022 140\n2023 136\n2024 132\n2025 127\n2026 120\n2027 113\n2028 104\n2029 93\n2030 82\n2031 72\n2032 61\n2033 51\n2034 41\n2035 33\n2036 25\n2037 19\n2038 13\n2039 8\n2040 5\n2041 2\n2042 1\n2043 0\n2044 0\n2045 0\n2046 0\n2047 0\n2048 0\n2049 0\n2050 0\n`

\nI 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.

\nAfter 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:

\n`python:\n 1: from datetime import date, timedelta\n 2: \n 3: def isTriangleDay(dt):\n 4: \"Can the the year, month, and day of the given date be the sides of a triangle?\"\n 5: y = dt.year % 100\n 6: m = dt.month\n 7: d = dt.day\n 8: sides = sorted(int(x) for x in (y, m, d))\n 9: return sides[0] + sides[1] > sides[2]\n10: \n11: def allDays(y):\n12: \"Iterator for all days in the given year.\"\n13: d = date(y, 1, 1)\n14: end = date(y, 12, 31)\n15: while d <= end:\n16: yield d\n17: d = d + timedelta(days=1)\n18: \n19: def triangleDays(y):\n20: \"Iterator for all the triangle days in the given year.\"\n21: return filter(isTriangleDay, allDays(y))\n`

\n`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:\nfor y in range(2000, 2051):\n print(f'{y} {len(list(triangleDays(y))):3d}')\n`

\nor by using the `sum`

command with an argument that produces a one for each element of `triangleDays`

,

`python:\nfor y in range(2000, 2051):\n print(f'{y} {sum(1 for x in triangleDays(y))}')\n`

\nThe 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.

\nIt 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.

\n

\n

"}, {"title": "A couple of game followups", "url": "https://leancrew.com/all-this/2023/11/a-couple-of-game-followups/", "author": {"name": "Dr. Drang"}, "summary": "A better grep command for picking a new Wordle guess and a Shortcut to eliminate an annoyance I have with Conlextions.", "date_published": "2023-11-20T18:20:27+00:00", "id": "https://leancrew.com/all-this/2023/11/a-couple-of-game-followups/", "content_html": "\n

- \n
- \n
A confession: I find it hard to distinguish between between the proper use of the terms

\n*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? ↩ \n

Here are some little tricks associated with Wordle and Conlextions, the Connections-like puzzle put out by Lex Friedman.

\nLast week, I mentioned that I needed a new starting guess for Wordle, and I wrote about how I used some simple command-line tools to see if the word I was considering was an appropriate choice. In a nutshell, I had been using IRATE as my first guess, but because it was the answer recently I wanted to try a new initial guess that might be the answer in the future. ALTER seemed like a good choice, but I wanted to make sure it hadn’t already been used. I had a list of all the answers in chronological order in a file named `answers.txt`

, and the most recent answer was DWELT.

My solution involved three separate commands. All three involved `grep`

and one included `head`

in a pipeline. Reader Leon Cowle was unsatisfied with this and began casting about for a cleaner solution. Here’s the single command he came up with to replace the three I used:

`egrep 'alter|dwelt' answers.txt\n`

\nThe output was

\n`dwelt\nalter\n`

\nwhich told me that ALTER was an answer and that it would come after DWELT. This was everything I needed to know in one step. Beautiful!

\nYou might argue that for a one-off like this, the solution that occurs to you first is the best because it takes the least amount of your time. Generally speaking, I agree with that, and I’m not unhappy with my three-command solution. But Leon has given me the best of both worlds. I got to have my own inefficient solution that I thought of quickly *and* I got to learn from his more elegant solution. Knowing which of two text strings appears first in a file is something I’m pretty sure I’ve had to do before and will have to do again. Now I have a simple and effective solution to pull out of my toolbox. Thanks, Leon!

As for Conlextions, I’ve been playing it for a few weeks and recommend it to anyone who likes the *NY Times* Connections game but is finding it a little too easy. I’ve been playing Connections since early summer and while it has gotten more difficult, the sense of accomplishment I get in solving it with no mistakes is wearing off. Conlextions is more diabolical and more satisfying when you solve it in four guesses.

I have only three gripes with Conlextions:

\n- \n
- I don’t think the
*t*belongs in its name. \n - Lex’s tendency to define groups as “words that begin with S,” or some other letter, is frustrating as hell because it’s both ridiculously easy and a connection I almost never see. \n
- The info that you share with others when you solve the puzzle includes the time it took. I’m a slow and methodical player, certain that Lex has laid so many traps that I need to think through all the possibilities three or four times before committing myself to any grouping. Including the time I spend is embarrassing, especially when I see the solution times of people like Dan Moren and Greg Pierce. \n

In protest of this prejudice against the excessively careful, I’ve recently taken to deleting the solve time from my posts on Mastodon. And to avoid the tedium of backspacing, I made this Shortcut that does the deleting for me:

\n\nIt searches the clipboard for a linefeed, the word “Solve,” and all the text after that. It replaces that with the empty string and puts the updated text back onto the clipboard.

\nSo now when I want to post my Conlextions result to Mastodon, I tap the “Share these results” button, press and hold the side button on my phone to activate Siri, and say “Delete Time.” The name of the Shortcut seems to be distinct enough that Siri hasn’t misinterpreted it yet.

\nI would not normally use Shortcuts for an automation like this. Keyboard Maestro would allow me to invoke it with a keystroke and could also do the pasting. But since I always play Conlextions on my phone, Shortcuts was the best option.

"}, {"title": "Nearly cheating", "url": "https://leancrew.com/all-this/2023/11/nearly-cheating/", "author": {"name": "Dr. Drang"}, "summary": "In which I skate up to the edge of cheating at Wordle.", "date_published": "2023-11-14T17:47:53+00:00", "id": "https://leancrew.com/all-this/2023/11/nearly-cheating/", "content_html": "Last week I solved Wordle in a single guess. My go-to first guess, IRATE, finally came in after months (over a year, I think) of use. So what do I do now? Seems like a good time to switch my starting guess.

\nBefore we go any further, I should mention, for those of you who don’t commit every utterance of mine to memory and are thinking “There was no IRATE last week,” that I don’t play the *NY Times* version of Wordle. Back in early 2022, right after the *Times* bought Wordle, I downloaded the original version and set it up on a server I control. That’s the game my family and I have been playing ever since. Overall, I’d say this was unnecessary. The *Times* hasn’t screwed up the game the way I thought it would,^{1} but it’s too late now for us to change.

Based on letter frequency tables, I figured ALTER would be a good new initial guess. But because I was so delighted when IRATE came up, I’d like to choose a word that

\n- \n
- Is among the list of 2315 words that are answers. \n
- Hasn’t been an answer already. \n

And I want to see if ALTER passes these two tests without spoiling myself with other answers, especially those that may be coming up soon.

\nI have the data to do this if I’m careful. In order to build my `wordle`

script, I needed to pull out all the answers and acceptable guesses from the original Wordle JavaScript source code. I was able to do this nearly blind—I saw just the first and last couple of words in each list and have long since forgotten them—and save them to three files: `guesses.txt`

, `answers.txt`

, and `wordle.txt`

, the last of which is a blending of the two others.

Some simple shell commands got me what I wanted. First, the simple part:

\n`grep alter answers.txt\n`

\nreturned `alter`

, which confirmed my belief that ALTER is an answer and didn’t reveal any other answers. Checking whether it had already been an answer was only a little trickier.

The `answers.txt`

file has the answer words in chronological order, one per line. To figure out where I currently am in that list, I got the line number of yesterday’s word, DWELT.

`grep -n dwelt answers.txt\n`

\nThe `-n`

option causes the line number of the found text to be printed along with the text:

`878:dwelt\n`

\nWith this information, I can now see if ALTER has already been the answer:

\n`head -n 878 answers.txt | grep alter\n`

\nThe `head`

command outputs just the first 878 lines of `answers.txt`

and the `grep`

command looks for `alter`

in that text. It returned nothing, so ALTER has *not* been an answer so far. It passes my test and will be my first guess from now on.

What I like about this solution is that I was run my test quickly without seeing any other answer words and without tipping myself off to when ALTER will appear, which I could have easily done with

\n`grep -n alter answers.txt\n`

\nI recognize that some of you will read this and think I’ve crossed the line of Wordle ethics into outright cheating. You can keep your opinions to yourself.

\n\n

\n

"}, {"title": "Consecutive heads or tails", "url": "https://leancrew.com/all-this/2023/11/consecutive-heads-or-tails/", "author": {"name": "Dr. Drang"}, "summary": "An extension of the Taskmaster coin flip game.", "date_published": "2023-11-12T02:26:59+00:00", "id": "https://leancrew.com/all-this/2023/11/consecutive-heads-or-tails/", "content_html": "\n

- \n
- \n
Although I recall some outrage about a year ago when FEAST was the too-on-the-nose word for Thanksgiving Day. ↩

\n \n

[Equations in this post may not look right (or appear at all) in your RSS reader. Go to the original article to see them rendered properly.]

\n\n

Earlier today, I talked about sticking with a problem longer than I probably should because I can’t stop. Let’s apply that pathology to the *Taskmaster* coin flip subtask and think about a slightly different problem.^{1} Suppose we flip a fair coin and stop when we’ve flipped *either* five consecutive heads *or* five consecutive tails. What is the expected number of flips?

As we did with the simpler game, we can imagine this as a board game to help us configure the Markov chain transition matrix. Here, we treat consecutive heads as positives and consecutive tails as negatives.

\n\n\n

\n**Update 11 Nov 2023 11:12 PM**

\nI screwed this up the first time through and should’ve seen the error before I published. The transformation matrices are correct now and give a result that makes more sense. Sorry about that.

We start with our marker on Square 0 and start flipping. A head moves us one square to the right; a tail moves us one square to the left. If we’re on a positive square, a tail takes to Square –1. If we’re on a negative square, a head takes us Square 1. The game ends when we reach either Square 5 or Square –5.

\nHere’s the game’s transformation matrix, where we’ve set the squares at the two ends of the board to be absorbing states:

\nP=\left[\begin{array}{ccccccccccc}1& 0& 0& 0& 0& 0& 0& 0& 0& 0& 0\\ \frac{1}{2}& 0& 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0\\ 0& \frac{1}{2}& 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0\\ 0& 0& \frac{1}{2}& 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0\\ 0& 0& 0& \frac{1}{2}& 0& 0& \frac{1}{2}& 0& 0& 0& 0\\ 0& 0& 0& 0& \frac{1}{2}& 0& \frac{1}{2}& 0& 0& 0& 0\\ 0& 0& 0& 0& \frac{1}{2}& 0& 0& \frac{1}{2}& 0& 0& 0\\ 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0& \frac{1}{2}& 0& 0\\ 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0& \frac{1}{2}& 0\\ 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0& 0& \frac{1}{2}\\ 0& 0& 0& 0& 0& 0& 0& 0& 0& 0& 1\end{array}\right]\nThe rows and columns represent the squares in numerical order from –5 to 5. The lower right half is similar to the transformation matrix we used in the earlier post, and the upper left half is sort of the upside-down mirror image of the lower right half. The main difference is that we never go to Square 0 after a flip; if the coin comes up opposite the run we were on, we go to the first square on the opposite side of Square 0.

\nAs we’ve done in the past, we create a Q transformation matrix by eliminating the rows and columns associated with the absorbing states from the P matrix:

\nQ=\left[\begin{array}{ccccccccc}0& 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0\\ \frac{1}{2}& 0& 0& 0& 0& \frac{1}{2}& 0& 0& 0\\ 0& \frac{1}{2}& 0& 0& 0& \frac{1}{2}& 0& 0& 0\\ 0& 0& \frac{1}{2}& 0& 0& \frac{1}{2}& 0& 0& 0\\ 0& 0& 0& \frac{1}{2}& 0& \frac{1}{2}& 0& 0& 0\\ 0& 0& 0& \frac{1}{2}& 0& 0& \frac{1}{2}& 0& 0\\ 0& 0& 0& \frac{1}{2}& 0& 0& 0& \frac{1}{2}& 0\\ 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0& \frac{1}{2}\\ 0& 0& 0& \frac{1}{2}& 0& 0& 0& 0& 0\\ \end{array}\right]\nThen we proceed as before, forming the matrix equation

\n(I-Q)\phantom{\rule{\"thinmathspace\"}{0ex}}m=1\nwhere m is the column vector of the expected number of flips to get to an absorbing state from Squares –4 through 4, and 1 is a nine-element column vector of ones. Solving this^{2} we get

So the expected number of flips to get either five consecutive heads *or* five consecutive tails is {m}_{0}=31, the value in the middle of this vector.\nThis is half the value we got last time, which makes sense.

Am I done with Markov chains now? I hope so, but you never know.

\n\n

\n

"}, {"title": "More general sunrise/sunset plots", "url": "https://leancrew.com/all-this/2023/11/more-general-sunrise-sunset-plots/", "author": {"name": "Dr. Drang"}, "summary": "Code that I spent too much time on.", "date_published": "2023-11-11T17:47:37+00:00", "id": "https://leancrew.com/all-this/2023/11/more-general-sunrise-sunset-plots/", "content_html": "\n

- \n
- \n
By the way, you can now see the whole episode on YouTube. ↩

\n \n - \n
Which I did with Mathematica, but you could do with any number of programs. Excel, for example. ↩

\n \n

I recently did that thing we’ve all done, where I kept working on a problem, refining the solution far beyond its value. I don’t think it’s necessarily wrong to overwork a problem—I always learn from the experience, and the reason I keep at it is that I’m having fun making the little improvements. And sometimes I get a blog post out of it.

\nEarlier this week, Michael Glotzer on Mastodon reminded me of a little script I wrote about 5 years ago that made this sunrise/sunset plot for Chicago:

\n\nAs I said at the time, there were several bespoke aspects to the script that built this plot. I did a lot of hand-editing of the US Naval Observatory data that was the script’s input. Also, the “Sunrise,” “Sunset,” and “Daylight” curve labels were set at positions and angles that were specific to the plot; they’d have to be redone if I wanted to make a plot for another city. To generalize the script to handle any set of USNO sunrise/sunset data, I’d have to alter the code to do the following:

\n- \n
- Read the data directly from the USNO text without editing. \n
- Add titles containing the location and year so the plots could be distinguished from one another. \n
- Change the way the curve labels were positioned to make them look good for any curve. \n

My goal was to feed the data to my script through standard input. I’d choose the location and year on the USNO site, copy the text that appears on the followup page,

\n\nand then pipe the text on the clipboard to my script via `pbpaste`

:

`pbpaste | sunplot\n`

\nThe result would be a PNG file in the current working directory, named according to the location and year—in this case, `Chicago, IL-2023.png`

.

As you can see, I’ve added the name and year as a title in the upper left corner and the latitude and longitude in the lower right corner. Also, the curves are labeled at their peaks or valleys, as appropriate. As before, the main curves are given in Standard Time (as the USNO presents it in its tables) and the curves associated with the darker yellow are given in Daylight Saving Time.

\nHere’s the code for `sunplot`

:

`python:\n 1: #!/usr/bin/env python\n 2: \n 3: import sys\n 4: import re\n 5: from dateutil.parser import parse\n 6: from datetime import datetime\n 7: from datetime import timedelta\n 8: from matplotlib import pyplot as plt\n 9: import matplotlib.dates as mdates\n 10: from matplotlib.ticker import MultipleLocator, FormatStrFormatter\n 11: \n 12: \n 13: # Functions\n 14: \n 15: def headerInfo(header):\n 16: \"Return location name, coordinates, and year from the USNO header lines.\"\n 17: \n 18: # Get the place name from the middle of the top line\n 19: left = 'o , o ,'\n 20: right = 'Astronomical Applications Dept.'\n 21: placeName = re.search(rf'{left}(.+){right}', header[0]).group(1).strip()\n 22: \n 23: # If the place name ends with a comma, a space, and a pair of capitals,\n 24: # assume it's in location, ST format and capitalize the location while\n 25: # keeping the state as all uppercase. Otherwise, capitalize all the words.\n 26: if re.match(r', [A-Z][A-Z]', placeName[-4:]):\n 27: placeParts = placeName.split(', ')\n 28: location = ', '.join(placeParts[:-1]).title()\n 29: state = placeParts[-1]\n 30: placeName = f'{location}, {state}'\n 31: else:\n 32: placeName = placeName.title()\n 33: \n 34: # The year is at a specific spot on the second line\n 35: year = int(header[1][80:84])\n 36: \n 37: # The latitude and longitude are at specific spots on the second line\n 38: longString = header[1][10:17]\n 39: latString = header[1][19:25]\n 40: \n 41: # Reformat the latitude into d° m′ N format (could be S)\n 42: dir = latString[0]\n 43: degree, minute = latString[1:].split()\n 44: lat = f'{int(degree)}° {int(minute)}′ {dir}'\n 45: \n 46: # Reformat the longitude into d° m′ W format\n 47: dir = longString[0]\n 48: degree, minute = longString[1:].split()\n 49: long = f'{int(degree)}° {int(minute)}′ {dir}'\n 50: \n 51: return placeName, lat, long, year\n 52: \n 53: def bodyInfo(body, isLeap):\n 54: \"Return lists of sunrise, sunset, and daylight length hours from the USNO body lines.\"\n 55: \n 56: # Initialize\n 57: sunrises = []\n 58: sunsets = []\n 59: lengths = []\n 60: \n 61: # Lengths of monthly columns\n 62: if isLeap:\n 63: daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n 64: else:\n 65: daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n 66: \n 67: # Rise and set character start positions for each month\n 68: risePos = [ 4 + 11*i for i in range(12) ]\n 69: setPos = [ 9 + 11*i for i in range(12) ]\n 70: \n 71: # Collect data from each day\n 72: for m in range(12):\n 73: for d in range(daysInMonth[m]):\n 74: riseString = body[d][risePos[m]:risePos[m]+4]\n 75: hour, minute = int(riseString[:2]), int(riseString[-2:])\n 76: sunrise = hour + minute/60\n 77: setString = body[d][setPos[m]:setPos[m]+4]\n 78: hour, minute = int(setString[:2]), int(setString[-2:])\n 79: sunset = hour + minute/60\n 80: sunrises.append(sunrise)\n 81: sunsets.append(sunset)\n 82: lengths.append(sunset - sunrise)\n 83: \n 84: return(sunrises, sunsets, lengths)\n 85: \n 86: def dstBounds(year):\n 87: \"Return the DST start and end day indices according to current US rules.\"\n 88: \n 89: # Start DST on second Sunday of March\n 90: d = 8\n 91: while datetime.weekday(dstStart := datetime(year, 3, d)) != 6:\n 92: d += 1\n 93: dstStart = (dstStart - datetime(year, 1, 1)).days\n 94: \n 95: # End DST on first Sunday of November\n 96: d = 1\n 97: while datetime.weekday(dstEnd := datetime(year, 11, d)) != 6:\n 98: d += 1\n 99: dstEnd = (dstEnd - datetime(year, 1, 1)).days\n100: \n101: return dstStart, dstEnd\n102: \n103: def centerPeak(data):\n104: \"Return the maximum value and the index of its central position.\"\n105: \n106: peak = max(data)\n107: # Average (to the nearest integer) the first and last peak indices\n108: peakPos = (data.index(peak) + (len(data) - data[-1::-1].index(peak))) // 2\n109: return peak, peakPos\n110: \n111: def centerValley(data):\n112: \"Return the minimum value and the index of its central position.\"\n113: \n114: valley = min(data)\n115: # Average (to the nearest integer) the first and last valley indices\n116: valleyPos = (data.index(valley) + (len(data) - data[-1::-1].index(valley))) // 2\n117: return valley, valleyPos\n118: \n119: \n120: # Start processing\n121: \n122: # Read the USNO data from stdin into a list of lines.\n123: # Text should come from https://aa.usno.navy.mil/data/RS_OneYear\n124: usno = sys.stdin.readlines()\n125: \n126: # Get location and year from header\n127: placeName, lat, long, year = headerInfo(usno[:2])\n128: \n129: # Is it a leap year?\n130: isLeap = (year % 400 == 0) or ((year % 4 == 0) and not (year % 100 == 0))\n131: \n132: # Get sunrise, sunset, and sunlight length lists from body\n133: sunrises, sunsets, lengths = bodyInfo(usno[9:], isLeap)\n134: \n135: # Generate list of days for the year\n136: currentDay = datetime(year, 1, 1)\n137: lastDay = datetime(year, 12, 31)\n138: days = [currentDay]\n139: while (currentDay := currentDay + timedelta(days=1)) <= lastDay:\n140: days.append(currentDay)\n141: \n142: # The portion of the year that uses DST\n143: dstStart, dstEnd = dstBounds(year)\n144: dstDays = days[dstStart:dstEnd]\n145: dstRises = [ x + 1 for x in sunrises[dstStart:dstEnd] ]\n146: dstSets = [ x + 1 for x in sunsets[dstStart:dstEnd] ]\n147: \n148: # Plot the data\n149: fig, ax =plt.subplots(figsize=(10,6))\n150: \n151: # Shaded areas\n152: plt.fill_between(days, sunrises, sunsets, facecolor='yellow', alpha=.5)\n153: plt.fill_between(days, 0, sunrises, facecolor='black', alpha=.25)\n154: plt.fill_between(days, sunsets, 24, facecolor='black', alpha=.25)\n155: plt.fill_between(dstDays, sunsets[dstStart:dstEnd], dstSets, facecolor='yellow', alpha=.5)\n156: plt.fill_between(dstDays, sunrises[dstStart:dstEnd], dstRises, facecolor='black', alpha=.1)\n157: \n158: # Curves\n159: plt.plot(days, sunrises, color='k')\n160: plt.plot(days, sunsets, color='k')\n161: plt.plot(dstDays, dstRises, color='k')\n162: plt.plot(dstDays, dstSets, color='k')\n163: plt.plot(days, lengths, color='#aa00aa', linestyle='--', lw=2)\n164: \n165: # Curve annotations centered on the peaks and valleys\n166: # To get these labels near the middle of the plot, we need to use\n167: # different functions for the northern and southern hemispheres\n168: riseFcn = {'N':centerValley, 'S':centerPeak}\n169: setFcn = {'N':centerPeak, 'S':centerValley}\n170: lengthFcn = {'N':centerPeak, 'S':centerValley}\n171: \n172: # Sunrise\n173: labeledRise, labeledRiseIndex = riseFcn[lat[-1]](sunrises)\n174: ax.text(days[labeledRiseIndex], labeledRise - 1, 'Sunrise', fontsize=12, color='black', ha='center')\n175: \n176: # Sunset\n177: labeledSet, labeledSetIndex = setFcn[lat[-1]](sunsets)\n178: ax.text(days[labeledSetIndex], labeledSet - 1, 'Sunset', fontsize=12, color='black', ha='center')\n179: \n180: # Daylight length\n181: labeledLight, labeledLightIndex = lengthFcn[lat[-1]](lengths)\n182: ax.text(days[labeledLightIndex], labeledLight + .75, 'Daylight', fontsize=12, color='#aa00aa', ha='center')\n183: \n184: # Place name and year in upper left; coordinates in lower right\n185: ax.text(datetime(year, 1, 20), 22, f'{placeName} – {year}', fontsize=16, color='black', ha='left')\n186: ax.text(datetime(year, 10, 10), 2, f'{lat}, {long}', fontsize=12, color='black', ha='left')\n187: \n188: # Background grids\n189: ax.grid(which='major', color='#cccccc', ls='-', lw=.5)\n190: ax.grid(which='minor', color='#cccccc', ls=':', lw=.5)\n191: \n192: # Horizontal axis shows month abbreviations between ticks\n193: ax.tick_params(axis='both', which='major', labelsize=12)\n194: plt.xlim(datetime(year, 1, 1), datetime(year, 12, 31))\n195: m = mdates.MonthLocator(bymonthday=1)\n196: mfmt = mdates.DateFormatter(' %b')\n197: ax.xaxis.set_major_locator(m)\n198: ax.xaxis.set_major_formatter(mfmt)\n199: \n200: # Vertical axis labels formatted like h:mm\n201: plt.ylim(0, 24)\n202: ymajor = MultipleLocator(4)\n203: yminor = MultipleLocator(1)\n204: tfmt = FormatStrFormatter('%d:00')\n205: ax.yaxis.set_major_locator(ymajor)\n206: ax.yaxis.set_minor_locator(yminor)\n207: ax.yaxis.set_major_formatter(tfmt)\n208: \n209: # Tighten up the white border and save\n210: fig.set_tight_layout({'pad': 1.5})\n211: plt.savefig(f'{placeName}-{year}.png', format='png', dpi=150)\n`

\nFor me, a 200-line script is pretty long. A lot of that is due to the continual tweaking I did to make it more general, more accommodating of different inputs.

\nThe sunrise and sunset data is read in from standard input on Line 122 and stored in a list of lines. Here’s an example:

\n` o , o , CHICAGO, IL Astronomical Applications Dept.\nLocation: W087 41, N41 51 Rise and Set for the Sun for 2023 U. S. Naval Observatory \n Washington, DC 20392-5420 \n Zone: 6h West of Greenwich \n\n\n Jan. Feb. Mar. Apr. May June July Aug. Sept. Oct. Nov. Dec. \nDay Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set Rise Set\n h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m h m\n01 0718 1630 0703 1706 0626 1741 0534 1816 0447 1849 0418 1919 0419 1930 0444 1909 0516 1824 0547 1733 0623 1645 0658 1621\n02 0718 1631 0702 1707 0624 1742 0532 1817 0446 1850 0418 1920 0420 1929 0445 1908 0517 1823 0549 1731 0624 1644 0700 1620\n03 0718 1632 0701 1708 0623 1743 0530 1818 0445 1851 0417 1921 0420 1929 0446 1907 0518 1821 0550 1729 0625 1643 0701 1620\n04 0718 1633 0700 1710 0621 1744 0529 1819 0443 1852 0417 1921 0421 1929 0447 1906 0519 1819 0551 1728 0627 1641 0702 1620\n05 0718 1634 0659 1711 0619 1746 0527 1820 0442 1853 0417 1922 0422 1929 0448 1904 0520 1818 0552 1726 0628 1640 0703 1620\n06 0718 1635 0658 1712 0618 1747 0525 1822 0441 1854 0416 1923 0422 1928 0450 1903 0521 1816 0553 1724 0629 1639 0704 1620\n07 0718 1636 0657 1713 0616 1748 0524 1823 0440 1856 0416 1923 0423 1928 0451 1902 0522 1814 0554 1722 0630 1638 0704 1620\n08 0718 1637 0656 1715 0615 1749 0522 1824 0438 1857 0416 1924 0424 1928 0452 1901 0524 1813 0555 1721 0631 1637 0705 1620\n09 0718 1638 0654 1716 0613 1750 0520 1825 0437 1858 0416 1925 0424 1927 0453 1859 0525 1811 0556 1719 0633 1636 0706 1620\n10 0718 1639 0653 1717 0611 1751 0519 1826 0436 1859 0415 1925 0425 1927 0454 1858 0526 1809 0557 1718 0634 1635 0707 1620\n11 0717 1640 0652 1719 0610 1753 0517 1827 0435 1900 0415 1926 0426 1926 0455 1857 0527 1807 0558 1716 0635 1634 0708 1620\n12 0717 1641 0651 1720 0608 1754 0516 1828 0434 1901 0415 1926 0426 1926 0456 1855 0528 1806 0559 1714 0636 1633 0709 1620\n13 0717 1642 0649 1721 0606 1755 0514 1829 0433 1902 0415 1927 0427 1925 0457 1854 0529 1804 0601 1713 0638 1632 0710 1620\n14 0716 1644 0648 1722 0605 1756 0512 1830 0432 1903 0415 1927 0428 1925 0458 1852 0530 1802 0602 1711 0639 1631 0710 1620\n15 0716 1645 0647 1724 0603 1757 0511 1831 0431 1904 0415 1927 0429 1924 0459 1851 0531 1800 0603 1709 0640 1630 0711 1620\n16 0715 1646 0645 1725 0601 1758 0509 1833 0430 1905 0415 1928 0430 1924 0500 1850 0532 1759 0604 1708 0641 1629 0712 1621\n17 0715 1647 0644 1726 0559 1759 0508 1834 0429 1906 0415 1928 0430 1923 0501 1848 0533 1757 0605 1706 0642 1628 0712 1621\n18 0714 1648 0642 1727 0558 1800 0506 1835 0428 1907 0415 1929 0431 1922 0502 1847 0534 1755 0606 1705 0644 1628 0713 1621\n19 0714 1649 0641 1729 0556 1802 0505 1836 0427 1908 0415 1929 0432 1921 0503 1845 0535 1753 0607 1703 0645 1627 0714 1622\n20 0713 1651 0640 1730 0554 1803 0503 1837 0426 1909 0416 1929 0433 1921 0504 1844 0536 1752 0609 1702 0646 1626 0714 1622\n21 0713 1652 0638 1731 0553 1804 0501 1838 0425 1910 0416 1929 0434 1920 0505 1842 0537 1750 0610 1700 0647 1625 0715 1623\n22 0712 1653 0637 1732 0551 1805 0500 1839 0425 1911 0416 1929 0435 1919 0506 1841 0538 1748 0611 1659 0648 1625 0715 1623\n23 0711 1654 0635 1734 0549 1806 0459 1840 0424 1912 0416 1930 0436 1918 0507 1839 0539 1746 0612 1657 0650 1624 0716 1624\n24 0710 1656 0634 1735 0548 1807 0457 1841 0423 1913 0417 1930 0437 1917 0508 1837 0540 1745 0613 1656 0651 1624 0716 1624\n25 0710 1657 0632 1736 0546 1808 0456 1843 0422 1913 0417 1930 0438 1916 0509 1836 0541 1743 0614 1655 0652 1623 0717 1625\n26 0709 1658 0631 1737 0544 1809 0454 1844 0422 1914 0417 1930 0439 1915 0510 1834 0542 1741 0616 1653 0653 1623 0717 1626\n27 0708 1659 0629 1738 0542 1811 0453 1845 0421 1915 0418 1930 0440 1914 0511 1833 0543 1740 0617 1652 0654 1622 0717 1626\n28 0707 1701 0627 1740 0541 1812 0451 1846 0420 1916 0418 1930 0441 1913 0512 1831 0544 1738 0618 1650 0655 1622 0718 1627\n29 0706 1702 0539 1813 0450 1847 0420 1917 0418 1930 0442 1912 0513 1829 0545 1736 0619 1649 0656 1621 0718 1628\n30 0705 1703 0537 1814 0449 1848 0419 1918 0419 1930 0442 1911 0514 1828 0546 1734 0620 1648 0657 1621 0718 1629\n31 0704 1704 0536 1815 0419 1918 0443 1910 0515 1826 0622 1646 0718 1629\n\nAdd one hour for daylight time, if and when in use.\n`

\nIt’s a wide table, so you’ll have to scroll sideways to see everything.

\nThe location and year are in the first two lines of the header. They’re collected with the `headerInfo`

function defined in Lines 15–51. It’s kind of a long function because the name of the location is given in uppercase (no matter how you specify the name in the input field), and I want mixed case in title. So there’s some messing around in the code to make sure the state or territory abbreviation, if present, is maintained as uppercase but the rest is converted to title case. Like this:

- \n
- CHICAGO, IL ⇒ Chicago, IL \n
- ST. PAUL, MN ⇒ St. Paul, MN \n
- CHARLOTTE AMALIE, ST. THOMAS, VI ⇒ Charlotte Amalie, St. Thomas, VI \n
- HOME ⇒ Home \n

As the last example suggests, you can enter the coordinates of your house and label it “Home,” but the web site will unhelpfully turn that into “HOME” in the output. The `headerInfo`

function will reconvert that back to “Home.”

You’ll note also that the latitude and longitude comes with the direction letter first, the degrees (with a leading zero if necessary), a space, and then the minutes. I prefer a more conventional expression, so `headerInfo`

does conversions like this:

- \n
- W064 56 ⇒ 64° 56′ W \n
- N18 20 ⇒ 18° 20′ N \n

This fussiness to fit my taste takes up lines of code, but I get what I want.

\nThe body of the data, which starts on the tenth line of the input and continues more or less to the end, contains all the sunrise and sunset times at specific character positions. The `bodyInfo`

function, defined in Lines 53–84, loops through the days of the year, month by month and day by day, extracting the data and converting it into a floating point number that represents the time of day in hours. It then simply subtracts the sunrise from the sunset to get the number of hours of daylight. A trick used later in the code—a holdover from the original version of the script—is that the vertical axis is plotting these floating point numbers. It only looks like it’s plotting a time object because the tick labels are formatted with a colon followed by two zeros.

Because the monthly data are in columns and the number of days in February is inconsistent, we have to pass a Boolean value, `isLeap`

to `bodyInfo`

to tell it how many rows of the February columns to go. The `isLeap`

variable is set in Line 128 using the standard Gregorian rules. I’m sure I could have used an existing library function to do this, but it was fun to do it in a single line of code. And yes, I tested that line against every type of year, even though I’ll be long dead before the simple but incorrect

`year % 4 == 0\n`

\nrule will fail in 2100.

\nThe `dstBounds`

function in Lines 86–101 returns the start and end dates for DST using the current rules for the United States. I decided I didn’t need a more global approach because the USNO site is really geared to US locations. When it comes to weak points in the code, this is #1, because these rules could be changed at any time. As with leap years, I’m sure I could have used a library to work out these dates, but I liked the idea of writing the code myself. If the rules change, I’ll know what to do.

(By the way, I’d say the #2 weak point is the assumed format of the data in the body of table. The USNO could change that at any time, and I’d have to go through `bodyInfo`

to update the character positions. Same with `headerInfo`

. But since the table layout has been consistent for years, I think those functions will survive a while.)

To me, the most interesting new code is the stuff that centers the curve labels under the peaks and valleys. Matplotlib has an optional argument to the `Axes.text`

function, `horizontalalignment`

or `ha`

for short, that knows how to center text at a particular coordinate, but that in itself isn’t good enough. Because the USNO data reports the sunrises and sunsets to the nearest minute, the minimum value of sunrise and maximum value of sunset last for several days. I want the labels to be centered within those stretches.

So I wrote the functions `centerPeak`

and `centerValley`

, defined in Lines 103–108 and 110–115, respectively, to return both the peak [valley] value and the middle location of the run of that peak [valley] value. Python’s `max`

and `min`

functions get the values, and the `index`

function will return the index of the first occurrence of those values. What `centerPeak`

and `centerValley`

do is also reverse the given list to find the index of the last occurrence of the max or min. It then averages (to the nearest integer) the first and last indices to get the index in the middle. This could be off by half a day from the true middle, but that’s not enough to notice on the graph.

Another interesting thing about these labels is that while the earliest sunrise, latest sunset, and longest daylight occur near the middle of the year for most of the US, there’s a place where that is reversed. American Samoa is in the southern hemisphere, so if I used the same label-positioning rules for it as I used for the rest of the country, the labels would be off at one end of the year or the other and might run off the end of the plot. So Lines 165–182 account for that by determining whether we search for peaks or valleys depending on the `N`

or `S`

at the end of the latitude string. For example, here’s a plot for Pago Pago:

They don’t use DST in Pago Pago; it’s too close to the equator for DST to be useful, and the US rules would make the adjustments go in the wrong direction. But I’m plotting it anyway—just as I plot it for Phoenix, Honolulu, and other places that don’t use DST—to show how the rules would work if they were applied. I’m showing Standard Time in the summer for places that switch to DST, so it seems consistent to show DST even in places where it doesn’t apply.

\nI’ve thought about adding a dashed line showing where DST would be in the winter months. Last year, the Senate passed a bill for year-round DST (the House let it lie), so it might be nice to show the consequences of that graphically. But for now, I’m going to put this away and move on to other ways of wasting my time.

\n\n

"}, {"title": "Taskmaster and Markov chains", "url": "https://leancrew.com/all-this/2023/11/taskmaster-and-markov-chains/", "author": {"name": "Dr. Drang"}, "summary": "Applying Markov chains to a Taskmasker task.", "date_published": "2023-11-10T13:19:30+00:00", "id": "https://leancrew.com/all-this/2023/11/taskmaster-and-markov-chains/", "content_html": "**Update 11 Nov 2023 4:32 PM**

\nBased on a tip from Marcos Huerta, I watched this video from Jacqueline Nolis and realized I have more work ahead if I expect to handle places in Alaska. Go to the USNO site and request data for Nome, AL. What a mess! Starting in late May, the sunsets are so late they roll over past midnight into the next day. From then until mid-July, a naive reading of the data (which is what my code does) says that sunset comes before sunrise. This year, and probably most years, there’s a day in mid-July with two sunsets, one just after the midnight at the beginning of the day and another just before the midnight at the end of the day. July 17, the day in question, has two lines associated with it.

Ms. Nolis, who’s pulling this same data from another online source and is using R to plot it, suggests using three days of data for each being plotted—the day before, the day of, and the day after—and then truncating the plot to show only the day of. That seems like a reasonable approach, but it may not be so reasonable if I continue to use the USNO data. I’ll have to give it some thought.

\nOne thing I’m sure of is that I want to stick with Python and Matplotlib instead of switching to R. I’ve done enough with R to know that its syntax is not for me, no matter how appealing Kieran Healy makes it seem.

\n[Equations in this post may not look right (or appear at all) in your RSS reader. Go to the original article to see them rendered properly.]

\n\n

In the latest episode of *Taskmaster*, of which Channel 4 has posted a sneak peek, there’s a subtask related to my last post on Markov chains. The contestants are required to flip a coin and get five consecutive heads before moving on to the next part of the task. As you might expect, there are a few double-headed coins available from earlier in the task, and that makes this subtask very easy for the contestants who notice them. But the subtask can also be done by brute force with a regular coin, which raises the question: how many flips would it take, on average, to accomplish this subtask with a fair coin?

It might be easier to see how Markov chains can be used to answer this question if we build a little board game—an even easier version of Snakes and Ladders—that matches the logic of the coin flip subtask. Imagine a six-square board with the squares labeled from 0 through 5.

\n\nYou start with your marker on the Square 0 and start flipping a coin. If it comes up heads, you move your marker forward; if it comes up tails, you go back to zero. You finish the game when your marker reaches the Square 5, which is the same as flipping 5 heads in a row.

\nAs we did in the earlier post, we’ll take the end square to be an absorbing state and build a transition matrix that reflects the probabilities of moving from a given square to the others. The rows represent the current square and the columns represent the square after the next flip.

\n\nAs you can see, the chance of moving forward one square is always ½, as is the chance of moving back to Square 0. The only square to which this doesn’t apply is Square 5 because that’s where the game ends.

\nWe’ve worked out all the math on this before. We define another transition matrix, Q, as the same as P but with the row and column associated with the absorbing state removed:

\n\nWe now define {m}_{0}, as the expected number of flips to get from Square 0 to the absorbing state at Square 5. Similar definitions apply to {m}_{1}, {m}_{2}, {m}_{3}, and {m}_{4}. Then

\n(I-Q)\phantom{\rule{\"thinmathspace\"}{0ex}}m=1\nwhere m is the column vector of the m terms and 1 is a five-element column vector of ones.^{1} Solving this yields

so the expected number of flips to get five consecutive heads is 62. Certainly doable but also frustrating—a common *Taskmaster* state of affairs.

You might be looking at the 32 at the end of m and thinking it’s too high to be the expected number of flips needed to get from four consecutive heads to the fifth consecutive head. While it’s true that you could get from Square 4 to Square 5 in one flip, the problem is that it’s equally likely that that flip will take you back to Square 0. So the expected number of flips to get from Square 4 to Square 5 is

\n\left(1\right)\phantom{\rule{\"thinmathspace\"}{0ex}}\frac{1}{2}+(62+1)\phantom{\rule{\"thinmathspace\"}{0ex}}\frac{1}{2}=32\njust as we calculated via the matrix equation.

\n\n

\n

"}, {"title": "Chains and ladders", "url": "https://leancrew.com/all-this/2023/11/chains-and-ladders/", "author": {"name": "Dr. Drang"}, "summary": "Reworking the math in a Numberphile video that I didn't understand.", "date_published": "2023-11-03T22:47:12+00:00", "id": "https://leancrew.com/all-this/2023/11/chains-and-ladders/", "content_html": "\n

- \n
- \n
I’m using bold for the vectors and matrices because that’s the common typographical convention. I didn’t use bold in the previous post because Marcus du Sautoy didn’t, and I wanted to be consistent with him. ↩

\n \n

\n

This week’s Numberphile video features the BBC’s favorite mathematician, Marcus du Sautoy, who explains how the game Snakes and Ladders^{1} is governed by the mathematics of Markov chains. Despite some experience analyzing Markov chains in grad school, I had trouble understanding one part of the video, so I pulled out my old textbook to clear things up.

But before we get into the math, a short digression. One of my favorite episodes of Melvin Bragg’s *In Our Time* radio show was the “Zeno’s Paradoxes” show from back in 2016. I distinctly remember being infuriated by the show because du Sautoy’s fellow guests, a philosopher and a classicist, clearly thought that Zeno’s reasoning on paradoxes like Achilles and the tortoise and the flying arrow was still valuable. Not because it gives us insight into the historical development of thinking on infinite series or state space representation—which it absolutely does—but because it really is puzzling that Achilles passes the tortoise. What made the show so entertaining was listening to du Sautoy’s obvious frustration with what he considered their obtuseness with regard to solved problems and his need to suppress that frustration for the sake of politeness.

OK, onto the math. Du Sautoy starts by setting up a small Snakes and Ladders game with just ten squares—labeled 0 through 9—one ladder, and one snake.

\n\nYou move across the board by rolling a single die and advancing your marker accordingly. The goal is to reach Square 9 with an exact roll. If you roll a number that is more than you need to reach Square 9, you stay where you are until the next roll.

\nHe then goes through the construction of a transition matrix, in which each element is the probability of moving from the square represented by the row to the square represented by the column. It looks like this when he’s done:

\n\nThe row and column of numbers outside the matrix are the squares on the game board. You’ll note that three of the positions are missing:

\n- \n
- Square 4 is missing because you never stay on it. If you land there, you immediately climb up to Square 7 via the ladder. \n
- Similarly, Square 8 is missing because if you land on it, you immediately slide down to Square 2 via the snake. \n
- Square 9 is missing because, du Sautoy says, the game is over when you reach that square. While that’s certainly true—and we’ll see later that entries for row and column 9 aren’t used in the calculations—I still think it’s helpful to include Square 9 in the transition matrix. Like this: \n

I’m calling this matrix $P$ to avoid confusing it with du Sautoy’s transition matrix, which he calls $Q$. We’ll return to this matrix soon.

\nIt’s after construction his transition matrix that du Sautoy does what I don’t understand. He wants to calculate the expected number of turns it will take to reach Square 9 from Square 0. The equation he comes up with is this infinite sum,

\nI+Q+{Q}^{2}+{Q}^{3}+{Q}^{4}+\dots\nwhere $I$ is the identity matrix and the powers represent matrix multiplication. The sum of the top row of the resulting matrix is the expected number of rolls to get to Square 9. I understand everything du Sautoy says as he explains what each of the terms in this series is, but I don’t get why it leads to the expected number of rolls.

\nI even understand the next part of the video, where some clever algebra shows that this infinite series converges to the non-infinite matrix^{2}

So the sum of the elements of the top row of this inverted matrix is the expected number of rolls to get to Square 9.

\nAs it happens, you can get to this final answer by a method I understand. We’ll start by returning to the $P$ matrix I defined earlier,

\n\nAs you can see, the first seven rows and columns are the same as $Q$. The first seven elements of the last column consists of the probabilities of getting to Square 9 from each of the earlier squares. These are the probabilities that were skipped over in the video, and they’re all either zero or $1/6$ because the roll has to be exactly the number of squares away from 9 you are. Note also that the sum of all the terms in each row must be 1 because you have to land somewhere after each row.

\nThe last row is special. It says that once you’re on Square 9, you can’t go anywhere else. In the study of Markov chains, this is known as an *absorbing state*, and the properties of absorbing states are pretty well known. In particular, there’s an established formula for the expected number of rolls to get from Square $i$ to Square 9, the absorbing state.^{3}

We’ll call the expected number of rolls to get from Square $i$ to Square 9 ${m}_{i}$. One thing we can say right away is that

\n{m}_{9}=0\nbecause you don’t need to roll to get to the end when you’re already there. For all the other values of $i$, we use some lateral thinking.

\nFirst, we know that if we’re on Square 1, it takes ${m}_{1}$ rolls to get to the end—that’s by definition. So if we start on Square 0 and go to Square 1 on our first roll, it will take, on average, $1+{m}_{1}$ rolls to get to the end. But of course we may not go from Square 0 to Square 1; we might go to Square 2 or 3 or whatever. Each of these possibilities for the first roll has a probability assigned to it in the transition matrix, $P$. So the expected value of the number of rolls to get from Square 0 to Square 9 is

\n{m}_{0}=(1+{m}_{1})\phantom{\rule{\"0.167em\"}{0ex}}{p}_{01}+(1+{m}_{2})\phantom{\rule{\"0.167em\"}{0ex}}{p}_{02}+(1+{m}_{3})\phantom{\rule{\"0.167em\"}{0ex}}{p}_{03}\n\phantom{\rule{\"1em\"}{0ex}}+\phantom{\rule{\"0.222em\"}{0ex}}(1+{m}_{5})\phantom{\rule{\"0.167em\"}{0ex}}{p}_{05}+(1+{m}_{6})\phantom{\rule{\"0.167em\"}{0ex}}{p}_{06}+(1+{m}_{7})\phantom{\rule{\"0.167em\"}{0ex}}{p}_{07}\nwhere the $p$ values are taken from the top row of the $P$ matrix. It may look like we’ve gone backwards here, introducing all the other ${m}_{i}$ into an equation for ${m}_{0}$. But let’s see what happens when we generalize this to any starting square and make the equation more compact with the summation symbol.

\n{m}_{i}=\sum _{{a}{l}{l}\phantom{\rule{\"0.222em\"}{0ex}}j}(1+{m}_{j})\phantom{\rule{\"0.278em\"}{0ex}}{p}_{ij}\nDoing some algebra, we get

\n{m}_{i}=\sum _{{a}{l}{l}\phantom{\rule{\"0.222em\"}{0ex}}j}{p}_{ij}+\sum _{{a}{l}{l}\phantom{\rule{\"0.222em\"}{0ex}}j}{m}_{j}\phantom{\rule{\"0.278em\"}{0ex}}{p}_{ij}\nNote that the first sum in this equation is the sum of all the elements of $P$ in row $i$, and that is 1 for each row. Combining that with ${m}_{9}=0$, we can simplify this equation to

\n{m}_{i}=1+\sum _{{a}{l}{l}\phantom{\rule{\"0.222em\"}{0ex}}j\ne 9}{m}_{j}\phantom{\rule{\"0.278em\"}{0ex}}{p}_{ij}\nThis works for all values of $i\ne 9$. So what we have here are seven linear equations in seven unknowns, which is hard to solve by hand, but easy to solve with a computer. If we put it in matrix form, it becomes interesting:

\nm=1+Q\phantom{\rule{\"0.167em\"}{0ex}}m\nHere, the $m$ is a column vector of all the ${m}_{i}$ terms, the $1$ is a column vector of ones, and $Q$ is the matrix $P$ without its last row and column—yes, it’s du Sautoy’s transition matrix that ignored Square 9. Since $m=I\phantom{\rule{\"0.167em\"}{0ex}}m$, we can do the following manipulation:

\nm=I\phantom{\rule{\"0.167em\"}{0ex}}m=1+Q\phantom{\rule{\"0.167em\"}{0ex}}m\n(I-Q)\phantom{\rule{\"0.167em\"}{0ex}}m=1\nm={(I-Q)}^{-1}\phantom{\rule{\"0.167em\"}{0ex}}1\nWell, this should look familiar. Because we’re multiplying by a column vector of ones, each element of $m$ is equal to the sum of the corresponding row of the inverted matrix. And therefore the expected number of rolls to get from Square 0 to Square 9 is the sum of the top row of

\n{(I-Q)}^{-1}\nIn solving this problem, you wouldn’t explicitly invert the matrix, because that’s more computationally intensive than solving a set of linear equations. But I wrote it out this way to show that it’s the same result as du Sautoy’s.

\nOne thing that isn’t the same as du Sautoy’s is the numerical answer. I got the following:

\nm=[8.6\phantom{\rule{\"1em\"}{0ex}}8.4\phantom{\rule{\"1em\"}{0ex}}8.4\phantom{\rule{\"1em\"}{0ex}}7.2\phantom{\rule{\"1em\"}{0ex}}7.2\phantom{\rule{\"1em\"}{0ex}}7.2\phantom{\rule{\"1em\"}{0ex}}7.2{]}^{T}\nAs you can see, my answer for ${m}_{0}$ is 8.6, not 10 as du Sautoy says in the video. I checked my expression for $Q$ and it matched his. I went through all the transition probabilities again and agreed with his. Then I looked in the comments and found that several Numberphile followers also got 8.6, which made me feel better about my answer. While it’s possible we all made the same mistake, given that this is a very simple calculation, I think it’s more likely du Sautoy had a typo somewhere in his work. If you see an error here, though, I’d like to know about it.

\n"}], "home_page_url": "https://leancrew.com/all-this/", "version": "https://jsonfeed.org/version/1", "icon": "https://leancrew.com/all-this/resources/snowman-200.png"}