Daylight Savings Times

In response to the latter part of this thread, instead of relying on hard-coded values and a limited number of timezones, I’ve created a script that relies on OS X’s built-in timezones record to retrieve the latest daylight savings times.

Edit: parseDumpedDate handler changed courtesy of Nigel Garvey.

property monthAbbrs : "JanFebMarAprMayJunJulAugSepOctNovDec"

on parseDumpedDate(dateString)
	-- One-line version:
	-- tell dateString to return (my date (word 3 & space & ((my (offset of (word 2 of dateString) in monthAbbrs)) + 2) div 3 & space & word -2 & space & text (word 4) thru (word -3)))
	
	set a to date (text (word 4) thru (word -3) of dateString)
	set day of a to word 3 of dateString
	set month of a to ((offset of (word 2 of dateString) in monthAbbrs) + 2) div 3
	set year of a to word -2 of dateString
	
	return a
end parseDumpedDate

(*** on run ***)

set {text returned:chosenYear, button returned:buttonReturned} to display dialog "Daylight Saving Times for Year:" default answer "2007" buttons {"Choose Timezone", "Cancel", "Use System Timezone"} default button "Use System Timezone"

if (buttonReturned is "Choose Timezone") then
	set firstLevel to paragraphs of (do shell script "ls -p /usr/share/zoneinfo | sed -e '/\\//!d' -e 's|/||'")
	set firstChoice to choose from list firstLevel with prompt "Continents:"
	if (firstChoice is false) then error number -128
	
	set secondLevel to paragraphs of (do shell script "ls /usr/share/zoneinfo/" & firstChoice)
	set secondChoice to choose from list secondLevel with prompt "Regions:"
	if (secondChoice is false) then error number -128
	
	set zonePath to "/usr/share/zoneinfo/" & firstChoice & "/" & secondChoice
else
	set zonePath to "/etc/localtime"
	set {firstChoice, secondChoice} to (do shell script "/usr/bin/readlink " & zonePath & " | sed -E -e 's|.*zoneinfo/||' -e 's|/|\\" & return & "|'")'s paragraphs
end if

try
	set DSTs to run script ("{" & text 1 thru -3 of ¬
		(do shell script "/usr/sbin/zdump -v " & zonePath & " | sed -E -e '/" & chosenYear & "/!d' -e 's|^[^ ]+ *|{utcDate:\"|g' -e 's/ = /\", localDate:\"/' -e 's/ isdst=1/\", isDST:true},¬/' -e 's/ isdst=0/\", isDST:false},¬/'") ¬
			& "}")
on error
	display dialog "No Daylight Savings Times found for " & chosenYear & " at " & secondChoice & ", " & firstChoice
	return
end try

set t to chosenYear & " Daylight Savings Times for " & secondChoice & ", " & firstChoice & ":" & return & return

