Sparking joy

I watched this Numberphile video a few days ago and learned not only about Harshad numbers, which I’d never heard of before, but also some new things about defining functions in Mathematica.

Harshad numbers1 are defined as integers that are divisible by the sum of their digits. For example,

42=424+2=426=7

so 42 is a Harshad number. On the other hand,

76=767+6=7613

so 76 is not. You can use any base to determine whether a number is Harshad or not, but I’m going to stick to base 10 here.

After watching the video, I started playing around with Harshad numbers in Mathematica. Because Mathematica has both a DigitSum function and a Divisible function, it’s fairly easy to define a Boolean function that determines whether a number is Harshad or not:

myHarshadQ[n_] := Divisible[n, DigitSum[n]]

Mathematica functions that return Boolean values often end with Q (Divisible being an obvious exception), so that’s why I put a Q at the end of myHarshadQ. A couple of things to note:

To get all the Harshad numbers up through 100, we can use the Select and Range functions like this:

Select[Range[100], myHarshadQ]

which returns the list

{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12,
 18, 20, 21, 24, 27, 30, 36, 40, 42, 45, 48,
 50, 54, 60, 63, 70, 72, 80, 81, 84, 90, 100}

You can check this against Sequence A005349 in the On-Line Encyclopedia of Integer Sequences.

There are a couple of ways to count how many Harshad numbers are in a given range. Either

Length[Select[Range[100], myHarshadQ]]

which is a straightforward extension of what we did above, or the less obvious

Count[myHarshadQ[Range[100]], True]

which works by making a list of 100 Boolean values and counting how many are True. The Count function is like Length, but it returns only the number of elements in a list that match a pattern. Both methods return the correct answer of 33, and they take about the same amount of time (as determined through the Timing function) to run.

While Mathematica doesn’t have a built-in function for determining whether a number is Harshad, it does have an external function, HarshadNumberQ, that does. This can be used in conjunction with ResourceFunction. The call

ResourceFunction["HarshadNumberQ"][42]

returns True. Similarly,

Select[Range[100], ResourceFunction["HarshadNumberQ"]]

returns the list of 33 numbers given above.

The resource function is considerably faster than mine. Running

Timing[Count[ResourceFunction["HarshadNumberQ"][Range[10000]], True]]

returns {0.027855, 1538}, while running

Timing[Count[myHarshadQ[Range[10000]], True]]

returns {0.06739, 1538}. The runtime, which is the first item in the list, changes slightly from one trial to the next, but myHarshadQ always takes nearly three times as long.

To figure out why, I downloaded the source notebook for HarshadNumberQ and took a look. Here’s the source code for the function:

SetAttributes[HarshadNumberQ, Listable];
iHarshadNumberQ[n_, b_] := Divisible[n, Total[IntegerDigits[n, Abs[b]]]]
HarshadNumberQ[n_Integer, b_Integer] := 
  With[{res = iHarshadNumberQ[Abs[n], b]}, res /; BooleanQ[res]]
HarshadNumberQ[n_Integer] := HarshadNumberQ[n, 10];
HarshadNumberQ[_, Repeated[_, {0, 1}]] := False

I won’t pretend to understand everything that’s going on here, but I have figured out a few things:

First, HarshadNumberQ is obviously defined to handle any number base, not just base-10. And it can handle a list of bases, too, so if you want to check on how many bases in which a number is Harshad, this is the function you want.

Second, HarshadNumberQ uses an argument pattern I’ve never seen before: n_Integer. This restricts it to accepting only integer inputs. It returns False when given noninteger arguments; it doesn’t just throw an error the way myHarshadQ does.

Third, HarshadNumberQ uses Total[IntegerDigits[n]] instead of DigitSum[n]. This, I believe, is at least part of the reason it runs faster than myHarshadQ. For example,

Timing[Total[IntegerDigits[99999^10]]]

returns {0.000084, 216}, while

Timing[DigitSum[99999^10]]

returns {0.000146, 216}.

Finally, the SetAttributes function is needed because Total would otherwise have trouble handling the output of IntegerDigits when the first argument is a list. Let’s look at some examples:

IntegerDigits[123456]

returns {1, 2, 3, 4, 5, 6}, a simple list that Total knows how to sum to 21. But

