Mild-Mannered Canadian Fury

Doug Stephen is Politely Peeved

Performing "Fixed Point" Date Arithmetic in TextExpander using AppleScript


Fri, 31 Aug 2012 Ā«permalinkĀ»

I’ve mentioned before that I’m a fan of the idea behind TextExpander. I have it on my MacBook Pro, but only use it here and there for little things. But between Merlin on Back to Work and Brett Terpstra sharing their TextExpander tricks, I’m beginning to realize that I could automate a little bit more of my day-to-day and make myself a lot happier.

So I’ve been digging in the past few days, and I discovered that something I needed wasn’t inherently built-in to TextExpander: the ability to perform what I am now going to call “Fixed Point” Date Arithmetic. TextExpander will allow you to perform basic date and time math; spit out the date of something two weeks from now, or three days from now, etc. But I was looking for something that would tell me what the date relative to a “fixed point” would be: what was the date last Saturday? What will the date be next Friday? Why would I want this? I wanted to start my “life hacking” with a little thing that’s been driving me crazy for a long time now: TPS repots Progress Reports.

Why?

Because the lab tends to favor self-starting workers instead of massive managerial oversight, they also implement a few accountability measures: Every morning at 9 AM we have an extremely short meeting where everyone says what they did yesterday and what they plan to accomplish today. And every week, we submit a progress report in spreadsheet format to a group mailing list. For whatever reason, our Progress Reports consider Saturday to be the start of the work week, so our reports run from Saturday to Friday. The real annoyance, at least for me anyway, comes from the file naming scheme. The scheme itself is actually quite clever; all progress reports have to start with the Saturday that they start on, but in the format “YYYYMMDD”. So for example, the date today would be “20120831”. The reason we pick this date format is because of one unique property: If the portion of the name following the date is consistent (aka if I name all of my progress reports from now until I die “date_dstephen_progress”) then performing a “Sort by Name” is equivalent to sorting chronologically. This is handy for batch processing and greatly simplifies writing small scripts and tools for file handling; we use this file naming convention outside of our progress reports as well so things like our Continuous Integration server can do very simple batch processing.

In spite of the pros of this date format, it’s not very human readable if you’re not used to it. Additionally, calendars typically aren’t set up to start the week on a Saturday, and most of us aren’t conditioned to look for those Saturdays. My point is that even though finding a Saturday on a calendar is trivial and writing that date format out is trivial, it’s actually a little annoying and tedious to have to do something that your brain isn’t used to. So I decided to hack it.

As it turns out, even though TextExpander doesn’t have this sort of date arithmetic built in, it does have the ability to evaluate various different scripting languages. Amongst those is the ability to write snippets that are AppleScripts. I chose to implement this as AppleScript because, for all of the derision the tool sometimes gets, I find its syntax to be very nice for extremely small script jobs and it also happens to have an awesome built-in Date/Time class. It makes manipulating dates arithmetically very easy by allowing for the coercion of date objects in to records and by performing all arithmetic by converting to seconds and exposing a set of constants to abstract away this information.

AppleScript Date/Time basics

Getting the current date object

This is the super easy part. AppleScript exposes a simple current date command that allows you to capture the current date in an object. The simplest way to perform this sort of date math is to capture the current date and then roll it back or push it forward until you arrive at the date you want to find.

Get Current Date
1
set currentDate to current date

Performing arithmetic on a date object

AppleScript has several built-in date constants to allow for the manipulation of dates. You can add and subtract days, months, weeks, etc. from a given date object. Under the hood, this is simply adding and removing seconds from a UNIX timestamp. But this is all abstracted away by the constants minutes, hours, days, and weeks. So, for example, let’s say we wanted to shift a date object forward in to next week, and then get the date of “tomorrow” during this next week:

Basic Date Arithmetic
1
2
set currentDate to current date
set currentDate to currentDate + weeks + days

The plurality of the constants may sound weird, but it might make sense when you want to do something multiple weeks or days in advance. If we wanted to go two weeks and two days in to the future, we could:

Basic Date Arithmetic, Part 2
1
2
set currentDate to current date
set currentDate to currentDate + (2*weeks) + (2*days)

You still have to make the multiplication explicit, you can’t make statements like 2 days unfortunately. But it’ll do for what we want.

