Chinese New Year and Ramadan
February 18, 2026 at 10:08 AM by Dr. Drang
Yesterday was Chinese New Year and today is the first day of Ramadan. Both of these dates are based on yesterday’s new moon, so I thought it would be fun to write a little script to see how often the dates coincide.
I used Emacs Lisp, mainly because I knew its calendar module had functions for converting between Chinese, Islamic, and Gregorian calendars. You may recall my date-convert script, which I first wrote back in 2008 and then updated a couple of years ago. Running it today, I got this output:
Gregorian: Wednesday, February 18, 2026
ISO: Day 3 of week 8 of 2026
Astro: 2461090
Julian: February 5, 2026
Hebrew: Adar 1, 5786
Islamic: Ramadan 1, 1447
Chinese: Cycle 78, year 43 (Bing-Wu), month 1 (Geng-Yin), day 2 (Gui-Hai)
My goal was to go through a few hundred years and print out (in Gregorian terms) the dates on which the first of Ramadan came one day after Chinese New Year. I’m very rusty in ELisp, so this probably isn’t very good code, but here it is:
#!/opt/homebrew/bin/emacs --script
(require 'calendar)
(require 'cal-islam)
(require 'cal-china)
;; Loop through 300 Islamic years, roughly centered on this year
(setq iy 1300)
(while (< iy 1600)
;; Get Ramadan 1 of the year as an absolute date
(setq a (calendar-islamic-to-absolute (list 9 1 iy)))
;; Get the month and year of this date in the Chinese calendar
(setq cdate (calendar-chinese-from-absolute a))
(setq cmd (cdr (cdr cdate)))
;; Print the Gregorian date if Ramadan 1 is the day after Chinese New Year
(if (equal cmd (list 1 2))
(princ (concat
(calendar-date-string (calendar-gregorian-from-absolute a))
"\n")))
(setq iy (1+ iy)))
The ELisp calendar modules use the idea of an “absolute” date, which is a simple count of days in the Gregorian calendar. Day 1 in this absolute scale corresponds to January 1 in what would have been Year 1 if the Gregorian calendar had existed back then. This is called the proleptic Gregorian calendar. The absolute date is used as a way station when converting between calendars. You’ll see calls to functions with to-absolute and from-absolute in their names in a few places in the code.
As you can see in the output from date-convert, we’re currently in Year 1447 of the Islamic calendar. The while loop increments the iy variable from 1300 to 1600, a 300-year period roughly centered on this year. I get the first day of Ramadan (the ninth month) in each of those years and see if it matches up with the second day of the Chinese year. If so, it prints out the corresponding Gregorian date.
The format for dates in the Chinese calendar has four terms: Cycle, Year, Month, and Day. The cmd variable has just the month and year, which we get from the four-term date by applying the cdr function twice. cdr is one of the first Lisp functions you learn about, and I enjoyed pulling it out of my mental mothballs.
Here’s the script’s output:
Wednesday, February 3, 1897
Monday, February 11, 1929
Friday, January 31, 1930
Tuesday, February 6, 1962
Saturday, January 26, 1963
Wednesday, February 1, 1995
Wednesday, February 18, 2026
Tuesday, February 3, 2060
Saturday, January 22, 2061
Wednesday, January 28, 2093
Wednesday, February 16, 2124
Friday, February 11, 2157
Tuesday, January 31, 2158
These coincidences typically come 30+ years apart, but sometimes they occur in two consecutive years. The yesterday/today coincidence is the fourth time it’s happened in my life. I doubt I’ll be around for the next one; if I am, I’ll have my caretakers write a post about it.
Another Apple icon regression
February 9, 2026 at 10:19 PM by Dr. Drang
Apple’s *OS 26 icons have been getting some well-deserved criticism over the past couple of months. There was Jim Nielsen’s complaint about menu icons in macOS. Then came Nikita Prokopov’s more detailed criticism of those same icons.1 And a lot of fun has been poked at Tahoe’s app icons, reaching a peak in heliograph’s deadpan post on Threads.
My long-overdue icon complaint is about a CarPlay icon introduced in the fall of 2024 along with iOS 18. Apart from when an app is taking over the screen, there are two primary screens in CarPlay: the app icon view, which is sort of like the home screen on an iPhone,
![]()
and the split screen view, which is sort of like the old split view in iPadOS, but with more parts,

You switch between the two views by tapping the button in the lower left corner of the screen. The button with the 3×3 grid of little squircles is clearly a way to get back to the app icon view. Yes, it used to be a 2×4 grid, which actually matched the icon layout on my screen, but it’s still obvious what the button does. The single hollow squircle, on the other hand, just makes no sense. It doesn’t look anything like the split view screen it takes you to.
This wasn’t the case before the fall of 2024. Here’s what that button used to look like:2
![]()
Kind of obvious where this button takes you, isn’t it?
It’s not that I don’t know what the single hollow squircle button does—I’ve been using it for 16 months. The icon could look like Kurt Vonnegut’s drawing of an asshole in Breakfast of Champions and I’d soon work out what the button was for,3 but the purpose of an icon is to communicate, not just be a placeholder. There’s also parallelism to consider. The icon view button looks like the screen it leads to; so should the split screen view button.
It’s probably impossible to tell the upper echelon of Apple that it’s breaking revenue records in spite of its software design, not because of it. I hope the next regime knows better.
-
Brent Simmons figured out how to get rid of these abominations, a service to humanity deserving of a Nobel Prize. ↩
-
I couldn’t find an image of this button in my Photos library, so I stole it from this TidBITS Talk page. ↩
-
Of course, Apple wouldn’t use an asshole icon—that’s Anthropic’s branding. ↩
Pulling values from a graph without an LLM
February 4, 2026 at 9:48 AM by Dr. Drang
The inability of Claude and (especially) ChatGPT to extract data accurately from the chart discussed in the last post gnawed at me. It seemed like the sort of thing an LLM should be able to do pretty well, but Claude’s table of values needed some careful editing before I could use it. And ChatGPT just completely botched the job, even after several attempts to steer it right. I knew I could do a much better job in not much more time than I spent trying to get the LLMs to do it.
As a reminder, here’s the graph:

What I wanted was a CSV file with a column of dates (the x-values) and a column of floating point numbers (the y-values). One row for each of the 29 points.
I’ve done things like this in the past with OmniGraffle and AppleScript, so that’s how I approached the problem.
The first step was to get the x-values, which didn’t involve OmniGraffle or AppleScript. I opened the image in Preview, selected the text labels along the x-axis with TextSniper, and pasted the result into a new BBEdit window. Text Sniper had no trouble reading the rotated text, but it did have the values separated by space characters instead of line feeds. No problem; I just did a find/replace in BBEdit to get this:
2006-10
2007-03
2007-06
2008-01
2008-03
2008-05
2008-09
2008-12
2009-04
2009-06
2009-09
2009-12
2010-02
2010-03
2010-06
2010-11
2011-02
2011-05
2015-06
2017-03
2024-03
2024-07
2024-11
2025-04
2025-05
2025-07
2025-10
2025-11
2026-01
Update 5 Feb 2026 12:11 AM
Joe Rosensteel pointed out on Mastodon that I could have copied the text in Preview instead of TextSniper. While I knew that Preview could select text in an image, I didn’t know it could do it when the text was rotated. As long as the selection setting in the Tools menu is not Rectangular Selection, the pointer will change to the familiar I-beam shape when you move it over text. It looks a little funny because the I-beam is oriented as if it were selecting horizontal text, but the selection works.
I still prefer TextSniper and keep it in my menu bar, ready to invoke with ⇧⌘2, because it OCRs any text—regardless of orientation—within its rectangular selection, but I bought it back before macOS had built-in OCR tools. TextSniper is better, but if I didn’t already own it, I probably wouldn’t buy it today.
These dates are just months, and I figured it would be best for the plot to assume they were near the middle of each month, so I appended “-15,” to the end of each line. That set all the dates to the 15th of the month, and the comma prepared the file for pasting in the y-values:
2006-10-15,
2007-03-15,
2007-06-15,
2008-01-15,
2008-03-15,
2008-05-15,
2008-09-15,
2008-12-15,
2009-04-15,
2009-06-15,
2009-09-15,
2009-12-15,
2010-02-15,
2010-03-15,
2010-06-15,
2010-11-15,
2011-02-15,
2011-05-15,
2015-06-15,
2017-03-15,
2024-03-15,
2024-07-15,
2024-11-15,
2025-04-15,
2025-05-15,
2025-07-15,
2025-10-15,
2025-11-15,
2026-01-15,
Then came the real work. I pasted the image into a new OmniGraffle document with the scale set to “1 pt = 1 pt.” I did this because I knew that OmniGraffle’s AppleScript dictionary returns all coordinates and lengths in points with the origin at the upper left corner of the document. I then drew some thin-lined rectangles to get the y-coordinate of the horizontal axis and calculate the y-scale of the image.
A rectangle with its top edge along the horizontal axis told me that the y-origin of the plot was 581 points down from the top of the document. To get the scale of the plot, I drew another rectangle whose bottom edge was aligned with the negative sign in the “-4” label and whose top edge was aligned with the equivalent part of the “4” label. That rectangle was 512.6 points high, so the scale of the plot is
I then made a new layer called “Markers” and drew a diamond (OmniGraffle has a built-in diamond shape) sized to match the markers in the plot. By duplication and dragging, I put a diamond shape over every marker. Here’s what it looked like with every diamond selected:

If you zoom in, you’ll see that the diamonds are 26.4 points tall, which means that the center of each diamond is 13.2 points below its top edge. We’ll need this value in the AppleScript because when OmniGraffle is asked for the location of an object, it returns the coordinates of the top left corner.
Here’s another useful tidbit I’ve learned when using OmniGraffle’s AppleScript dictionary in the past: when you ask for every shape in a layer, the list of shapes is returned in the top-to-bottom order shown in the left sidebar. You may think this is obvious, but it isn’t. As you add shapes to a layer, each new shape appears in the sidebar above its predecessor. So the order of the items in the every shape list is the opposite of the creation order. The upshot of this is that to get the y-values of the diamonds in left-to-right order (to match their date order), I created them in right-to-left order.1
Update 5 Feb 2026 12:11 PM
I forgot to mention here that the order of shapes in the sidebar is reverse chronological order only by coincidence. It’s actually the stacking order. The reason that usually corresponds to reverse chronological order is that new shapes stack above older shapes. You can, of course, take the shapes out of their original order by using the various Bring and Send commands in OmniGraffle’s Arrange menu.
Now it’s time to write and run the AppleScript that extracts the y-coordinates of the centers of the diamonds and scales them to match the plot. It’s pretty simple:
applescript:
1: set yValues to {}
2: set yOrigin to 581
3: set yOffset to 13.2
4: set yScale to 64.075
5:
6: tell application "OmniGraffle"
7: tell layer "Markers" of canvas 1 of document 1
8: set diamonds to every shape
9: repeat with d in diamonds
10: set pt to (origin of d)
11: set end of yValues to (yOrigin - (item 2 of pt) - yOffset) / yScale
12: end repeat
13: end tell
14: end tell
15:
16: yValues
Line 1 initializes the list of y-values we’ll be building. Lines 2–4 set the values we established above. After the tell commands to establish that we’re focusing on the “Markers” layer, Line 8 creates the list of diamonds. Lines 9–12 loop through that list to get the location (origin) of each diamond and convert it to the value being plotted. The order in which the subtraction is done in Line 11 accounts for the fact that OmniGraffle’s coordinates increase as you go down but the plot’s coordinates increase as you go up.
Line 16 spits out the list of y-values in the Result section of Script Editor:
{1.777610453442, 2.105119526221, 2.417138303752,
-2.823376834196, -2.217843888773, 0.804769157205,
-3.225882114947, -2.315093559097, 1.802782567774,
2.308216918434, 3.517346715278, 3.011460781549,
2.606423024043, 3.211060498316, 4.212510467041,
4.018455838485, 4.315198697655, 4.018455838485,
3.316423039057, 2.512942396277, 1.803414490836,
1.203973154221, -0.507963600878, -0.304047453731,
-0.80360682595, -2.016491975871, -1.812215148741,
-2.521036825178, -2.2390063608}
That’s way more digits than is justified, but the extra digits don’t hurt anything. After turning each comma-space combination into a linefeed, I did a column paste to put the y-values after the dates. Adding a header line turned it into the CSV file I wanted for plotting:
Date,Hawk
2006-10-15,1.777610453442
2007-03-15,2.105119526221
2007-06-15,2.417138303752
2008-01-15,-2.823376834196
2008-03-15,-2.217843888773
2008-05-15,0.804769157205
2008-09-15,-3.225882114947
2008-12-15,-2.315093559097
2009-04-15,1.802782567774
2009-06-15,2.308216918434
2009-09-15,3.517346715278
2009-12-15,3.011460781549
2010-02-15,2.606423024043
2010-03-15,3.211060498316
2010-06-15,4.212510467041
2010-11-15,4.018455838485
2011-02-15,4.315198697655
2011-05-15,4.018455838485
2015-06-15,3.316423039057
2017-03-15,2.512942396277
2024-03-15,1.803414490836
2024-07-15,1.203973154221
2024-11-15,-0.507963600878
2025-04-15,-0.304047453731
2025-05-15,-0.80360682595
2025-07-15,-2.016491975871
2025-10-15,-1.812215148741
2025-11-15,-2.521036825178
2026-01-15,-2.2390063608
I still have the CSV file made by Claude. Let’s compare:
| Date | Claude | AppleScript | Difference |
|---|---|---|---|
| 2006-10-15 | 1.7 | 1.78 | -0.08 |
| 2007-03-15 | 2.0 | 2.11 | -0.11 |
| 2007-06-15 | 2.4 | 2.42 | -0.02 |
| 2008-01-15 | -2.9 | -2.82 | -0.08 |
| 2008-03-15 | -2.2 | -2.22 | 0.02 |
| 2008-05-15 | 0.8 | 0.80 | -0.00 |
| 2008-09-15 | -3.2 | -3.23 | 0.03 |
| 2008-12-15 | -2.5 | -2.32 | -0.18 |
| 2009-04-15 | 1.7 | 1.80 | -0.10 |
| 2009-06-15 | 2.2 | 2.31 | -0.11 |
| 2009-09-15 | 3.5 | 3.52 | -0.02 |
| 2009-12-15 | 3.0 | 3.01 | -0.01 |
| 2010-02-15 | 2.6 | 2.61 | -0.01 |
| 2010-03-15 | 3.1 | 3.21 | -0.11 |
| 2010-06-15 | 4.2 | 4.21 | -0.01 |
| 2010-11-15 | 4.0 | 4.02 | -0.02 |
| 2011-02-15 | 4.4 | 4.32 | 0.08 |
| 2011-05-15 | 4.1 | 4.02 | 0.08 |
| 2015-06-15 | 3.3 | 3.32 | -0.02 |
| 2017-03-15 | 2.5 | 2.51 | -0.01 |
| 2024-03-15 | 1.7 | 1.80 | -0.10 |
| 2024-07-15 | 1.2 | 1.20 | -0.00 |
| 2024-11-15 | -0.5 | -0.51 | 0.01 |
| 2025-04-15 | -0.3 | -0.30 | 0.00 |
| 2025-05-15 | -0.6 | -0.80 | 0.20 |
| 2025-07-15 | -2.1 | -2.02 | -0.08 |
| 2025-10-15 | -2.0 | -1.81 | -0.19 |
| 2025-11-15 | -2.5 | -2.52 | 0.02 |
| 2026-01-15 | -2.3 | -2.24 | -0.06 |
Claude’s values were off by only about 0.2 at worst, but recall that it originally included a spurious date and had the last seven y-values assigned to the wrong date. The “Claude” values shown above are after I figured out those obvious errors and corrected them.
An odd thing about Claude’s errors: they don’t show a consistent bias in either magnitude or direction. Like everyone who programs, I’m used to seeing output with incorrect numbers. (In my first draft of the AppleScript, I multiplied by yScale instead of dividing, which gave pretty wild results.) What I’m not used to is inexplicably wrong numbers—numbers that aren’t wrong in a predictable way.
At the risk of lengthening this post even further, here’s the graph with the AppleScript-derived values:

And here’s the Python code that created it:
python:
1: #!/usr/bin/env python3
2:
3: import pandas as pd
4: from datetime import datetime
5: import matplotlib.pyplot as plt
6: from matplotlib.ticker import MultipleLocator, AutoMinorLocator
7: from matplotlib.dates import DateFormatter, YearLocator, MonthLocator
8:
9: # Import the data
10: df = pd.read_csv('warsh-applescript.csv', parse_dates=[0])
11: x = df.Date
12: y = df.Hawk
13:
14: # Create the plot with a given size in inches
15: fig, ax = plt.subplots(figsize=(8, 5))
16:
17: # Add a line
18: ax.plot(x, y, 'D-', color='#60150a', lw=3, ms=5)
19:
20: # Set the limits
21: plt.xlim(xmin=datetime(2006,1,1), xmax=datetime(2026,12,31))
22: plt.ylim(ymin=-4, ymax=5)
23:
24: # Set the major and minor ticks and add a grid
25: ax.xaxis.set_major_locator(YearLocator(2))
26: ax.xaxis.set_minor_locator(YearLocator(1))
27: ax.xaxis.set_major_formatter(DateFormatter('%Y'))
28: ax.yaxis.set_major_locator(MultipleLocator(1))
29: ax.axhline(y=0, color='k', lw=.5)
30:
31: # Title and axis labels
32: plt.title('Warsh commentary')
33: plt.xlabel('')
34: plt.ylabel('Monetary hawkishness')
35:
36: # Make the border and tick marks 0.5 points wide
37: [ i.set_linewidth(0.5) for i in ax.spines.values() ]
38: ax.tick_params(which='both', width=.5)
39:
40: # Reduce the whitespace around the plot
41: plt.tight_layout()
42:
43: # Save as PNG
44: plt.savefig('20260202-Warsh commentary via AppleScript.png', format='png', dpi=150)
Importing Pandas is overkill just to read a CSV file, but it’s more efficient of my time. The rest of the code is basically just my typical Matplotlib boilerplate with a few tweaks for dealing with the limits and tick spacing.
Did this take longer than having Claude return the CSV file? Of course it did, but it didn’t take much more than half an hour or so, equally divided between the OmniGraffle drawing and the AppleScript coding. And that half-hour was much more satisfying than arguing with a random number generator and then editing its work after losing the argument.
Also, because I’ve now written up the process (which took significantly longer than doing it), I have a method I can use with confidence in the future.
-
There are, of course, many ways to reverse a list, but there’s no
reversecommand in AppleScript. It’s easier to remember to create the shapes in reverse order. It takes no more time to do it that way. ↩
Plotting via Claude
February 2, 2026 at 8:46 PM by Dr. Drang
Paul Krugman included a terrible plot in his Substack post this morning. It’s meant to support his contention that Kevin Warsh, Trump’s soon-to-be nominee for chair of the Federal Reserve, is a political hack. It does, but it’s still terrible.
Krugman’s not to blame—at least not entirely—for how bad the plot is. It was given to him by Neil Dutta, who apparently had Claude assess the monetary hawkishness of Warsh’s statements, assign a number to it, and plot those numbers against the dates on which the statements were made. Here’s the result:

If you look along the horizontal axis, you’ll see immediately why this is an awful plot. The points are uniformly spaced horizontally, even though the spacing of the dates is far from uniform. Apparently, Claude considered entries like “2006-10” as categories rather than dates. Not especially intelligent of Claude or Dutta.
Krugman—who probably did’t want to go to the trouble of replotting the data—alerted his readers to the problem without being mean to Dutta:
If you look carefully at that chart, you’ll see that there’s a gap in the timeline for several years after Warsh was passed over for Fed chair during Trump’s first term.
Had the data been plotted correctly, you wouldn’t have to look carefully:

Because I didn’t want to go to too much trouble in making this plot, I didn’t adjust the labels on the horizontal axis the way I usually do. The labels are aligned with January 1 of each year.
You may also notice that the y-values in my graph aren’t a perfect match with the y-values in Dutta’s original. Using a “sauce for the goose” approach, I told Claude to assess Dutta’s plot and generate a CSV file with all the points. There were some serious errors in the resulting CSV—30 data points instead of 29 and 8 negative points at the right end of the graph instead of 7—but they were fairly easy to fix.1 And since the relative hawkishness of Warsh’s commentary is a pretty soft number, I’m not worried about the individual values being off by a few tenths. The main thing was to get the dates plotted as they should be.
-
I should point out that the fixing was done by me, not Claude. I tried to get Claude to fix its mistakes, but it just added more. I also tried to get ChatGPT to generate a CSV from Dutta’s plot, and it fucked the assignment so thoroughly—and was so smarmy in its apologies—it made me question whether I should ever use it again. ↩