IntegerDigits[{12, 345, 6789}]

returns a lists of lists, {{1, 2}, {3, 4, 5}, {6, 7, 8, 9}}, which Total has trouble with. Calling

Total[IntegerDigits[{12, 345, 6789}]]

returns an error, saying that lists of unequal length cannot be added. We can get around this error by giving Total a second argument. Calling it as

Total[IntegerDigits[{12, 345, 6789}], {2}]

tells Total to add the second-level lists, returning {3, 12, 30}, which is just what we want. The problem is that this second argument to leads to an error when the first argument is a scalar instead of a list.

This, as best I can tell, is where the

SetAttributes[HarshadNumberQ, Listable];

line comes in. By telling Mathematica that HarshadNumberQ is Listable, we can define the function as if Total were always being used on scalars, and the system will thread over lists just like we want.

Since I’m just writing myHarshadQ for my own entertainment and not for others to use, I can take some of what I learned above and rewrite it to run faster. Like this:

SetAttributes[myHarshadQ, Listable]; 
myHarshadQ[n_] := Divisible[n, Total[IntegerDigits[n]]]

With this new definition, myHarshadQ runs considerably faster. Calling

Timing[Count[myHarshadQ[Range[10000]], True]]

returns {0.013527, 1538}. This is roughly twice as fast as HarshadNumberQ, probably because the definition doesn’t deal with other bases or handle errors gracefully.

Now I can look at Harshad numbers over a broad range without having to wait long for results. To see how they’re distributed over the first million numbers, I ran

Histogram[Select[Range[999999], HarshadNumberQ], {25000}, ImageSize -> Large]

and got this histogram (with a bin width of 25,000):

Harshad distribution histogram

The stepdown pattern repeats every 100,000 numbers, moving down each time. Let’s stretch out the range to three million and see what happens.

Histogram[Select[Range[2999999], HarshadNumberQ], {25000}, ImageSize -> Large]

Harshad distribution histogram over 3 million

The pattern repeats at the larger scale, too.

If you want to learn some other Harshad facts, you should watch this extra footage at Numberphile. There doesn’t seem to be anything directly practical you can do with Harshad numbers, but you can use them to exercise your mathematical muscles. Or learn new things about the Wolfram Language.


  1. You might think Harshad numbers are named after their discoverer or, in accordance with Stigler’s Law, their popularizer, but no. See either the video or the Wikipedia article for the origin of the name. 


Clinching

Although I complained last year of the Sports app’s deficiencies, I’ve been giving it another try. I do like having the score of a favorite team continually updated in the Dynamic Island, but just this morning I learned of another problem.

With the baseball season winding down, I looked in at the wild card standings. Here’s the American League:

American League Wild Card standings

The legend (just out of sight below the bottom of the screenshot) says that the little green arrows in the left margin mean “Clinched Playoffs” and the red bars mean “Out of Playoff Contention.” Unlike most sports apps, there’s no designation for clinching the division, an important omission.

Worse than the omission, though, is the outright error. The Mariners have definitely clinched a spot in the playoffs. They’re five games ahead of their nearest rival in the AL West, the Astros, and there are only two games left in the season. How did the Sports app screw this up? I suspect it comes down to the omission mentioned above. The Mariners are in the playoffs because they’ve clinched their division; if Sports doesn’t account for divisional clinching, it may not be able to tell that the Mariners are guaranteed a spot in the playoffs.

(And more than just a spot. Because they’ll have at least the second best record among the AL division winners, the Mariners will get a first-round bye.)

We see the same problem in the National League:

National League Wild Card standings

Like the Mariners, the Dodgers are definitely in the playoffs but haven’t been awarded the green arrow. What’s worse about this omission is that the Padres—the team the Dodgers beat out for the NL West—have been marked as a playoff team.

So I’m happy to keep Sports around for its Dynamic Island support, but I still won’t be using it as my main sports app. I’ll continue bouncing between the apps from CBS and ESPN. Neither is great, but they’re both better than Apple’s.

Update 27 Sep 2025 4:16 PM
A few people have suggested that I wasn’t seeing green arrows for the Mariners and Dodgers because I was looking at the Wild Card tabs instead of the Divisional tabs. As it happens, I still have a screenshot of the AL Divisional tab taken at the same time as the two screenshots above. No arrow for the Mariners.

