Dates & Times in AppleScripts

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.

More fun with Dates & Times in AppleScript.

Here’s a few more tricks with dates that I find useful and thought would be good additions to this thread. Pretty much everything here has been uncovered by trial and error. I own a number of books on AppleScript, but have never come across anything that covers these topics. If someone has somewhere else, I don’t mean to discredit them, but to the best of my knowledge I worked this all out on my own. Though I’m sure pretty much anyone else here could have done the same if they wanted to…

UPDATE: Post edited slightly to include some of Nigel Garvey’s excellent feedback & information below. Also, as he mentioned, be aware that these scripts will all compile if you have similar (US-format) Time & Date localization settings. The hard-coded dates I’ve used will be misinterpreted and/or fail if a non-US date format is your preference. But of course, all dates and scripts can be easily modified or adapted to any date & time format.

[b]

  1. Simple method for including a hardcoded string in your code.[/b]

We all know that if you compile the following:


date "1/1/2000"  --> Returns:  date "Saturday, January 1, 2000 12:00:00 AM"
-- But, somewhat annoyingly, your code also changes to that as well.

but if you instead compile:


date ("1/1/2000" as string) --> You get the same result, but you code doesn't change.

This may not be a groundbreaking revelation, but I prefer to use that notation because I dislike it when AppleScript automatically reformats the dates in my scripts.

[b]

  1. Coercing times to numbers[/b]

As a reminder–since this is commonly documented–you can extract the time from any AppleScript date:


time of (current date)

That will return the number of seconds elapsed since 12:00:00 AM that morning.


time of date ("8/14/2008 1:23:45 PM" as string) --> Returns: 48225 (seconds)

And 48,225 seconds is the amount of time in 13 hours 23 minutes and 45 seconds.

Unfortunately, there’s no simple way to directly coerce the date portion of an AppleScript date into a number.
You can extract it as a string (date string), but if you try to coerce it to number it fails.


date string of date ("8/14/2008" as string) --> Returns: "Thursday, August 14, 2008"

But, if you try to coerce it like the time property of date…


date string of date ("8/14/2008" as string) as number --> AppleScript Error!

There’s a variety of ways to explain why this would be useful. The easiest way for me to explain comes from my use of FileMaker Pro (which I like to use a lot). It has both date & time fields as well as a timestamp field (which is practically identical to AppleScript’s date value). The key notion being that a date is really just an integer that indicates the number of days elapsed from “1/1/0001”, and time is just an integer indicating the number of seconds elapsed since “12:00:00 AM”. And the timestamp is nothing more than the combination of a date & time value. AppleScript’s date values are basically identical to a FileMaker timestamp in that regard. Because it combines both date & time information. Fortunately in FileMaker it is easy to extract and coerce dates & times into integers and combine them into timestamps. As we see here, AppleScript allows you to easily extract the time value, but doesn’t let you extract the date alone. There are many good reasons for wanting to do this in AppleScript, and there is a way.
We’ll get to it soon…

[b]

  1. Time differences[/b]

As I mentioned, AppleScript seems to store dates in a similar manner to FileMaker’s timestamp: the number of seconds elapsed from a starting date.

This can be demonstrated by running the following script:


date ("1/1/2001" as string) - date ("1/1/2000" as string) --> Returns 31622400

And 31,622,400 seconds is exactly equal to 366 days (2000 was a leap year).

Therefore, by dividing by the number of seconds in a day, you can get the number of days between any two dates.
Here’s a simple handler that accomplishes that:


( date ("1/1/2001" as string) - date ("1/1/2000" as string) ) div days
--> Returns: 366 (days)

[b]

  1. Coercing a date to a number (the brute force approach)[/b]

So we just found it’s possible to determine the difference (in seconds or days) between any two dates. That’s real progress! Unfortunately, there just doesn’t seem to be a direct way to determine the exact numerical value for a given date. At least I can’t find a way. All of the following look like good coercions that should work, but all fail and result in an AppleScript error:


return (current date) as integer --> AppleScript Error!
return (current date) as number --> AppleScript Error!
return (current date) * 1 --> AppleScript Error!

So, how can we determine the exact number assigned to a particular date? Well, my approach is a bit of a kludge, but it works for me. I prefer to assume that AppleScript uses the exact same formula as FileMaker so I’m going to create a handler that makes it the same.

As I mentioned, FileMaker considers a one (1) to be the date “1/1/0001”.

The lowest date that I can type in Script Editor (more on this next) is “1/1/1000”. And FileMaker represents that date by the number 364878.

So given those two facts, we can forge a handler for coercing dates to numbers:


on coerceDateToNum(theDate) 
	set {dDate, dDateNum} to {"1/1/1000", 364878}
	return (theDate - (date dDate)) div days + dDateNum
end coerceDateToNum

return coerceDateToNum(date("8/14/2008" as string)) --> Returns: 733268

I find that pretty useful!

And better still, all the date values agree exactly with FileMaker’s equivalent timestamp values, confirming that both are using the same formula for date calculation–which I believe is the “proleptic Gregorian calendar” for those keeping track.

