Dates & Times in AppleScripts

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

This whole thread is VERY intriguing but, unfortunately, I am too limited in my AppleScript knowledge to fully grasp all the intricacies of the coercion of dates.

Currently, I am trying to coerce a string from an astrology program (“Io Sprite”) and add either a day, month or year to advance or regress by any of these intervals.

I can get the LocalDate (app class) out as a string but adding or subtracting from it and and putting it back in the InfoCardWindow is eluding me.

Any ideas or help would be greatly appreciated.

~Roc

Give us an example of the string date you want to coerce.