American League Divisional standings

I didn’t take a screenshot of the NL Divisional tab, but I looked at it, and there was no arrow for the Dodgers at the time. Since I took these screenshots, all the tabs have been updated and all the teams that have clinched a playoff spot have arrows. I don’t know what caused the update, but it’s kind of late—the Mariners clinched their division days ago.


A bad response

This morning, as I was scrolling through Apple News, I came upon this article from the Wall Street Journal about how NFL teams are punting less than they used to. It included this terrible graph:

Punts per game - Apple News

Who would make their horizontal axis look like that, with the labels jammed together? I was reading the article on my phone, but image above came from the Mac version of Apple News—the two are the same.

I had a feeling the WSJ wasn’t at fault here and went to look at the same article on its website. Viewing the page on my Mac in a normal-width Safari window, the graph was much better:

Punts per game - website

There are some nits I’d pick with this, but overall it’s a good graph and shows what the writer wants to get across.

The website graph isn’t a static image, it’s built from JavaScript and has a sort of simple interactivity. If you run your pointer across the graph, a little marker will appear on the line and a popup will show the year and average punts per game for that year. Here’s a screenshot I took while the pointer was aligned with 1985:

Punts per game - website interactive

I said above that I took the screenshots with my Safari window at what I’d consider a normal width. Narrowing the window to less than about 1300 pixels wide, the graph suddenly changed its look and matched what I’d seen in Apple News (it also lost its interactivity). So the ugly graph is due to WSJ’s responsive design. I switched to my phone to look at the web page there, and sure enough, I saw the ugly version of the graph again.

As you’ve probably guessed, the graph on the web page looks fine on my iPad when Safari takes up the full screen—it’s wide enough to avoid the switch to ugly mode.

So I was wrong. The awful horizontal axis is the WSJ’s fault, it’s just that they’re only torturing people who read the site on their phones. Or through Apple News.

I feel compelled to mention that a later graph in the article looks fine at narrow widths and in Apple News. It’s this graph showing the changing history of play calling on fourth down:

Fourth down behavior

Because it’s looking at only 35 years of data instead of 85, narrowing the chart doesn’t jam the horizontal tick labels together. This should have shown the chartmakers at WSJ how to fix the other graph: fewer tick labels. I suspect that putting labels every ten years instead of every five (and making the unlabeled minor ticks five years apart) would’ve done the trick.


Framed iPhone screenshots with Python

After getting my new iPhone 17 Pro last Friday, I decided I should update my system for framing iPhone screenshots. This should have meant just downloading new templates from the Apple Design Resources page and changing a filename in the Retrobatch workflow, but I decided to effectively start from scratch.

This wasn’t just for the fun of doing it. My previous system had two problems:

  1. It typically would frame just one screenshot when I invoked the Keyboard Maestro macro with several screenshots selected.
  2. It usually left Retrobatch open even though the last step in the KM macro was to quit Retrobatch.

I suspected both of these problems could be solved—as many Keyboard Maestro problems can—by adding a Pause action here or there. But I always feel kind of dirty doing that; it’s very much a trial-and-error process that leaves me with no useful knowledge I can apply later. The necessary pauses depend on the applications being automated and the speed of the machine the macro is running on. Also, a safe pause (or set of pauses) slows down the macro, and I thought my framing macro was already on the slow side.

My substitute for Retrobatch was Python and the Python Imaging Library (PIL), which is now available through the Pillow project. The script I wrote, called iphone-frame, can be run from the command line on the Mac like this:

iphone-frame IMG_4907.PNG

where IMG_4907.PNG is the name of an iPhone screenshot dragged out of the Photos app into the current working directory. This will turn the raw screenshot on the left into the framed screenshot on the right:

Raw and framed portrait screenshots

I have the deep blue phone, so that’s the template I use to frame my screenshots. It’s in this download from the Design Resources page.

Because I often don’t need the full resolution in these framed screenshots, frame-iphone has an option to cut the resolution in half:

iphone-frame -h IMG_4907.PNG

Full resolution is 1350×2760 pixels (for portrait), and half resolution is 675×1380.

I don’t do landscape screenshots very often, but iphone-frame can handle them with no change in how it’s called.

iphone-frame IMG_4908.PNG

turns the raw screenshot on the top into the framed screenshot below it.

Raw and framed landscape screenshots

iphone-frame can be run with more than one argument. If I’ve dragged out several screenshots, I can frame them all by running something like

iphone-frame IMG*

In a call like this, I don’t have to distinguish between the portrait and landscape screenshots—iphone-frame works that out on its own.

One other thing: if iphone-frame is called with no arguments, it reads the file names from standard input, one file per line. This gets used by a Keyboard Maestro macro that calls iphone-frame, which we’ll get to later.

Here’s the source code:

python:
 1:  #!/usr/bin/env python3
 2:  
 3:  from PIL import Image, ImageDraw
 4:  import os
 5:  import sys
 6:  import getopt
 7:  
 8:  # Parse the command line
 9:  half = False
10:  opts, args = getopt.getopt(sys.argv[1:], 'h')
11:  for o, v in opts:
12:    if o == '-h':
13:      half = True
14:  
15:  # If there are no arguments, get the screenshot file paths from stdin
16:  if not args:
17:    args = sys.stdin.read().splitlines()
18:  
19:  # Open the iPhone portrait mode frame
20:  pframe = Image.open(f'{os.environ['HOME']}/Library/Mobile Documents/com~apple~CloudDocs/personal/iphone-overlays/iPhone 17 Pro - Deep Blue - Portrait.png')
21:  
22:  # Apply the appropriate frame to each screenshot
23:  for a in args:
24:    # Open the original screenshot
25:    shot = Image.open(a)
26:  
27:    # Rotate the frame if the screenshot was in landscape
28:    if shot.size[0] > shot.size[1]:
29:      frame = pframe.rotate(90, expand=True)
30:    else:
31:      frame = pframe
32:  
33:    # Offsets used to center the screenshot within the frame
34:    hoff = (frame.size[0] - shot.size[0])//2
35:    voff = (frame.size[1] - shot.size[1])//2
36:  
37:    # Round the screenshot corners so they fit under the frame
38:    # Use a 1-bit rounded corner mask with corner radius of 100
39:    mask = Image.new('1', shot.size, 0)
40:    draw = ImageDraw.Draw(mask)
41:    draw.rounded_rectangle((0, 0, shot.size[0], shot.size[1]), radius=100, fill=1)
42:    shot.putalpha(mask)
43:  
44:    # Extend the screenshot to be the size of the frame
45:    # Start with transparent image the size of the frame
46:    screen = Image.new('RGBA', frame.size, (255, 255, 255, 0))
47:    # Paste the screenshot into it, centered
48:    screen.paste(shot, (hoff, voff))
49:  
50:    # Put the frame and screenshot together
51:    screen.alpha_composite(frame)
52:  
53:    # Make it half size if that option was given
54:    if half:
55:      screen = screen.resize((screen.size[0]//2, screen.size[1]//2))
56:  
57:    # Save the framed image, overwriting
58:    screen.save(a)

I hope there are enough comments to explain what’s going on, but I do want to mention a few things:

OK, iphone-frame is easy to use if I already have a Terminal window open and the working directory set to where I’ve dragged the screenshots. This is a pretty common situation for me, but it’s also common that I don’t have a Terminal window open, and that’s when a quick GUI solution is best. I get that through this Keyboard Maestro macro, which you can download.

KM Frame iPhone Screenshots

To use this in the Finder, I select the screenshot images I want to frame and press ⌃⌥⌘F. A window appears, asking if I want the framed screenshots to be original size or halved.

KM Frame iPhone Screenshot prompt

In very short order, the images change from raw screenshots to framed screenshots. They might also rearrange themselves if I have my Finder window sorted by Date Modified.

The only comment I have on the Keyboard Maestro macro is that the %FinderSelections% token returns a list of the full path names to each selected file, one file per line. That means it’s in the right form to send to iphone-frame as standard input. I don’t think I can send a token directly to a shell script, which is why I set the variable AllImageFiles to the contents of %FinderSelections%.

It’s only been a few days, but this new system seems to be working well. You’ll probably be seeing framed screenshots from me in the near future as I start to complain about fit and finish (or lack of same) in iOS 26.