NOTE: It should be mentioned that the Gregorian calendar took effect on approximately October 15th, 1582. So any date calculated prior to that will not correspond to actual historical dates since most countries were using some interpretation of the Julian calendar before that time. For that reason, historians and astronomers don’t actually use the Gregorian Calendar for tracking dates.

[b]

  1. Date Limits.[/b]

Anyway, the next thing that I want to explore is the range limits for dates. There seems to be conflicting documentation about this. As I we know, Script Editor won’t accept a year larger than 9999 or earlier than 1000. For example, when I compile the following:


date ("1/1/0999" as string) --> Returns: date "Tuesday, January 1, 2999 12:00:00 AM"

But this seems to be only a limitation of the script editor because if you execute the following:


date ("1/1/1000" as string) - 1 * days

Lo and behold, you get the result…

date “Tuesday, December 31, 0999 12:00:00 AM”

So it appears that AppleScript can store dates earlier than 1000. It just doesn’t make it easy for you to do so.

Conversely, if you add a day on the the highest date, look at what you get:


date ("12/31/9999" as string) + 1 days --> Result: date "Saturday, January 1, 0000 12:00:00 AM"

Isn’t that fascinating? I think it’s neat because of the following:


date ("12/31/9999" as string) --> Returns: date "Friday, December 31, 9999 12:00:00 AM"
date ("12/31/9999" as string) + 1 * days --> Returns: date "Saturday, January 1, 0000 12:00:00 AM"
date ("12/31/9999" as string) + 366 * days --> Returns: date "Sunday, December 31, 0000 12:00:00 AM"
date ("12/31/9999" as string) + 367 * days --> Returns: date "Monday, January 1, 0001 12:00:00 AM" 

Notice how the weekdays increment correctly (without weekday discontinuity). The last day of year 9999 is a Friday and the first day of year 0000 is a Saturday, and so forth. Therefore, year “0000” could also represent year “10000” and all the weekedays should be correct for Gregorian time. This could be extended so that years 0001 thru 9999 could represent years 10001 thru 19999 respectively, and on and on… Though, I can’t imagine why anyone would ever need that for normal use, but it does point out an interesting property of the proleptic Gregorian calendar.

[b]

  1. Coercing numbers to dates[/b]

Coercing numbers to dates is just as problematic as the reverse. None of the following works:


return 1 as date --> AppleScript Error!
return date 1 --> AppleScript Error!

But these do work, and yield interesting results:


