Transposing dates between time zones

I’ve been tackling this for the final stage of my “next recurrence in iCal” project elsewhere in this forum. Contrary to my fear that it would require an external AppleScript Studio or Objective-C file, it turns out to be doable with just AppleScript and the Unix “date” command. The latter can return a date that’s so many seconds from the beginning of the Unix “era” (1st January 1970 00:00:00 GMT) and also accepts an environment variable “TZ” that governs the time zone in which the date is expressed.

(* Convert an ISO-format date string to an AppleScript date. *)
on isotToDate(isot)
	set theDate to date "Saturday 1 January 1583 00:00:00"
	set n to (text 1 thru 8 of isot) as integer
	set theDate's year to n div 10000 - 1
	set theDate's month to n mod 10000 div 100
	set theDate's day to n mod 100
	if ((count isot) > 8) then
		set n to (text 10 thru 15 of isot) as integer
		set theDate's time to n div 10000 * hours + n mod 10000 div 100 * minutes + n mod 100
	end if
	
	return theDate
end isotToDate

(* Transpose a date/time from GMT to a given time zone. *)
on GMTtoTZ(GMTDate, TZ)
	-- Subtract date "Thursday 1 January 1970 00:00:00" from the GMT date. Result in seconds, as text. 
	copy GMTDate to date19700101
	tell date19700101 to set {year, its month, day, time} to {1970, 1, 1, 0}
	set eraTime to (GMTDate - date19700101)
	if (eraTime > 99999999) then
		set eraTime to (eraTime div 100000000 as text) & text 2 thru 9 of (100000000 + eraTime mod 100000000 as integer as text)
	else if (eraTime < -99999999) then
		set eraTime to (eraTime div 100000000 as text) & text 3 thru 10 of (-100000000 + eraTime mod 100000000 as integer as text)
	else
		set eraTime to eraTime as text
	end if
	
	return isotToDate(do shell script ("TZ='" & TZ & "' /bin/date -r " & eraTime & " +%Y%m%dT%H%M%S"))
end GMTtoTZ

(* Transpose a date/time from a given time zone to GMT. *)
on TZtoGMT(TZDate, timeZoneID)
	-- Initial stab. The difference between the local date when GMT is the given date is usually the same as
	-- the difference between the given date when it's local and the GMT date we actually want .
	set GMTDate to TZDate - (GMTtoTZ(TZDate, timeZoneID) - TZDate)
	-- . but not around the time the clocks go forward. If the GMT obtained doesn't correspond to the given local date,
	-- shift to a nearby local date where the above DOES work, get a new GMT, then unshift it by the same amount.
	set checkDate to GMTtoTZ(GMTDate, timeZoneID)
	if (checkDate is not TZDate) then
		if (GMTDate > checkDate) then -- "Clocks forward" is towards GMT.
			set shift to GMTDate - checkDate
		else -- "Clocks forward" is away from GMT.
			set shift to -days
		end if
		set nearbyDate to TZDate + shift
		set GMTDate to nearbyDate - (GMTtoTZ(nearbyDate, timeZoneID) - nearbyDate) - shift
	end if
	
	return GMTDate
end TZtoGMT

(* Transpose a date/time from one time zone to another and/or correct a date in the missing hour when the clocks go forward. *)
on transposeDate(theDate, timeZone1ID, timeZone2ID)
	if (timeZone1ID is "GMT") then
		set GMTDate to theDate
	else
		set GMTDate to TZtoGMT(theDate, timeZone1ID)
	end if
	if (timeZone2ID is "GMT") then return GMTDate
	return GMTtoTZ(GMTDate, timeZone2ID)
end transposeDate

-- Demo: Transpose from UK time to US Eastern time.
transposeDate(date "Monday 22 June 2009 23:32:28", "Europe/London", "US/Eastern")
--> date "Monday 22 June 2009 18:32:28"

Edits: A couple of comments clarified in the script. Minor bug fix. transposeDate() handler rewritten to avoid superfluous conversions to GMT.
More recently, isotToDate() handler rewritten to cope with the misbegotten date handling in Snow Leopard.
More recently still, GMTtoTZ() handler rewritten, principally to change the method used to convert the large eraTime number to a decimal string for use in the shell script. Thanks to Yvan Koenig for pointing out the weakness in the original.

Thinking about this clever script, I realized that I didn’t know the identities of the time zones. This little script will tell you how the system identifies them. Open System Preferencs/Date & Time and after selecting a place on the world map under the Time Zone tab, run this:

tell (do shell script "ls -al /etc/localtime") to set LocalRegion to word -2 & "/" & word -1

Hi, Adam.

Thanks very much for posting that. As you obviously know, “/etc/localtime” is a symbolic link (the UNIX equivalent of an alias file) to the particular file that holds the data for the time zone to which the computer’s currently set, eg. “/usr/share/zoneinfo/Canada/Atlantic”. The files and subfolders in “/usr/share/zoneinfo/” are so arranged that whatever follows “zoneinfo/” in a file’s POSIX path is the same as the name of the time zone it represents.

I discovered a really fast way of getting this last year:

set localTimeZoneName to text from word -2 to end of (POSIX path of ("/etc/localtime" as POSIX file as file specification))

. but when I asked for opinions about its validity on the AppleScript-Users list, the general view was that the POSIX file coercion was a hack that might break in the future. Mark J. Reed came up with this Perl alternative, which I rather like. It explicitly follows the symbolic link and uses “zoneinfo/” as a delimiter with the result:

set localTimeZoneName to (do shell script "perl -le 'print( readlink(\"/etc/localtime\") =~m{zoneinfo/(.*)} )' ")

With regard to the script at the top of this thread, I’m currently looking for a way to speed it up. I now have a working version of my iCal recurrence predictor script that incorporates this code, but the speed’s noticeably affected by the fact that each date transposition requires at least three ‘do shell script’ calls. (Fortunately, there are no date transpositions during the recurrence tracing itself!) The performance can be statistically improved by not doing superfluous GMT conversions (I’ve now edited that into the script above), but ideally, I’d like to find a way that’s faster anyway before releasing the new version of the recurrence predictor.

This much shorter and faster effort works on systems from Leopard to the present.

on TZtoTZ(TZ1date, TZ1, TZ2)
	return (do shell script ("eraTime=$(TZ=" & TZ1 & " date -jf '%FT%T' '" & (TZ1date as «class isot» as string) & "' '+%s') ; TZ=" & TZ2 & " date -r  \"$eraTime\" '+%FT%T'") as «class isot») as date
end TZtoTZ

TZtoTZ(current date, "Europe/London", "Canada/Pacific")

The use of «class isot» makes it a bit of a hack, but hey. :wink: The ‘as string’ must be ‘as string’ and not ‘as text’. The second ‘as «class isot»’, just before the end, isn’t a coercion but a parameter of ‘do shell script’.

I’m sure there must be an explicit function to convert between time zones, but I’ve never seen one.

Edit: In another thread, McUsrII pointed me to the %F and %T formatting codes, which are equivalent to and shorter than the %Y-%m-%d and %H:%M:%S I had here originally.

Hello.

How you do it, is pretty much how you would do it, at least as a principle on the system level. The time is stored as UTC, and represented, by the time zone, so you convert from one time zone to another, by merely changing the time zone.

I like class isot, I hope it isn’t a hack! :slight_smile:

Yeah. It’s nice. :slight_smile: Less laborious than setting individual properties to construct a date and immune to the various date string bugs which have afflicted AppleScript dates since the introduction of AS 2.0 in Leopard.