5ab5traction5 - World Wide and Wonderful

Wedding Thanksgiving-versary

The question

I woke up this morning to a question from an interesting question from my father:

"How often has your mother and I's anniversary landed on Thanksgiving?"

He had already read an article with the answers but had also (rightly) assumed that it would be a simple and fun exercise in APL.

Thanks to some very handy date-time additions to Dyalog 18.0, the code for discovering which years my parents' anniversary fell on Thanksgiving is quite simple. (And I have no doubt it can be made even simpler still, once I share it with the congenial crowd over at the APL Orchard!)

The code

yr ← ⍳2050 ⋄ Years ← yr[⍸ {⍵>1977} yr]
AnnDates ← Years ∘., ⊂(11 26)
DayOfWeek ← 'DDDD' (1200⌶) (1 ⎕DT AnnDates)
AnnDates[⍸ {⍵≡'THURSDAY'} ¨ DayOfWeek]

An explanation

First we set up our year range:

yr ← ⍳2050 ⋄ Years ← yr[⍸ {⍵ > 1977} yr]

We create a temporary variable yr that holds all numbers from 1 to 2050 and feed that through a filtering function ({⍵ > 1977}) that returns a boolean list of the same length as yr. This is then passed as the argument to the wonderful function that returns the indices corresponding to a boolean value of 1.

While this is certainly more awkward than many modern range functions (for instance, 1977 ^.. 2050 in Raku), it is nevertheless easy to write and falls naturally out of an APL way of thinking.1

AnnDates ← Years ∘., ⊂(11 26)

APL's generic cross-product is pretty amazing in my opinion. Here we are creating a list of dates where each cell is an array of three elements. We must first create a cell out of 11 26 by passing it to enclose () -- otherwise the cross-product would be applied to both 11 and 26 instead of the pair of them.2

DayOfWeek ← 'DDDD' (1200⌶) (1 ⎕DT AnnDates)

This is the line that shows off the new ⎕DT and 1200⌶ functions from Dyalog 18.0. (1 ⎕DT AnnDates) converts all of our freshly generated dates into Dyalog's internal format (specified by 1; there are many other formats available). See the release notes for 18.0 for further information.

Now that it is in a representation that the interpreter understands as a date time (rather than a cell consisting of three numbers, as recognizable as it is to us that such a cell represents a date), it is possible to pass it to 1200⌶, an "I-beam" function that can be used to specify formatting of dates in an extremely flexible manner.

Thus, DayOfWeek becomes an array of the same length as AnnDates but which contains the actual day of the week on which the anniversary lands. Since it is impossible for a Thursday to land after November 26th, any anniversary that landed on a Thursday necessarily also landed on Thanksgiving.

As such, we only need to repeat our indexing magic from the first line with a different filtering function:

AnnDates[⍸ {⍵≡'THURSDAY'} ¨ DayOfWeek]

Two minor differences exist in contrast to the first line of the program:

  1. We are indexing into one array (AnnDates) using the results of the filtering function applied to a second array (DayOfWeek) instead of involving only one array (the temporary yr from the first line).

  2. The use of the each function (¨) to pass the cells of DayOfWeek to the filtering function one at a time. If I understand correctly, the each function is often considered a code smell in APL because it results in a traditional looping construct internally. Such looping constructs are more costly relative to the kind of vectorized function application that is possible in APL -- an essential dynamic in how the language remains competitive in performance today.

Regarding point number 2, I'm fairly certain that I'll be able to update here with an "each-free" version shortly after sharing this with experienced APL programmers.

The answer

If we want to know just the anniversaries that land on Thanksgiving, our answer is already available from the expression ⍸ {⍵≡'THURSDAY'} ¨DayOfWeek:

4 10 15 21 32 38 43 49 60 66 71

Or we have our human-readable date cells via AnnDates[⍸ {⍵≡'THURSDAY'} ¨DayOfWeek]:

1981 11 26  1987 11 26  1992 11 26  1998 11 26  2009 11 26  2015 11 26  2020 11 26  2026 11 26  2037 11 26  2043 11 26  2048 11 26

But the question was actually how many had happened up to and including 2020 (I'm hopeful for my parents to be able to make it further than this year, so I optimistically set the end range to 2050).

We can find the answer with the following expression:

+/ {⍵[1] ≤ 2020} ¨ (AnnDates[⍸ {⍵≡'THURSDAY'} ¨ DayOfWeek])

This expression uses the reduction (/) operator in combination with the addition function (+) to add up the elements of the right hand expression into a single value.3

Since boolean true is simply 1, when we add up each element that is true for {⍵[1] ≤ 2020}, we get 7.

So, Dad, your answer is that this year is a 1 out of 7 experience spread across your 43 years (and counting!) of marriage.

We can create a decent display of each year and the corresponding anniversary, saving some mental math along the way (and once again extending optimistically into the future:

OnThursday   {⍵≡'THURSDAY'} ¨ DayOfWeek
 AnnDates[OnThursday] ,¨ OnThursday

The table function () is used here to give a vertical list and is purely for display purposes in this specific context, though it is not primarily a display-oriented function.4

The join function (,) in combination with the each function (¨) "zips" the two arrays together to create a new array of the same length, resulting in the following:

1981 11 26 4  
 1987 11 26 10 
 1992 11 26 15 
 1998 11 26 21 
 2009 11 26 32 
 2015 11 26 38 
 2020 11 26 43 
 2026 11 26 49 
 2037 11 26 60 
 2043 11 26 66 
 2048 11 26 71

Keep up the exercise and the youthful outlook, my parents, and let's see how far down the list we can make it!

Coda: Regarding "Thanksgiving"

Recognition and anger towards the genocide of Native Americans was a formative experience for me in the first decade of my life and continues to shape me every day. I felt it necessary to mention this because writing about Thanksgiving without mentioning the underlying complexity surrounding the holiday would feel disingenuous to an extreme.

However, it was a fun question posed by my father about his wedding anniversaries and it was a fun experience to code a solution in APL. I can't let complexity alone override my desire to share my voice any longer.

For anyone needing a small dose of catharsis on "Thanksgiving", Wednesday from the Addam's Family has you covered.

Anyone who doubts the validity and impact of the term genocide in this context would do well to read books such as 1491, Black Elk Speaks, or Neither Wolf Nor Dog, to scratch only a few items off of an ever-growing mountain of important works on the subject.

  1. The Dyalog distribution includes a dfns workspace with many useful functions provided by both first and third parties over the years, one of which is iotag, a generalized ioata () with many interesting capabilities).

  2. A different choice from ∘., would be to use the idiom, which zips two arrays together. If the arrays are of unequal length and the right array is of length 1, then that array is appended to each cell of the longer array that was passed as the left argument. This idiom is used in a later example so I chose to keep the dot product version here.

  3. ⍵[1] refers to the first element of array argument . APL has the ability to switch between index origins but by default the language uses an index origin of 1. To be perfectly frank, I don't mind this at all and actually find it refreshing in many cases.

  4. The experienced APL user no doubt notices that I have not bothered to make a distinction between monadic and dyadic function definitions during my explanations. This was done in order to lessen the complexity level of this piece a bit, rather than out of ignorance.


- 3 toasts