return date ("0" as string) --> (Returns today's date at 12:00:00 AM)

return date ("1" as string) --> (Returns the 1st date of the current month at 12:00:00 AM)

so if today’s date is in August 2008, you would get the following:


return date ("2" as string) --> Returns: date "Saturday, August 2, 2008 12:00:00 AM"

It fails for any “values” beyond the number of days in the current month. So the following occurs:


return date ("31" as string) --> Returns: date "Sunday, August 31, 2008 12:00:00 AM"
return date ("32" as string) --> AppleScript error!

That might prove useful at some point, but doesn’t help us much with coercing numbers to dates.
Anyway, it’s probably evident at this point that we can create another brute force coercion handler just like we did previously (see #4 above):


on coerceNumToDate(theNum)
	set {dDate, dDateNum} to {"1/1/1000", 364878}
	return (date dDate) + (theNum - dDateNum) * days
end coerceNumToDate 

return coerceNumToDate(1234567) --> Returns date "Friday, February 16, 3381 12:00:00 AM"

This works for all numbers between 1 and 3652059 (1/1/0001 and 12/31/9999)
Anything outside that range (or negative numbers) produces incorrect results.

[b]

  1. Coercing numbers to Time.[/b]

Previously (in #2 above), we saw it was straightforward to coerce a time value from an AppleScript date. It would also be helpful to do the opposite: to coerce a number (of seconds) into a time value. Doing so is fairly straightforward, but since time values don’t exist outside of an AppleScript date, we’ll need to start with a date at 12:00:00 AM. So here’s a good opportunity to use the previous ‘date(“0” as string)’ trick (from #6 above) to get the start of today’s date:


on coerceNumToTime(theNum)
	set x to date ("0" as string)
	set hours of x to theNum div hours
	set minutes of x to theNum mod hours div minutes
	set seconds of x to theNum mod hours mod minutes
	return time string of x
end coerceNumToTime

return coerceNumToTime(12345) --> Returns: "3:25:45 AM"

That works, but notice the math to extract the hours, minutes and seconds. We can take advantage of “seconds overflow” when assigning times that will allow us to write this handler a little more concisely. You see, we can assign a value up to 32767 (or 2^15 - 1) to the seconds property, and AppleScript willl overflow the time into the minutes & hours. That’s about 9 hours worth of seconds, so it’s not enough to allow us to eliminate the hours assignment, but we can eliminate the minutes assignment. So we’ll end up with the following equivalent handler:


on coerceNumToTime(theNum)
	set {x, x's hours , x's seconds} to {date ("0" as string), theNum div hours, theNum mod hours}
	return time string of x
end coerceNumToTime

return coerceNumToTime (12345) --> Returns: "3:25:45 AM"

NOTE: This will work with any integer from 0 to 86399 (since there are 86400 seconds in a day). And will roll over for higher numbers.

[b]

  1. Coercing Timestamps[/b]

Of course, the date coercion handlers (see #4 & #6 above) can be modified to accept and return numbers representing seconds. As I mentioned, FileMaker calls that a “timestamp” (rather than a “date” in AppleScript). I prefer the former terminology, so I’ll name these handlers as such:


on coerceTimeStampToNum(theDate)
	set {dDate, dDateNum} to {"1/1/1000", 364878}
	return (theDate - (date dDate)) + (dDateNum - 1) * days
end coerceTimeStampToNum

return coerceTimeStampToNum (date ("8/15/2008 4:35:15 PM" as string))
--> Returns: 6.3354414915E+10 (which is of course 63,354,414,915 seconds since 1/1/0001 12:00:00 AM)

And the reverse:


on coerceNumToTimestamp(theNum)
	set {dDate, dDateNum} to {"1/1/1000", 364878}
	return (date dDate) + (theNum - (dDateNum - 1) * days)
end coerceNumToTimestamp

return my coerceNumToTimestamp(63354414915) 
--> Returns: date "Friday, August 15, 2008 4:35:15 PM"

[b]

  1. Using old dates[/b]

AppleScript (or is it just the Script Editor?) doesn’t like it if you enter a date with the year less than 1000. It prevents us from directly entering such dates and converts them to higher values. As we’ve already seen, the following happens:


date ("1/2/34" as string) --> Returns: date "Monday, January 2, 2034 12:00:00 AM"
date ("1/2/0034" as string) --> Returns: date "Monday, January 2, 2034 12:00:00 AM"

But we know AppleScript can store and use dates prior to year 1000. So how can we use such “old” dates?

The simplest way is to create a date and then set the year to the proper year. Here’s a handler that does that:


on setOldDate(theMonth, theDay, theYear)
	set t to date ((theMonth as string) & "/" & theDay & "/" & theYear)
	set t's year to theYear
	return t
end setOldDate

return setOldDate(1, 2, 34) --> Returns: date "Monday, January 2, 0034 12:00:00 AM" 

Who knows, that could be helpful if you’re working on some sort of ancient AppleScript?

NOTE: It should be mentioned again that the Gregorian calendar took effect on approximately October 15th, 1582. So any date calculated prior to that will not correspond to actual historical dates since most countries were using some interpretation of the Julian calendar before that time. For that reason, historians and astronomers don’t actually use the Gregorian Calendar for tracking dates.

Anyway, one step better, the following handler accepts a short date string (forward slash delimited) as the parameter:


on setDate(theDateStr)
	set {TID, text item delimiters} to {text item delimiters, "/"}
	set {mm, dd, yy, text item delimiters} to every text item in theDateStr & TID
	set {t, t's year} to {date theDateStr, yy}
	return t
end setDate

my setDate("1/2/34")  --> date "Monday, January 2, 0034 12:00:00 AM"

An even more generalized handler that can deal with dash or space delimiters (as AppleScript can) might be useful as well, so here goes:


on setDate(theDateStr)
	set {R, TID} to {false, text item delimiters}
	repeat with i in {"/", "-", " "}
		set text item delimiters to i
		set {x, text item delimiters} to {every text item in theDateStr, TID}
		if (count of x) ≥ 3 then
			set {R, R's year} to {date theDateStr, item 3 of x}
			exit repeat
		end if
	end repeat
	return R
end setDate

my setDate("1/2/34") --> Returns: date "Monday, January 2, 0034 12:00:00 AM"
my setDate("1-2-34") --> Returns: date "Monday, January 2, 0034 12:00:00 AM"
my setDate("1 2 34") --> Returns: date "Monday, January 2, 0034 12:00:00 AM"

-- And, interestingly, the following even works:

my setDate("1/2/34/5/6/12") --> Returns: date "Monday, January 2, 0034 5:06:12 AM"
my setDate("1-2-34-5-6-12") --> Returns: date "Monday, January 2, 0034 5:06:12 AM"
my setDate("1 2 34 5 6 12") --> Returns: date "Monday, January 2, 0034 5:06:12 AM"

And the nice feature of that handler is that it’s easy to add additional delimiters to check (like a period or colon) just by adding them to the list in line #3, but realize that it only uses one delimiter at a time, not a combination of them.

[b]

  1. Other units: Weeks & Months[/b]

Finally, there are two other units that come in handy when performing business or scheduling calculations. Weeks and Months. Sometimes it’s useful to determine how many weeks or months have elapsed between two particular dates. I’ve also used this in document backup scripts for determining when to trigger weekly & monthly backups.

The first handler returns the number of weeks that have elapsed since date “1/1/0001”:


on getDatesWeekNum(theDate)
	set {dDate, dDateNum} to {"1/1/1000", 364878}
	return ((theDate - (date dDate)) div days + dDateNum) div 7 + 1
end getDatesWeekNum

return getDatesWeekNum ( date ("8/14/2008" as string )) --> Returns: 104753 (weeks)

NOTE: Or it can be written a bit more compactly using the [b]coerceDateToNum/b handler (from #4 above)


on getDatesWeekNum(theDate)
	return (my coerceDateToNum(theDate)) div 7 + 1
end getDatesWeekNum

return getDatesWeekNum ( date ("8/14/2008" as string )) --> Returns: 104753 (weeks)

And finally, to determine how many months have elapsed since date “1/1/0001”:


on getDatesMonthNum(theDate)
	return ((year of theDate) - 1) * 12 + (month of theDate)
end getDatesMonthNum

return getDatesMonthNum ( date ("8/14/2008" as string )) --> Returns: 24092 (months)

Both the [b]getDatesWeekNum/b & [b]getDatesMonthNum/b handlers can come in handy by finding the differences between their results when operating on two different dates (often the current date and some milestone in the past). That can be very useful in determining whether a specific weekly or monthly task has been performed. It can also be used to group data into weeks or months for the purposes of data processing or reporting functions.

So there you have it. That’s my contribution to the Time & Date discussion. It was a lot of fun uncovering and gathering this information together, and thanks definitely go to Adam Bell and the MacScripter site for hosting the wealth of information in this unScripted forum. I know I’ve used it on many occasions. Hopefully this will help someone else with a future project someday.

Don Aehl

All these scripts were written and tested to work on the following platform:

International Format Region: United States (default) “MM/DD/YYYY HH:MM:SS 12HR”
Script Editor: 2.1.1 (81)
AppleScript: 1.10.7
Operating System: Mac OS X (10.4.11)
Model: iMac G5 (PowerPC)

Thanks for this. Adds to the body of tricks with dates.

Hi, daehl.

Thanks for writing up and posting your research. I hope you won’t mind a few observations:

1.

More accurately, date “1/1/2000” compiles on your machine (and on mine) to an AppleScript date object representing midnight on 1st January 2000. This compiled object is presented to your view in Script Editor as date “Saturday, January 1, 2000 12:00:00 AM”, because that’s the way dates are configured in your Date and Time preferences. On my machine, I see date “Saturday 1 January 2000 00:00:00”. But if I sent my compiled script to you, you’d see the date in your own format, not mine, when you looked at it in Script Editor. The point is that, once compiled, the date is usable on anyone’s machine.

Here the date isn’t realised until the script’s run (adding slightly to the running time). It’s interpreted according to the preferences on the running machine, so the above works for you and me, but would error on a machine configured for yyyy/mm/dd short dates. Similarly, if you’d written date (“1/2/2000” as string), your machine would produce date “Sunday, January 2, 2000 12:00:00 AM” when it ran the script, mine would produce date “Tuesday 1 February 2000 00:00:00”. Hard-coded date strings aren’t transportable.

There is, of course, a problem when posting to a forum like this, since you can only post the source code, not the compiled script. However you write a date, it’s bound not to work for someone, unless they edit it for themselves. Before Leopard, it was possible to use this unofficial coercion:

"2000/1/1" as «class isot» as date

. but the “2000/1/1” has to be a string, not Unicode text, so it probably doesn’t work with Leopard.

2.

The time of a date is returned as an integer anyway. No need for the coercion. The date would error on machines outside the USA, since there’s no fourteenth month of the year.

3.

AppleScript has the predefined constants weeks, days, hours, and minutes, whose values are the number of seconds in those periods. (The names are in the plural so that you can use expressions like 2 * weeks for the number of seconds in a fortnight.) So you could write:

return ((date "1/1/1") - (date "1/1/0")) div days
--> Returns: 366 (days)

5.

The official range of dates that AppleScript will accept, irrespective of the editor used, is (in my format) date “Wednesday 1 January 1000 00:00:00” to date “Friday 31 December 9999 23:59:59”. AppleScript can go beyond these limits internally to give itself headroom and floor-room in its date calculations.

9.

AppleScript (it’s not Script Editor) doesn’t mind if years less than 1000 are entered, but it interprets them as shorthand for nearby four-digit years. When I first started AppleScripting in 1997, two digits were taken to mean years beginning with “19” and three digits as years beginning with “1”. Shortly after that, the interpretation depended on the system date at the time of the interpretation. (I’m not sure if this was introduced in time for the end of the century or whether AppleScript could do this already.) Currently, both my Jaguar and Tiger machines interpret year numbers between “91” and “99” in date strings as years between 1991 and 1999, and any other two-figure year numbers as years between 2000 and 2090.

date ("1/1/91" as string)
--> date "Tuesday 1 January 1991 00:00:00"

date ("1/1/90" as string)
--> date "Sunday 1 January 2090 00:00:00"

Thanks, Nigel! Very good points, and well worth mentioning. I see what you mean about hard-coded dates and localization preferences. Both are important considerations that I hadn’t previously needed to reckon with. And the use of days as a constant is something I overlooked and will definitely use from now on. In fact, I’m going to edit my post above to fix that. Thanks!

However, regarding AppleScript’s Time & Date limits. It seems clear that the true limits are:

1/1/0000 12:00:00 AM thru 12/31/9999 11:59:59 PM (using a 12HR US Time format)

I have no idea why, but it seems to be just an arbitrary limitation to prevent years earlier than 1000 being typed into a script. My guess is it’s a negative side-effect of their particular implementation of support for 2-digit year date entry. But the underlying Time & Date data structure fully supports the complete range of 4-digit years. That, along with the inexplicable lack of a numeric coersion for dates is a bit odd, but it’s fun to find the workarounds…

It doesn’t, Nigel

Thanks, Adam. I’m not too upset about that as I’d gone off the idea of using it anyway. The idea was suggested (by Kai, I think) in order to accommodate a few people with non-English systems who complained that they had to edit dates in scripts posted on the BBS before they’d compile. Another idea, which probably still works, was to coerce the date to «class isot» on one’s own machine and then paste the result into into the post with the reverse coercion:

On my machine:

date "Saturday 1 January 2000 00:00:00" as «class isot»
--> «data isot323030302D30312D30315430303A30303A3030»

Then, on the BBS:

But I now feel it’s better to post compiled dates ” even if they don’t suit everyone straight off the page ” than to post inefficient and unofficial circumlocutions that might give beginners the idea it’s the right way to do things.

Interesting technique. While I agree it’s a bit cumbersome to implement an alternative date format to ensure everyone can compile a script here, there is another option now that achieves the same ends, but is possibly a more dependable approach than coercing raw date/time data.
The timestamp handler’s I described (8. Coercing Timestamps) can achieve the same result, and are probably a lot more reusable than raw data (well if you use FileMaker, it’s definitely reusable).

I reworked the handlers a little, so the following script should compile with the correct date on any Mac regardless of that system’s date format:


--return coerceTimeStampToNum(date "Monday, August 18, 2008 1:03:33 PM") --> 6.3354661413E+10

set theDate to coerceNumToTimestamp(6.3354661413E+10)
return theDate

on coerceNumToTimestamp(theNum)
	set {t, t's year, t's month, t's day} to {current date, 1, 1, 1}
	set {t's hours, t's minutes, t's seconds} to {0, 0, 0}
	return t + theNum
end coerceNumToTimestamp

on coerceTimeStampToNum(theDate)
	set {t, t's year, t's month, t's day} to {current date, 1, 1, 1}
	set {t's hours, t's minutes, t's seconds} to {0, 0, 0}
	return theDate - t
end coerceTimeStampToNum

So the [b]coerceTimeStampToNum/b handler could be used on the programmer’s Mac, and then the resulting number encodes the date in seconds. Then the [b]coerceNumToTimestamp/b just needs to be included in order to decode the date/time.

Just another way to accomplish the same goal.

Or these should be OK for the next 90 years or so: :wink:

--return coerceTimeStampToNum(date "Monday, August 18, 2008 1:03:33 PM") --> 6.3354661413E+10

set theDate to coerceNumToTimestamp(6.3354661413E+10)
return theDate

on coerceNumToTimestamp(theNum)
	return (date ("1 1 1" as text)) + (theNum - 6.3113904E+10)
end coerceNumToTimestamp

on coerceTimeStampToNum(theDate)
	return theDate - ((date ("1 1 1" as text)) - 6.3113904E+10)
end coerceTimeStampToNum
  1. No year 0 in the Gregorian calendar.
  2. You ignore official limits at your peril. :slight_smile:

But poking around briefly on my Jaguar machine, it seems you can go back further. I don’t know how far. You just have to read the years as BC rather than AD:

set theDate to coerceNumToTimestamp(-1)
--> date "Sunday 31 December 0001 23:59:59"

set theDate to coerceNumToTimestamp(-44 * weeks + days)
--> date "Tuesday 29 February 0001 00:00:00" (1 BC would have been a leap year in the Gregorian calendar.)

on coerceNumToTimestamp(theNum)
	return (date ("1 1 1" as text)) + (theNum - 6.3113904E+10)
end coerceNumToTimestamp

Well, technically there’s also no year 1 or any years before 1582 in the Gregorian calendar (when it started use). That’s why the Proleptic Gregorian Calendar was invented (otherwise known as international standard ISO 8601), which does have a year zero, and it’s a leap year :wink:

http://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar

But I think you’re right in that there’s no year “0” in AppleScript.
Originally, I thought that the year “wrapped-around” from 9999 to 0000, but now I see that year “1/1/0000” is actually equivalent to “1/1/10000”

So the range of dates that AppleScript can be made to display is more like

1/1/0001 12:00:00 AM BC thru 1/1/0001 12:00:00 AM AD thru 12/31/9999 11:59:59 PM thru 12/31/0000 11:59:59 PM (using a 12HR US Time format)

But, clearly AppleScript (under the hood) is capable of storing dates well beyond those limits. Based on my tests, it allows advancement of up to 9.223372096907E+18 seconds from 1/1/0001 and subtraction of up to -9.223371976802E+18 seconds from it as well. That would yield a data range spanning somewhere around 584 Billion Years!

Of course, AppleScript can only store about 15 significant digits in a number, so that limits the fully accessible date range to around 63 Million Years! (wherein every second can be specifically addressed)

But, in practice the limit is even lower, because AppleScript will only let you set the year property of a date to as low as 0 (1 BC, though it reports that as year 0001) and to a maximum of year 32,767 (or 2^15 - 1).

And finally, it’s possible to demonstrate a failure in AppleScript’s date formula above year 29,940.


set {x, x's day, x's month, x's year} to {current date, 31, 12, 29940}
set {x's hours, x's minutes, x's seconds} to {0, 0, 0}
return x --> date "Tuesday, December 31, 9940 12:00:00 AM"

The above works fine (other than the year is reported as 9,940 instead of 29,940), But, one year higher and it all falls apart:


set {x, x's day, x's month, x's year} to {current date, 1, 1, 29941}
set {x's hours, x's minutes, x's seconds} to {0, 0, 0}
return x --> date "Friday, January 1, 1904 12:00:00 AM"

So as near as I can determine, AppleScript appears to be (at least on my G5 Mac) capable of directly addressing date properties (down to the second) for every date between 1 BC thru 29,940 AD as well as making use of those values in meaningful calculations. Though, as we all know, it can only display the last four digits of any year above 9,999. And it’s then simple to create a handler for coercing such a date into a string with the correct year:


set {x, x's day, x's month, x's year} to {current date, 23, 1, 12345}
set {x's hours, x's minutes, x's seconds} to {4, 56, 12}

return x --> Returns: date "Tuesday, January 23, 2345 4:56:12 AM"

on getDateTimeString(x)
	set {w, m, d, y, t} to {x's weekday, x's month, x's day, x's year, x's time string}
	return (w & ", " & m & " " & d & ", " & y & " " & t) as string
end getDateTimeString

return getDateTimeString(x) --> Returns: "Tuesday, January 23, 12345 4:56:12 AM"

One final way to demonstrate this (mainly for fun) is to calculate the average year length for all years within the acceptible range:


set {y1, y2} to {0, 29940} -- From 1/1/1-BC to 1/1/29940-AD
set {x1, x1's day, x1's month, x1's year} to {current date, 1, 1, y1}
set {x1's hours, x1's minutes, x1's seconds} to {0, 0, 0}
set {x2, x2's day, x2's month, x2's year} to {current date, 1, 1, y2}
set {x2's hours, x2's minutes, x2's seconds} to {0, 0, 0}

return (x2 - x1) / days / (y2 - y1) --> Result: 365.24248496994 days per year

The Gregorian calendar repeats every 400 years, so if you change the range to {0, 400} or {400, 800} etc. it returns exactly 365.2425. Which completely agrees with the average Gregorian year length.

Using dates beyond the range (0-299940) causes that AppleScript calculation to fail.

So, that’s about as crazy as I want to get with all this. It’s making my head hurt now…but it was still fun!

How would I go about getting the difference between 2 time strings (ie “11:45pm” - “8:32am”)?

thanks,
Will

You could do this:

set {t1, t2} to {"11:45pm", "8:32am"}
return (date t1) - (date t2) --> Returns: 54,780 (seconds)

But be careful, since you are not including a specific date, Applescript will assume you are referring to the current date when it coerces the time to a date (timestamp).

For example (here’s what I get today):

return date ("11:45pm" as string) --> Returns: date "Monday, February 9, 2009 11:45:00 PM"

So to be more flexible (and allow time ranges greater than a day) you should probably specify the date in your code as well:

set {t1, t2} to {"2/9/09 11:45pm", "1/30/09 8:32am"}
return (date t1) - (date t2) --> Returns: 918,780 (seconds)
-- or
return ((date t1) - (date t2)) / days --> Returns: 10.634027777778 (days)

Hi there,

I’m searching for a script that will repeat an event in iCal on the first weekday of every month.

I need a reminder on the first weekday (ie Monday to Friday, excluding Saturday and Sunday) of every month to go online to make some payments.

On some months, the first of the month may fall on a Saturday or Sunday - say 1 Jul - in which case I want iCal to move the event to the next weekday which will be Monday, ie 2 or 3 Jul automatically when I set up the event in iCal.

I can do this quite easily in Entourage but somehow it doesn’t sync over well in iCal and subsequently my iPhone. I would like to set up the event inside iCal and get it to repeat the way I want it to through a script.

Appreciate if anyone who knows it could help me out.

Thanks.
Kedeb

Hi, Kedeb. Welcome to MacScripter.

The iCalendar standard does have a recurrence type that would allow what you want, but unfortunately iCal doesn’t observe it.

You could try this idea. Adjust the calendar and summary details in the script to your own requirements, save the script as an applet in a permanent location such as ~/Library/Scripts/Applications/iCal/, and run it immediately. It creates an all-day event in iCal, on the first working day of the following month. The event contains an open file alarm that runs the script again on the day to create a similar event in the month after that. The process is thus self-perpetuating for as long as you need it to be. Minimal testing suggests that, if the computer’s not on when an alarm’s due, the alarm will trigger next time the user logs in ” but I don’t know how much leeway there is with this.

-- Get the 1st day of next month.
set now to (current date)
tell now + (32 - (now's day)) * days to set nextEventDate to it + (1 - (its day)) * days
-- If it's a Saturday or a Sunday, advance to the following Monday.
set w to nextEventDate's weekday
if (w is Saturday) then
	set nextEventDate to nextEventDate + 2 * days
else if (w is Sunday) then
	set nextEventDate to nextEventDate + days
end if

set myPath to POSIX path of (path to me) -- Assumes this script's saved as an applet.

-- Make an alarmed, all-day event in iCal, on the calculated day. (Use your own calendar and summary details.)
tell application "iCal"
	activate
	make new event at end of events of calendar 4 with properties {summary:"On-line payments", start date:nextEventDate, allday event:true}
	make new open file alarm at end of open file alarms of result with properties {trigger interval:0, filepath:myPath}
end tell

By the way, for future reference, requests for script help should be made in the “OS X” forum. :slight_smile:

Thank you so much, Nigel, for this simple yet effective script. It worked nicely and got me my first event in August right where I wanted it.

I’m just not sure how to make it repeat itself perpetually but it shouldn’t be difficult figuring it out following your instructions. The hardest part is over :).

I actually have quite a few events that are similar in nature but on different dates, so this is just great for me!

I’m sorry about where I posted my request for help. Thanks for pointing it out to me. Would it be possible then to move this discussion to the OS X forum? I know in some other forums that it’s possible for the administrator to move posts or threads.

The idea is that when date of the August event actually arrives, the alarm attached to the event automatically runs the script again (more precisely, an invisible helper application that’s part of the iCal software, but which runs independently of iCal itself, reacts to the stored alarm data and runs the script at the required time) to create the September event, and so on. Sorry my explanation wasn’t clear.

Also, the script writes own its location into the alarm so that the alarm can find it next time. For this to work, the script has to be saved as an application in the location where you want to keep it before it’s run for the first time.

Do let me know if there are any problems, or if it’s not quite what you need. :slight_smile:

Thanks again Nigel. I got it now. There are two more things I need to sort with this script:

  1. I had preferred that the event gets populated throughout the calendar rather than just the first month. It worked very well according to the way you designed it. In fact, I quite like the way it automatically creates next month’s event when the alarm goes off (it’s quite cool actually) but I guess I’m used to seeing recurrent events actually appearing in the subsequent months when I create them. I’m thinking that in some other events where I will deploy this script, it is important that I see the event appear in subsequent months in case this recurrent event is a meeting or something like that and I need to be sure I don’t schedule another appointment which clashes with the timing.

  2. I sync-ed this over to my iPhone and as you probably already know, it doesn’t quite work in iPhone, that is, iPhone doesn’t run scripts. So the alarm came and went and that’s about it. While I could get it back to the iPhone every time I sync it, I would prefer that it works in iPhone because I use my iPhone a lot more than iCal.

So what I’m hoping for is a script that will sort of permanently create all these first weekday events inside iCal as normal recurrent events and then when I sync it over to iPhone, I’ll get all these dates populated into iPhone’s calendar as recurrent events. I can then manipulate them as normal recurrent events - like delete a single occurrence or the whole series. So the script is a run-once kind of script.

I’m sorry for this long posting but I am a little disappointed with Apple that such a function in iCal needs such a workaround. I used to use Outlook and Entourage and probably got pampered by these apps.

Hi, Kedeb.

Sorry I’ve been so long getting back. I’m having a busy week and also ran into some hitherto unsuspected iCal peculiarities that needed to be checked out before I posted the second script below.

A “recurring event” is just a single event with a recurrence rule, which iCal interprets “on the fly” to display the subsequent reiterations in the calendar. The iCalendar recurrence rule for the first non-(Saturday-or-Sunday) of every month would be “FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1”. But although iCal observes many recurrence types that can’t be set in its GUI, the BYSETPOS rule part isn’t one of them (at least, up to iCal 2.0.5). So a “recurring event” of the type you want isn’t possible in iCal.

The easiest thing with AppleScript would be to create a separate event for each month ” though of course it’s only sensible to do this for a certain distance ahead. Five years’ worth of reminders would require sixty events:

-- Adjust these properties to your own requirements.
property theSummary : "On-line payments" -- Event legend.
property calendarNameOrNumber : -1 -- The calendar name or index number.
property numberOfMonths : 60 -- Number of months in the period to cover.

set nextEventDate to (current date)
set nextEventDate's time to 0

repeat numberOfMonths times
	-- Get the 1st day of next month.
	tell nextEventDate + (32 - (nextEventDate's day)) * days to set nextEventDate to it + (1 - (its day)) * days
	-- If it's a Saturday or a Sunday, advance to the following Monday.
	set w to nextEventDate's weekday
	if (w is Saturday) then
		set nextEventDate to nextEventDate + 2 * days
	else if (w is Sunday) then
		set nextEventDate to nextEventDate + days
	end if
	-- If it's the New Year bank holiday, adjust accordingly.
	if (nextEventDate's month is January) then
		if (w is Friday) then
			set nextEventDate to nextEventDate + 3 * days
		else
			set nextEventDate to nextEventDate + days
		end if
	end if
	
	-- Make an all-day event in iCal for the calculated day.
	tell application "iCal" to make new event at end of events of calendar calendarNameOrNumber with properties {summary:theSummary, start date:nextEventDate, allday event:true}
end repeat

An alternative would be to create an event that recurred on the first day of every month, then go through the calendar, manually dragging iterations that fell on a Saturday or Sunday to the following Monday. (This can’t easily be done with AppleScript.) Again, it’s only sensible to do it for a certain amount of time ahead, but the “detached events” so created would be relatively few in number. For the five years beginning next month, there’d be 17 detached events (or 20 if you were also avoiding New Year bank holidays) plus the original recurring one. “Detached events” are linked to specific iteration times of a recurring event and are expressed instead of the iterations that would otherwise have appeared at those times. iCal 2.0.5 hides them from AppleScript.

An AppleScriptable variation on this would be to delete (or “exclude”) iterations that fell on the offending days and create completely independent events for the alternative days. The number of independent events would be the same as the number of detached events with the previous method.

The iCal peculiarities I mentioned affect this script:

  1. With both iCal 1.5.5 and iCal 2.0.5, if a recurring event is specified to iterate n times, and n > 1, and the event’s an all-day event, then iCal only expresses (n - 1) iterations. I haven’t done anything about this bug in the script.
  2. iCal returns index references to events. But I find that iCal 2.0.5 cavalierly reindexes events on the fly if it feels like it, rendering event references in variables obsolete. The script therefore sets up a filter-by-UID reference for the recurring event so that it can be identified when the time comes to exclude its unwanted iteration dates.
  3. The events the script creates, although testable with AppleScript, don’t become visible in iCal (2.0.5) until I quit it and reopen it. The script therefore arranges for this to happen.
-- Adjust these properties to your own requirements.
property theSummary : "On-line payments" -- Event legend.
property calendarNameOrNumber : -1 -- The calendar name or index number.
property numberOfMonths : 60 -- Number of months in the period to cover.

-- Get the first day of the month after a given date.
on firstOfNextMonth(now)
	tell now + (32 - (now's day)) * days to return it + (1 - (its day)) * days
end firstOfNextMonth

-- If a 1st-of-month date falls during a weekend or is New Year's Day, get the next working day.
on nudge(thisDate)
	set w to thisDate's weekday
	if (w is Saturday) then
		set thisDate to thisDate + 2 * days
	else if (w is Sunday) then
		set thisDate to thisDate + days
	end if
	if (thisDate's month is January) then
		if (w is Friday) then
			set thisDate to thisDate + 3 * days
		else
			set thisDate to thisDate + days
		end if
	end if
	
	return thisDate
end nudge

set startDate to nudge(firstOfNextMonth(current date))
set startDate's time to 0

-- Create a recurring all-day event that repeats on the first of every month for the set number of months.
-- Get a filter-by-UID reference to it for reliability in iCal 2.0.5.
tell application "iCal"
	set rootEventUID to uid of (make new event at end of events of calendar calendarNameOrNumber with properties {start date:startDate, summary:theSummary, allday event:true})
	set rootEvent to a reference to (first event of calendar calendarNameOrNumber whose uid is rootEventUID)
	set rootEvent's recurrence to "FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1;COUNT=" & numberOfMonths
end tell

-- Trace the recurrence sequence, looking out for weekend and New Year dates.
set recurrenceDate to startDate
set excludedDates to {}
repeat (numberOfMonths - 1) times
	-- If the next recurrence date falls during a weekend or is New Year's Day, append it
	-- to the excluded-date list and create an alternative event for the next working day.
	set recurrenceDate to firstOfNextMonth(recurrenceDate)
	if (recurrenceDate's weekday is in {Saturday, Sunday}) or (recurrenceDate's month is January) then
		set end of excludedDates to recurrenceDate
		set recurrenceDate to nudge(recurrenceDate)
		tell application "iCal" to make new event at end of events of calendar calendarNameOrNumber with properties {start date:recurrenceDate, summary:theSummary, allday event:true}
	end if
end repeat

-- Set the recurring event's 'excluded dates' to the excluded-date list to stop those particular recurrence
-- instances being expressed, then quit and reactivate iCal to make the changes visible in the calendar (in iCal 2.0.5).
tell application "iCal"
	set rootEvent's excluded dates to excludedDates
	quit
end tell
tell application "System Events" to repeat while (application process "iCal" exists)
	delay 0.2
end repeat
tell application "iCal" to activate

Thanks again Nigel. I really appreciate the help you are giving me. I’m at a conference now myself but I just wanted to quickly drop a line to say thank you. It’s going to take a while for me to digest all that. Thanks again.

I am a wannabe scripter, have been for years… I DO like to dabble, though.

Here’s what I want to do: I want to place in a signature on my outgoing emails, a little countdown timer. (such as countdown to my vacation, etc.)

Is applescript even what I need? I found SignatureProfiler which will allow me to place applescripts or even html code into my signatures. Now I’m trying to figure out how to do the timer thing.
Am I in the right place?
Thank you.
Dee Dee