Eight days a week

Longtime readers of this blog—both of you—know I’m a sucker for date and calendar calculations. I suppose it’s because I spend all my professional time calculating with floating point numbers. The beautiful work that can be done with integers only is fascinating to me.

So yesterday when I saw this tweet from Justin Lancy,

A fun @TextExpander experiment by @DougStephenJr uses AppleScript to work with dates. Love to see this idea built upon: vtr.pe/N3GcaA
  — Justin Lancy (@Veritrope) Sat Sep 1 2012 9:30 AM CDT

I knew I’d have to check it out.

The post is by Doug Stephen, and what he wants ultimately is an AppleScript TextExpander snippet that provides the date of “last Saturday,” whatever that phrase happens to mean on the day it’s run, in yyyymmdd form. Along the way, he shows several examples of AppleScript date calculations.

I am, of course, leading up to my own rewrites of Doug Stephen’s scripts. I realize that by doing this I’m implying that I know better than he does, and that isn’t the impression I want to leave. Most programming problems can be solved in several different ways, and the way any particular programmer chooses will be based on his or her experience, familiarity, and taste. In some cases, one choice will be objectively better than another because it uses a far more efficient algorithm, but that isn’t the case here. I just want to show different ways of solving the problem because I think they’re interesting.

Here’s the final AppleScript from Doug’s post:

 1:  set lastSaturday to current date
 2:  if lastSaturday's weekday is Saturday then set lastSaturday to lastSaturday - days
 4:  repeat until lastSaturday's weekday is Saturday
 5:    set lastSaturday to lastSaturday - days
 6:  end repeat
 8:  set {year:lastSaturdaysYear, month:lastSaturdaysMonth, day:lastSaturdaysDay} to lastSaturday
10:  return lastSaturdaysYear * 10000 + lastSaturdaysMonth * 100 + lastSaturdaysDay

We’ll leave the formatting code in the last two lines alone for right now and look at the part that figures out the date of “last Saturday.”1 It’s basically a searching algorithm that starts with yesterday and steps backward one day at a time until it hits a Saturday. As long as you recognize that the variable lastSaturday isn’t intended to contain the date of last Saturday until the end of the repeat loop, it’s pretty easy to understand.

My approach to this sort of problem would be to calculate how many days ago “last Saturday” was and jump directly there. This is what Dershowitz and Reingold do in Calendrical Calculations, the book that’s had the biggest influence on my date and calendar programming style. And as it turns out, calculating the number of days back to last Saturday is trivial in AppleScript.

Weekdays in AppleScript are constants, Sunday through Saturday. Each of these constants has an integer representation.

Weekday Integer
Sunday 1
Monday 2
Tuesday 3
Wednesday 4
Thursday 5
Friday 6
Saturday 7

So if you run get Wednesday as integer in the AppleScript Editor, you’ll get an answer of 4.

Weekday constant in AppleScript

If you look at the table above closely, you’ll see that the integer associated with each weekday happens to be how many days back you have to go to get to “last Saturday.” So a simple way to jump right to last Saturday is

1:  set today to current date
2:  set twd to (weekday of today) as integer
4:  set lastSaturday to today - twd * days
6:  return (lastSaturday's year) * 10000 + (lastSaturday's month) * 100 + (lastSaturday's day)

where I’m using the same days trick Doug used (and explained) in his post to move a date forward or backward.

Note also that I’ve removed one step from Doug’s method of outputting the date in yyyymmdd format. I didn’t see the need to create the property list he did; AppleScript already has a pretty succinct way to get at the properties of a date.

I confess that my script is a bit of a cheat. It’s made much shorter than it would otherwise be because we want the date of last Saturday. If we needed the date of “last Monday” or “last Friday,” we couldn’t just use the integer of today’s weekday constant as the number of days to move back; we’d actually have to do a little modulo arithmetic.

Luckily for us, AppleScript has the mod operator, which is exactly what we need. Here’s an AppleScript function that returns the date of the last weekday for any given weekday:

 1:  on lastWeekday(wd)
 2:    set today to current date
 3:    set twd to weekday of today
 4:    if twd is wd then
 5:      set d to 7
 6:    else
 7:      set d to ((twd as integer) - (wd as integer) + 7) mod 7
 8:    end if
 9:    return today - (d * days)
10:  end lastWeekday

The generality to any weekday is provided by Lines 4-8. If today happens to be the same weekday we’re looking for, then the last one was seven days ago—that’s what Line 5 gives us. Otherwise, we have to do some calculating.

Weekdays cycle around with a seven-day period, which makes them perfect for modulo, or remainder, arithmetic.

Weekday cycle

The formula in Line 7 gives us the number of days we need to move counterclockwise on the weekday cycle no matter where the target and today are on the cycle. The addition of 7 to the difference between the weekday values ensures that we have a positive number before doing the mod operation.2

The lastWeekday function would be used in an AppleScript like this:

set lastWednesday to lastWeekday(Wednesday)

A similar function that looks ahead to the next weekday would look like this:

 1:  on nextWeekday(wd)
 2:    set today to current date
 3:    set twd to weekday of today
 4:    if twd is wd then
 5:      set d to 7
 6:    else
 7:      set d to (7 + wd - twd) mod 7
 8:    end if
 9:    return today + (d * days)
10:  end nextWeekday

It works the same as lastWeekday except it moves clockwise on the cycle rather than counterclockwise.

Doug’s post includes functions that do the same things. As you might expect from his Saturday-finding script, they search one day at a time instead calculating how far to jump via mod.

Although, as we’ve seen, these date calculations can be done in AppleScript, if I were starting from scratch, I’d almost certainly do them in Python using the datetime library. Here’s a quick example:

 1:  #!/usr/bin/python
 3:  from datetime import date, timedelta
 4:  import sys
 6:  def lastWeekday(wd):
 7:    today = date.today()
 8:    twd = today.weekday()
 9:    if twd == wd:
10:      d = 7
11:    else:
12:      d = (twd - wd + 7) % 7
13:    return today - timedelta(days=d)
15:  sys.stdout.write(lastWeekday(5).strftime('%Y%m%d'))

As you can see, the logic is basically the same as in the AppleScripts. The only significant difference is that Python’s datetime library numbers the weekdays from 0 to 6 instead of from 1 to 7, and it starts on Monday instead of Sunday. Therefore, we need to give 5 as the argument to lastWeekday to get the last Saturday.

Python also has a much richer set of date formatting options through the strftime method. Getting the output in yyyymmdd is simpler than it is in AppleScript.

I wouldn’t be surprised to learn that there are simpler ways to jump directly to the desired date, even in the general case. If you know of one, I’d like to hear about it.

  1. The reason Doug’s interested in getting the date of last Saturday is explained in his post. It doesn’t matter to us here. 

  2. Languages differ in how they handle remainder calculations when one of the numbers is negative, but they all work the same way when both numbers are positive. By adding the 7, I don’t have to remember which way AppleScript works.