set oldItem to null
repeat with anItem in DSTs
	if (oldItem is not null and oldItem's isdst is not anItem's isdst) then
		if (anItem's isdst) then
			set t to t & "Begins "
		else
			set t to t & "Ends "
		end if
		set date1 to parseDumpedDate(oldItem's localdate)
		tell date1's date string to if it ends with chosenYear then
			set t to t & text 1 thru -6
		else
			set t to t & it
		end if
		set t to t & "," & return & tab & "changing from " & time string of date1 & " to " & time string of parseDumpedDate(anItem's localdate) & return
	end if
	set oldItem to anItem
end repeat

display dialog t buttons {"OK"} default button 1

Old parseDumpedDate handler:

on parseDumpedDate(dateString)
	set a to date (text (word 4) thru (word -2) of dateString)
	set day of a to word 3 of dateString
	set month of a to ((offset of (word 2 of dateString) in monthAbbrs) + 2) div 3
	return a
end parseDumpedDate

nice, very cool.

I like the slight rearrangement: asking for the year first and in the buttons offering a choice of time zone. Much nicer presentation. Thanks for posting it :slight_smile:

Seconded. Unfortunately, on my machine, of the years I’ve tried so far, only 2007 gives the right weekdays. The day numbers and months appear to be about right, though.

UK system time zone
Mac OS 10.4.9

Later:

The problem’s in the parseDumpedDate() handler. The initial setting of a to ‘date (text (word 4) thru (word -2) of dateString)’ doesn’t necessarily produce a date in the right year. (On my UK system, when a string containing only the time followed by the year is used in a date specifier, the year figure’s ignored and the system fills in the details from the current date.) This works for me:

on parseDumpedDate(dateString)
	set a to date (text (word 4) thru (word -3) of dateString)
	set day of a to word 3 of dateString
	set month of a to ((offset of (word 2 of dateString) in monthAbbrs) + 2) div 3
	set year of a to word -2 of dateString
	
	return a
end parseDumpedDate

You’re totally correct. The problem was my assumption that the year wasn’t ignored, and not doing the proper testing. I’ve edited it.

This board’s great for having scripts tested, especially for that one bug that I usually leave in as a ‘secret’ surprise!
As usual, thanks to Nigel Garvey for his rigourous testing! And thanks to the others for the nice comments. :smiley:

Hi Querty,

a great script, thank you :slight_smile:
Unfortunately your new parseDumpedDate handler doesn’t work properly
with all international date formats, but Nigel’s does

Hi Stefan, sorry for the delayed response.
Can you tell me for which languages that it doesn’t work with? I would like to test this, thanks.

In the mean-time, I’ve swapped Nigel’s in, without regard for any optimisation, so at least we know it actually works. :slight_smile:

For all languages like german, whose date format order is day, month, year instead of month, day, year

Yes, sorry, your primary language. :rolleyes: My fault. :stuck_out_tongue:

From what I could find, the problem seemed to be in the spelling of the months. I’ve kept Nigel’s version.
I’ve also changed the display text, so it at least uses localised weekday and month names.

The spelling of the month doesn’t matter, because you get always the same information with zoneinfo
regardless of the language. In your parseDumpedDate handler you assumed the date order m / d / y.
Setting a date just with the date keyword and a string is never independent from international date format settings.
The most robust way is to take a present date and change its values or like Nigel does, set only the time with a string
and change month, day and year “manually”

As discussed here, I’ve written an adaptation of this example in the form of a handler that converts a given date from local time to GMT, accounting for the daylight savings offset, if any, at the time of the given date. Here is the script:

on parseDumpedDate(dateString)
	set a to date (text (word 4) thru (word -3) of dateString)
	set day of a to word 3 of dateString
	set month of a to ((offset of (word 2 of dateString) in "JanFebMarAprMayJunJulAugSepOctNovDec") + 2) div 3
	set year of a to word -2 of dateString
	return a
end parseDumpedDate

on ConvertToGMT(_inputDate)
	
	try
		-- Get a list of significant DST dates in _localDate's year.
		set _dstData to run script ("{" & text 1 thru -3 of ¬
			(do shell script "/usr/sbin/zdump -v /etc/localtime | sed -E -e '/" & year of _inputDate & "/!d' -e 's|^[^ ]+ *|{_utcDate:\"|g' -e 's/ = /\", _localDate:\"/' -e 's/ isdst=1/\", _isDST:true},¬/' -e 's/ isdst=0/\", _isDST:false},¬/'") ¬
				& "}")
	on error
		-- Use plain GMT offset if no local DST data is available.
		return _inputDate - (time to GMT)
	end try
	
	-- Initialize defaults.
	set _dstOffset to time to GMT
	set _stdOffset to time to GMT
	set _dstLocal to (current date)
	set _stdLocal to _dstLocal
	
	-- Inspect each DST data point.
	set _prevData to null
	repeat with _data in _dstData
		
		-- Any transition between DST and standard time is of interest.
		if (_prevData is not null and _prevData's _isDST is not _data's _isDST) then
			
			-- Convert the last data point's dates into AppleScript date objects.
			set _dataLocal to parseDumpedDate(_prevData's _localDate)
			set _dataUTC to parseDumpedDate(_prevData's _utcDate)
			
			-- Record the seasonal offset to GMT and the season endpoints.
			if (_data's _isDST) then
				-- We have entered DST; the dates are the last standard times.
				set _stdOffset to (_dataLocal - _dataUTC)
				set _dstLocal to _dataLocal
			else
				-- We have entered standard time; the dates are the last DST times.
				set _dstOffset to (_dataLocal - _dataUTC)
				set _stdLocal to _dataLocal
			end if
			
		end if
		
		set _prevData to _data
		
	end repeat
	
	-- Determine which GMT offset to use based on the sequence of _inputDate and the DST dates.
	if _dstLocal is less than _stdLocal then
		-- DST in middle of year (northern hemisphere)
		if (_inputDate is less than or equal to _dstLocal) or (_inputDate is greater than _stdLocal) then
			return _inputDate - _stdOffset
		else
			return _inputDate - _dstOffset
		end if
	else if _stdLocal is less than _dstLocal then
		-- Standard time in middle of year (southern hemisphere)
		if (_inputDate is less than or equal to _stdLocal) or (_inputDate is greater than _dstLocal) then
			return _inputDate - _dstOffset
		else
			return _inputDate - _stdOffset
		end if
	end if
	
	-- default return only if _dstLocal equals _stdLocal (only if no or incomplete _dstData)
	return _inputDate - (time to GMT)
	
end ConvertToGMT

-- test case for change from DST to standard time in my time zone (US Eastern)
--ConvertToGMT(date "Sunday, November 4, 2007 1:59:59 AM") -- 5:59:59 AM
--ConvertToGMT(date "Sunday, November 4, 2007 2:00:00 AM") -- 7:00:00 AM

I use this function in a pair of scripts I wrote to modify the timestamps of Yojimbo items.

Hi, anoved.

Your script adjusts for the switches between GMT and British Summer Time on the correct dates and at the correct times too.

But there’s an interpretation problem (for everyone) around the time of the switch. When the clocks go back for Standard Time, the local time repeats the hour between 1:00:00 and 1:59:59. There’s no way for the script to know which occurrence of that hour is meant. Going the other way, from Standard to Daylight Saving Time, the hour’s skipped completely, so perhaps there should be a little error message when a local time’s specified in the non-existent period. :wink: