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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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)