This article is heavily dependent on contributions by Nigel Garvey & Kai Edwards
Scripters often need to identify, predict, or calculate dates in their scripts - for publishing, accounting, labeling files and archives, or finding special dates. This is often just a minor part of what the script actually does, but achieving the result can become a production number in its own right. For instance, AppleScript doesn’t (at the time of writing) provide numerical equivalents for months, so it seems at first that the only way to handle them is to use a look-up list, a repeat loop, and lots of ‘if-thens’. However, it’s usually possible - with a little mathematical forethought and the exploitation of some of the peculiarities of date variables and AppleScript coercion - to get the required results by much shorter means.
These are solutions to a few problems that have cropped up in various AppleScript fora over the past few years. Possibly no one else in the world will have exactly the same requirements, but hopefully the examples explained in this article will give some idea of the sort of thing that can be done and with minor modifications can be adapted to the reader’s task. Most of the scripts here are “plain vanilla” AppleScript and do not rely on Scripting Additions or other tools. Where possible, I have credited the authors of these ideas although they may appear here in slightly modified form.
The Fundamentals of AppleScript’s Dates & Time
The AppleScript’s Standard Additions Scripting Addition adds support for a “date” class to AppleScript and date-like strings of text can be coerced to dates by preceding them with the word “date” or following them with “as date”. In addition, AppleScript will return the current date and time with a simple instruction: “current date” that often needs parentheses around it.
current date --> date "Saturday, April 15, 2006 4:29:04 PM"
In Jaguar and Tiger, a date can be formatted as follows using a record
set {year:y, month:m, day:d} to (current date)
y --> 2006
m --> April
d --> 15
-- note that the month given above is not text unless coerced to text. "April" is a word in the AppleScript lexicon.
-- these results can also be obtained directly, one at a time:
month of (current date) --> April
-- or as a list:
{year, month, day} of (current date) --> {2006, April, 15}
-- and finally, the day of the week can be extracted:
weekday of date "Saturday, April 15, 2006 4:29:04 PM" --> Saturday
It might also occur to the reader to add time to these formats but the result might be a surprise:
time of date "Saturday, April 15, 2006 4:29:04 PM" --> 59344
-- This is the number of seconds since midnight rather than 4:29:04 PM. But ask for "time string":
time string of (current date) --> "4:29:04 PM" -- the time without the date
-- Similarly, we can get the "date string", without the time appended to it.
date string of (current date) --> "Saturday, April 15, 2006"
-- but note that in both cases, the result is [b]text[/b], not a [b]date[/b]
AppleScript is rather flexible about what it will coerce to a date as well, but the results depend on your settings in International System Preferences. All of the numerical forms must follow the International preferences set for the user. The numerical examples below work if you have selected “United States” in the System Preferences/International/Formats pane and have not customized them or chosen a calendar other than “Gregorian”. In the United Kingdom and much of Europe, the day of the month should precede the month if the month is given numerically, again presuming that the reader has not customized the order, and of course, the name of the month must be in the appropriate language. When the name of the month is given, the day-month order is arbitrary but the year must be last and a period is not required but may be used if the month is abbreviated. AppleScript’s Gregorian date range is from midnight of January 1, 1000 to the second before midnight (23:59:59) on December 31, 9999.
date "12/25/04"
date "12-25-04"
date "12 25 04"
date "dec 25 04"
date "25 DEC 2004" -- with or without commas
-- All compile to: date "Saturday, December 25, 2004 12:00:00 AM"
Manipulating Dates and Times
As we saw above, pure vanilla AppleScript returns the month of a date as an AppleScript key word like April. Often we want the month as a number. Fortunately, AppleScript will coerce a month name (not in quotes, but as an AppleScript word) to a number if it is multiplied by 1 (or any other number) in any order:
November * 1 --> 11
1 * June --> 6
-- this also works this way:
tell (date "june 3, 2006") to get its month as integer --> 6
-- or
June as integer --> 6 (without quotes on June)
That basic idea leads to a very compact script for converting a date to a short Finder-sortable form: yyyy.mm.dd (these will ASCII sort in date order).
date_format("July 29, 1937") --> "1937.07.29"
to date_format(old_date) -- Old_date is text, not a date.
set {year:y, month:m, day:d} to date old_date
tell (y * 10000 + m * 100 + d) as string to text 1 thru 4 & "." & text 5 thru 6 & "." & text 7 thru 8
end date_format
-- Note that the delimiter is easily changed to "", space, "-", or "/" if preferred.
Given that the handler here is rather cryptic, it deserves an explanation. The following is from an entry by Kai Edwards in this thread as posted in bbs.applescript.net.
date_format("12 April 2006") -- for example
to date_format(old_date)
-- get the date as an AppleScript date
date old_date
--> This: date "12 April 2006" [ coerce date string to date ]
--> Compiles to: date "Wednesday, April 12, 2006 00:00:00"
-- get the date elements [year, month & day] from the date
set {year:y, month:m, day:d} to result
--> y = 2006, m = April, d = 12
-- We want to shift these numbers so we can add them to get our final form
y * 10000
--> 2006 * 10000
--> 20060000
result + m * 100
--> [m * 100] = [April * 100] = [4 * 100] = 400 [ coerced to number ]
--> 20060000 + 400
--> 20060400 the year followed by the month with leading zero, the day 00.
result + d
--> 20060400 + 12
--> 20060412 the year, the month with leading zero, the day of the month.
-- now coerce the result to string
result as string
--> 20060412 as string
--> "20060412"
-- tell the result to format the output
-- tell avoids having to save the result as a variable and repeat it several times
tell result -- tell "20060412"
text 1 thru 4
--> text 1 thru 4 of "20060412" = "2006"
result & "."
--> "2006" & "."
--> "2006."
result & text 5 thru 6
--> text 5 thru 6 of "20060412" = "04"
--> "2006." & "04"
--> "2006.04"
result & "."
--> "2006.04" & "."
--> "2006.04."
result & text 7 thru 8
--> text 7 thru 8 of "20060412" = "12"
--> "2006.04." & "12"
--> "2006.04.12" [ final result returned by handler ]
end tell
end date_format
It should be noted here that there’s an even shorter script to get the current date in the same short form but it should also be noted that shorter is not faster in this instance because there is a system overhead in “doing” a shell script:
do shell script "date '+%Y.%m.%d'"
--> 2006.04.15 (for example)
Short forms can also be used as the basis for comparisons as in this script by Bruce Phillips:
set vacationDays to {"4/21/06", "5/29/06"}
tell (current date) to get "" & (it's month as integer) & "/" & it's day & "/" & (text -2 thru -1 of (it's year as text))
if vacationDays contains result then "Today"
Nigel Garvey’s “Date Tips”
In the ScriptBuilders.net collection, there is a folder full of scripts by Nigel Garvey called: “DateTips 1.1”. The description reads:
“Solutions to a few real-life problems that have been posed in AppleScript discussion groups in recent years, concerning the prediction or calculation of dates. The solutions themselves may not be useful to anyone else, but they demonstrate the sort of thing that can be done with AppleScript dates. There’s also a handler that returns short dates in the user’s preferred order rather than the scripter’s.”
I have found several of these useful enough to include here as they involve date manipulations that are not at all obvious but they are relatively straight-forward to alter for other end uses. Space does not permit elaborate explanations, but they are well-documented themselves. In several instances, I have added and removed portions of the originals, but the link above will get the reader a pristine set.
-------- First of Next Month or Weekday of First of Next Month --------
Comments by Nigel Garvey
-- If you set the day of any AppleScript date to 32, the date overflows into the
-- following month. You can then set the day again to whichever day you want in
-- that month. This is the insight that got me going in AppleScript, so it's my
-- particular favourite.
on firstOfNextMonth from theDate -- returns a date
copy theDate to d
set d's day to 32
set d's day to 1
d -- return the resulting date
end firstOfNextMonth
firstOfNextMonth from (current date)
set wd1 to weekday of (firstOfNextMonth from (current date)) -- Thursday
-- and for some other day [added by Bell]:
set wd12 to weekday of (twelveNextMonth from (current date)) --> Monday
on twelveNextMonth from aDate
copy aDate to d
set d's day to 32
set d's day to 12
d
end twelveNextMonth
-------- First or Third Wednesday of the Month --------
-- A society held its meetings on the first and third Wednesdays of each month,
-- and the secretary had to send out announcements about them on the Saturdays
-- eleven days before each one. He wanted to know how he could automate the
-- recognition of these Saturdays.
--
-- Logic: If eleven days' time is a Wednesday and its date lies between the 1st
-- and the 7th or between the 15th and the 21st, then today's one of those
-- Saturdays. The following is a perfectly good test for this:
on announceToday1() -- returns true or false
tell (current date) + 11 * days to ¬
return its weekday is Wednesday and its day is in {1, 2, 3, 4, 5, 6, 7, 15, 16, 17, 18, 19, 20, 21}
end announceToday1
-- If, like me, though, you like to go for the most efficient algorithm you can
-- find, you can get a slight improvement by calculating whether or not the day
-- falls within the first seven days of the first or second fortnight of the
-- month. Basically: is the day modulo 14 less than 8? But the 14th, 28th, 29th,
-- 30th, and 31st also pass this test, so they have to be eliminated. It turns out
-- that if you subtract the day from 28, the *result* modulo 14 will lie between 7
-- and 13 in the case of qualifying dates, and between -3 and 6 otherwise....
on announceToday2() -- returns true or false
tell (current date) + 11 * days to ¬
return its weekday is Wednesday and (28 - (its day)) mod 14 > 6
end announceToday2
announceToday1()
-------- Month Arithmetic --------
-- This is a development of my "1st of next month" handler. If you set the day of
-- any date to 32, the date overflows into the following month. If you then set the
-- day of the new date to the original value, you end up with a date that's exactly
-- one calendar month after the original. This technique works for single additions
-- of up to about 19 months (19 * 32 days) before the overflow begins to overshoot
-- the target month. The handler below can add or subtract *any* number of months
-- within the extremes of the AppleScript date range (1st January 1000 00:00:00 to
-- 31st December 9999 23:59:59). If the original day doesn't exist in the target
-- month (eg. 31st February), the handler returns the last day of that month
-- instead. To subtract months, use a negative 'months' parameter.
--
-- With Mac OS 8.6 (and possibly earlier) the result will be a day or two out if
-- newDate is in the year 2401 for any part of the process.
on addMonths onto oldDate by m -- returns a date
copy oldDate to newDate
-- Convert the month-offset parameter to years and months
set {y, m} to {m div 12, m mod 12}
-- If the month offset is negative (-1 to -11), make it positive from a year previously
if m < 0 then set {y, m} to {y - 1, m + 12}
-- Add the year offset into the new date's year
set newDate's year to (newDate's year) + y
-- Add the odd months (at 32 days per month) and set the day
if m is not 0 then tell newDate to set {day, day} to {32 * m, day}
-- If the day's now wrong, it doesn't exist in the target month
-- Subtract the overflow into the following month to return to the last day of the target month
if newDate's day is not oldDate's day then set newDate to newDate - (newDate's day) * days
return newDate
end addMonths
-- Add 4 years (48 calendar months)
addMonths onto (current date) by 48
--> date "Wednesday, May 26, 2010 12:48:47 PM"
-- Subtract 18 months
addMonths onto (current date) by -18
--> date "Friday, November 26, 2004 12:48:47 PM"
-------- Days in a Month --------
I have used this script and the short date format script we saw earlier to create sortable dates for naming a set of daily folders for every day of the month as part of a ToDo filing application. Any simpler approach would have to generate 31 folders for every month or use a more complex method of determining the number of days in the month.
-- This is another application of the date-overflow method of my "1st of next
-- month" handler. Overflow to a date in the following month by setting its day 32.
-- The day of the resulting date is the amount of the overflow. Subtract this from
-- 32 for the length of the current month.
on daysInMonth for theDate -- returns an integer
copy theDate to d
set d's day to 32
32 - (d's day)
end daysInMonth
daysInMonth for (date "Friday, February 22, 2008 12:00:00 AM") --> 29
-- Alternatively, here's a one-liner that uses arithmetic instead of overflow:
on daysInMonth2 for theDate
32 - ((theDate + (32 - (theDate's day)) * days)'s day)
end daysInMonth2
As a final offering from Mr. Garvey’s set of DateTips, this script calculates the date of a given day of the week a number of weeks before or after a given starting date. I used it in a script for calculating the dates and doses of a medication that was to be tapered off over a number of weeks. I’ve given it Nigel’s name for it here, but I used “TGIF” - Thank God It’s Friday.
-------- Date Hence (or Ere Now) of an Instance of a Given Weekday --------
-- This was written at the suggestion of, and in collaboration with, Arthur Knapp.
-- Its purpose is similar to that of 'Next Thursday's date', but it's far more
-- flexible. It returns the date of *any* instance of *any* weekday before or after
-- the input date - eg. the 2nd Saturday after the date. Its parameters are the
-- reference date, the weekday required, and the number of the instance. Positive
-- instance parameters refer to the future; negatives to the past. An instance
-- parameter of 0 returns the given weekday that occurs in the same Sunday-to-
-- Saturday week as the reference date. If you find this handler useful, you're
-- more than welcome to think up a more convenient name for it. ;-)
-- Next Thursday's date (the date of the first Thursday after today)
DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate(current date, Thursday, 1)
-- Last Thursday but one's date (the date of the second Thursday before today)
DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate(current date, Thursday, -2)
-- The Thursday of this week (may be before, after, or on today's date)
DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate(current date, Thursday, 0)
on DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate(d, w, i) -- returns a date
-- Keep an note of whether the instance value *starts* as zero
set instanceIsZero to (i is 0)
-- Increment negative instances to compensate for the following subtraction loop
if i < 0 and d's weekday is not w then set i to i + 1
-- Subtract a day at a time until the required weekday is reached
repeat until d's weekday is w
set d to d - days
-- Increment an original zero instance to 1 if subtracting from Sunday into Saturday
if instanceIsZero and d's weekday is Saturday then set i to 1
end repeat
-- Add (adjusted instance) * weeks to the date just obtained and zero the time
d + i * weeks - (d's time)
end DateOfThisInstanceOfThisWeekdayBeforeOrAfterThisDate
Hope you find these useful. Enjoy.