Fixed Point Date Arithmetic: Find a Specific Day

We now have all the tools we need to get where we’re going. This logic and the script are incredibly small and simple. I’ll first start off by demonstrating the general case, a parameterized function that lets us find the date of the next weekday as given by the function argument. Function declaration in AppleScript is done using the on keyword:

Declaring a Find Date Of Next Weekday Function
1
2
3
4
5
6
7
8
9
10
11
on getDateOfNextWeekday(nextWeekdayToFind)
  set returnDate to current date

  if returnDate's weekday is nextWeekdayToFind then set returnDate to returnDate + days

  repeat until returnDate's weekday is nextWeekdayToFind
    set returnDate to returnDate + days
  end repeat

  return returnDate
end getDateOfNextWeekday

The first if statement just checks to see if today’s day is the same day as the one we’re looking for, and forces an increment so that the loop will move forward instead of telling us our solution is “today”. If we wanted to find a previous day, we would decrement by days. Because a date object holds on to a weekday property, we can easily loop over dates until we find the weekday we’re looking for. This is why AppleScript is so wonderfully suited to this date arithmetic task.

Performing a lookup of the most recent day of the week matching your search criteria is just as easy, but with subtraction instead of addition:

Declaring a Find Date Of Previous Weekday Function
1
2
3
4
5
6
7
8
9
10
11
on getDateOfNextWeekday(nextWeekdayToFind)
  set returnDate to current date

  if returnDate's weekday is nextWeekdayToFind then set returnDate to returnDate - days

  repeat until returnDate's weekday is nextWeekdayToFind
    set returnDate to returnDate - days
  end repeat

  return returnDate
end getDateOfNextWeekday

This logic works reasonably well, as long as I fill out my progress reports on Friday. If I want to do my progress report on a Monday, which I sometimes do, then obviously I will have to add some extra logic to hop backwards a week; detect when I need to do this, and then move to the Saturday two days prior and subtract a single weeks quantity from the date. Super simple. For now, I’m just filling out my reports every Friday night (which is what we’re “supposed” to be doing anyway), rather than fiddling with week logic. But it wouldn’t be hard to incorporate.

Now I need a sane way to format the date so that it is useful to me; the date object as it stands contains a bunch of information that I don’t need.

Date object coercion to isolate properties

AppleScript allows for “coercion” of objects, that is, converting an object to another type of object. In our case, we will be leveraging this to transform date objects in to a truncated record, which is essentially an unordered set of key-value pairs, much like a dictionary. This allows us to isolate the Year, Month, and Day of the Month from the date object, which itself holds on to the entirety of a UNIX date (day of week, time, etc.). It’s a really strange looking pattern that has shown up all over the internet in my searches, and to be honest I’m still so new to AppleScript I’m not 100% sure how it works. According to the language guide, dates cannot be directly converted to records. And properties can’t have variables in their definitions. But this works, so I won’t complain. My suspicion is that the anonymous record is created and then the requested properties are copied over from the date object on the “right hand side” of the assignment, but I’m not totally sure.

Coercing Date Object To Record
1
2
3
4
5
6
7
set {year:yearVar, month:monthVar, day:dayVar} to (current date)

-- We can now use yearVar, monthVar, and dayVar 
-- as their own entities, each containing the respective
-- information from the current date object

return yearVar * 10 -- At time of writing, would return 20120

You can use this technique to extract any other properties that might be useful to you, like weekday or time. I, personally, use these three properties to construct the “YYYYMMDD” format that I need for my progress reports. Since TextExpander doesn’t use AppleScript files, but rather evaluates a snippet as AppleScript when told to do so, I have to inline all of the logic that I just showed you. At the end of the day, my Progress Report Date snippet (trigger: ;prda)1 looks something like this:

TextExpander Last Saturday Snippet

Simply note that you need to change the Content Type in the drop down at the top of the snippet content, and that’s it.



  1. I prefix all of my snippets with a semi-colon, because a semi-colon with text after it and no whitespace between is so uncommon (even in coding) that it helps me prevent a sort of crossover between normal typing and explicitly invoking TextExpander. A lot of people prefer to use the double-letter in front or the like, but I got the idea of using a rare punctuation mark from Brett Terpstra, who defaults to a ,, to start his snippets.