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. 

9 Responses to “Eight days a week”

  1. Andrea says:

    Did you notice in the days’ graph both arrows point in the same direction?

  2. Dr. Drang says:

    Jesus, how did I manage to screw that up? Thanks, Andrea, it’s fixed now.

  3. Keith says:

    Excellent work as usual. My problem is the vagueness of “last”. To me, when it’s Sunday and someone asks about “last Saturday”, I think of 8 days ago rather than yesterday.

  4. Ryan Gray says:

    Yes, the “last *day” term is nearly worthless if it would map to “yesterday” or even “the day before yesterday”. I’ll say “last Saturday”, on Monday and every time they will ask “you mean the day before yesterday?” or “you mean this past Saturday?”. I’ll pause for a moment thinking “of course, what else did you think I meant”, and they are clearly thinking “why the hell don’t you just say ‘the day before yesterday’?”. There’s also the same problem going forward if the “next” day is either tomorrow or the day after tomorrow.

  5. Dr. Drang says:

    Keith and Ryan,
    This isn’t a Turing test. Doug Stephen isn’t talking to a person, he’s typing a TextExpander snippet that inserts a date according to a set of rules defined by his company. I called it “last Saturday” because “most recent Saturday before today” was too clumsy to keep typing again and again.

    If you really wanted to avoid confusion with “yesterday” and “day before yesterday,” you could easily change the code to

    on lastWeekday(wd)
      set startDay to (current date) - 2 * days
      set swd to weekday of startDay
      if swd is wd then
        set d to 7
        set d to ((swd as integer) - (wd as integer) + 7) mod 7
      end if
      return startDay - (d * days)
    end lastWeekday

    which would skip over the past two days. But that isn’t what Doug’s script did, so mine didn’t do it either.

  6. Dave C. says:

    I just found that site and sent Stephen an email.

    I recommended this instead:

    # use echo to remove carriage return
    echo -n $(date -v-1d -v-sat "+%Y%m%d")

    I’ve been using the same core code to get future weekdays (i.e. 2 Thursdays from now) for about a year now without issue.

  7. Dr. Drang says:

    Dave C. wins the comment thread. I had no idea you could use the -v switch to specify days of week or months of year. And I didn’t know you could apply several -v switches sequentially. Amazing what you can learn if you actually read the man page instead of just skimming it..

  8. Dexter Ang says:

    It’s a shame Mac’s date command doesn’t have an equivalent to Linux’s -d option.

    With that I can get the date using somewhat natural language. For example, now it’s Sept 25, 2012. To get 2 Tuesdays ago, I can just type:

    date -d”2 weeks ago Tuesday” +%Y%m%d

    and get 20120911. “Last Saturday” would be 20120922. Going forward, remove the “ago” as that seems to be the keyword. So:

    date -d”2 weeks Tuesday” +%Y%m%d

    is 20121009. I’ve used this in a script that archives a set of log files from “1 month ago”. Makes reading the command easier. Too bad it isn’t available on Mac OS X.

  9. Dr. Drang says:

    In general, Dexter, I’m not a big fan of natural language inputs. They almost always introduce ambiguity that forces you to run tests or read the documentation very carefully, both of which are counter to their intent.

    Having said that, I was a big fan of Date::Manip when I used Perl.