From ISO 8601 conversion to general date handling.

I’m new to AppleScript, but I’ve been programming since 1964. So I’ve been looking, unsuccessfully I’m afraid, for something like a library of date handling routines. What I’m trying to do comes in both simple and complex forms:

Simple Form
Given a date & time string that more or less conforms to ISO 8601 standards, such as “2013-01-31 12:31:57” convert it to the U.S. format and then coerce to a variable of type “date.”

Complex Form
Given a date & time string in any of a number of common formats, coerce it to a date-type variable in a portable way. This will require either (a) learning the system’s Dates and Times formats (System Preferences > Language & Text > Formats) so the string can be converted to a legitimate source format or (b) programmatically saving the current Dates & Times formats, substituting a particular set of formats (perhaps the one of the source string), modifying the source string if necessary, coercing the source string into a date variable, and restoring the original Dates & Times formats.

The difference between the two is that the simple form is hard-wired for ISO 8601 to U.S. on the way to setting the date-type variable, whereas the complex form is general and can coerce any standard date & time format to a date-type variable.

Can anyone help me with this?

Thanks.

Hi,

Knowing that are so many ways to format a date, you might want to start with knowing what type of date and converting that.

Edited: I guess the question is where are you getting all these different formats?

Edited: If you get it from the user, then you might want to nuse something like a date picker.

gl,
kel

I don’t think you’ll find any ready-made libraries. The simple form should be easy enough to parse your self, but the complex form gets tricky. If you’re using Mavericks, probably the best approach would be to use an ASObjC-based library and Cocoa’s NSDateFormatter class. That also gives you simple access to the various locale settings.

Wow! :slight_smile: Welcome to MacScripter.

There’s actually no need to convert to US format if the ultimate goal’s a ‘date’ variable. You only need to extract the figures from the text and set the appropriate properties of a date object. One way to get a date object to modify is to use the ‘current date’ function. (A date object can also be written and compiled directly into your script in your own machine’s local format. It will appear on other machines in their local formats. But the ‘current date’ method’s better when distributing code in text form, as here.)

on moreOrLessISO8601ToDate(iso8601)
	-- Get a string containing just the digit characters from the input.
	set digitsOnly to (do shell script "sed 's/[^0-9]//g' <<<" & quoted form of iso8601)
	
	set digitCount to (count digitsOnly)
	if (digitCount is 8) then -- Assume only the date is included.
		tell digitsOnly to set {y, m, d, hr, min, sec} to {text 1 thru 4, text 5 thru 6, text 7 thru 8, 0, 0, 0}
	else if (digitCount is 14) then -- Assume both the date and the time are included.
		tell digitsOnly to set {y, m, d, hr, min, sec} to {text 1 thru 4, text 5 thru 6, text 7 thru 8, text 9 thru 10, text 11 thru 12, text 13 thru 14}
	else -- Don't know how to interpret the input.
		error
	end if
	
	-- Get a date object and modify it appropriately.
	-- Its day should be set below 29 first to avoid any overflow during the other settings.
	-- The numeric texts are automatically coerced to integers during the setting.
	tell (current date) to set {its day, its year, its month, its day, its hours, its minutes, its seconds, dateObject} to {1, y, m, d, hr, min, sec, it}
	
	return dateObject
end moreOrLessISO8601ToDate

set dateVariable to moreOrLessISO8601ToDate("2013-01-31 12:31:57")

There are system calls which can read the user’s Date/Time preferences, as Shane has said. There are examples using shell scripts in other threads on this site. You still have to use that information yourself though.

A simple vanilla way to deduce the user’s preferred short-date order is to set up a date specifier using a known short date and see what gets set to what in the resulting date object:

-- Create a date object from the short-date string "1 2 3".
set testDate to date ("1 2 3" as text)
-- Get the year, month, and day values from the date object.
-- The prior subtraction of the time is a precaution against a current AppleScript date bug.
set {year:y, month:m, day:d} to testDate - (testDate's time)

-- Arrange "year", "month", and "day" into a list in the order suggested by the respective values.
set shortDateOrder to {missing value, missing value, missing value}
tell shortDateOrder to set {item (y mod 10), item (m as integer), item d} to {"year", "month", "day"}

return shortDateOrder
--> {"day", "month", "year"} on my machine

And you can always skip sed and use AppleScript’s Swiss army knife of text processing, text item delimiters:

on moreOrLessISO8601ToDate(iso8601)
	set saveTID to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {"-", space, ":"}
	set numberGroups to text items of iso8601
	set AppleScript's text item delimiters to saveTID
	set valuesCount to count of numberGroups
	if valuesCount = 3 then
		set {y, m, d} to numberGroups
		set {hr, min, sec} to {0, 0, 0}
	else if valuesCount = 6 then
		set {y, m, d, hr, min, sec} to numberGroups
	else
		error
	end if
	tell (current date) to set {its day, its year, its month, its day, its hours, its minutes, its seconds, dateObject} to {1, y, m, d, hr, min, sec, it}
	return dateObject
end moreOrLessISO8601ToDate

Hi Shane.

The problem with using delimiters to divide a more-or-less-ISO-8601 date into individual values is that there may actually be no delimiters at all in the date or time, as in, say, an iCalendar file, where the same date would be supplied as “20130131T123157”. So the values should ideally be coerced back together anyway in order to catch that possibility. Since this takes up two more lines of TID action, I decided to use sed instead for clarity.

Thanks everyone for your help!

Since I’m going to use this script to process batches of records regularly, I wanted to see which approach was more efficient. Nigel’s script took 28 ms on my iMac, and Shane’s took 21. I guess this is due to the time it takes to start a shell and run sed.

But Nigel has a good point that an input string may contain no delimiters. So I modified Shane’s script to remove any delimiters and then apply Nigel’s approach. This actually reduced the time to 20ms! I’m not sure why this would be the case since the modified script actually has extra statements, as Nigel pointed out. I probably ought to run each version 10,000 times to get better timing estimates, but I’ll leave this as an exercise for the reader. :lol:

Anyway, here’s the modified script, complete with timing statements:


on moreOrLessISO8601ToDate(iso8601)
	set saveTID to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {"-", space, ":", "/", "T"}
	set numberGroups to text items of iso8601
	set AppleScript's text item delimiters to saveTID
	set digitsOnly to numberGroups as text
	
	set digitCount to (count of digitsOnly)
	if (digitCount is 8) then -- Assume only the date is included.
		tell digitsOnly to set {y, m, d, hr, min, sec} to {text 1 thru 4, text 5 thru 6, text 7 thru 8, 0, 0, 0}
	else if (digitCount is 14) then -- Assume both the date and the time are included.
		tell digitsOnly to set {y, m, d, hr, min, sec} to {text 1 thru 4, text 5 thru 6, text 7 thru 8, text 9 thru 10, text 11 thru 12, text 13 thru 14}
	else -- Don't know how to interpret the input.
		error
	end if
	
	-- Get a date object and modify it appropriately.
	-- Its day should be set below 29 first to avoid any overflow during the other settings.
	-- The numeric texts are automatically coerced to integers during the setting.
	tell (current date) to set {its day, its year, its month, its day, its hours, its minutes, its seconds, dateObject} to {1, y, m, d, hr, min, sec, it}
	
	return dateObject
	
end moreOrLessISO8601ToDate


set mgRightNow to "perl -e 'use Time::HiRes qw(time); print time'"
set mgStart to do shell script mgRightNow
-------------------
set dateVariable to moreOrLessISO8601ToDate("2013-01-31 12:31:57")
---------------
set mgStop to do shell script mgRightNow
set mgRunTime to mgStop - mgStart
display dialog "This took " & mgRunTime & " seconds." & return & "that's " & (round (mgRunTime * 1000)) & " milliseconds."

Hi.

For safety, the coercion of the list of numeric texts into one should be done with the TIDs set explicitly to “”, thus:

on moreOrLessISO8601ToDate(iso8601)
	set saveTID to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {"-", space, ":", "/", "T"}
	set numberGroups to text items of iso8601
	set AppleScript's text item delimiters to "" -- Set the TIDs explictly for coercions of lists to text.
	set digitsOnly to numberGroups as text
	set AppleScript's text item delimiters to saveTID

	-- etc.

Most of the time measured in your script comes from the shell scripts used to run the timer! :wink: Timing it with an OSAX I have, it’s so fast I can’t get a reading! Repeating it 1000 times takes 1.28 seconds. If I coerce the long numeric text to a number and do everything else numerically rather than textually, 1000 iterations takes only 0.47 seconds:


on moreOrLessISO8601ToDate(iso8601)
	set saveTID to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {"-", space, ":", "/", "T"}
	set numberGroups to text items of iso8601
	set AppleScript's text item delimiters to ""
	set digitsOnly to numberGroups as text
	set AppleScript's text item delimiters to saveTID
	
	set digitCount to (count of digitsOnly)
	if (digitCount is 8) then -- Assume only the date is included.
		tell (digitsOnly as integer) to set {y, m, d, t} to {it div 10000, it mod 10000 div 100, it mod 100, 0}
	else if (digitCount is 14) then -- Assume both the date and the time are included.
		tell (digitsOnly as number) to set {y, m, d, t} to {it div 1.0E+10, it mod 1.0E+10 div 100000000, it mod 100000000 div 1000000, it mod 1000000 div 10000 * hours + it mod 10000 div 100 * minutes + it mod 100}
	else -- Don't know how to interpret the input.
		error
	end if
	
	-- Get a date object and modify it appropriately.
	-- Its day should be set below 29 first to avoid any overflow during the other settings.
	-- The numeric texts are automatically coerced to integers during the setting.
	tell (current date) to set {its day, its year, its month, its day, its time, dateObject} to {1, y, m, d, t, it}
	
	return dateObject
	
end moreOrLessISO8601ToDate

repeat 1000 times
	set dateVariable to moreOrLessISO8601ToDate("2013-01-31 12:31:57")
end repeat

I’ll just wipe the coffee off my screen again :wink:

Do I detect a hint of sarcasm from the leading advocate of ASObjC? :wink:

heaven forbid – it was pure amusement :slight_smile:

Since this is one of the top hits for applescript and ISO8601, I thought I would share my ugly perhaps, but working applescript to convert current time to ISO8601

on date2ISO8601()
	set the_date to (current date) - (time to GMT) + (3600 * 16)
	set the_year to year of the_date
	set the_month to ((month of the_date) * 1)
	if the_month < 10 then
		set the_month to "0" & the_month
	end if
	set the_day to day of the_date
	
	if the_day < 10 then
		set the_day to "0" & the_day
	end if
	
	set the_time to time string of the_date
	set the_time to stringtolist(the_time, " ")
	set the_timeAMPM to item 2 of the_time
	set the_time to stringtolist(item 1 of the_time, ":")
	set the_time_hours to (item 1 of the_time) as number
	
	if the_time_hours = 12 then
		set the_time_hours to 0
	end if
	
	if the_timeAMPM = "PM" then
		set the_time_hours to (((the_time_hours) + 12) as text)
	end if
	
	if the_time_hours < 10 then
		set the_time_hours to ("0" & the_time_hours as text)
	end if
	
	return (the_year & "-" & the_month & "-" & the_day & "T" & the_time_hours & ":" & item 2 of the_time & ":" & item 3 of the_time & "Z" as text)
	
end date2ISO8601

on stringtolist(theString, delim)
	set oldelim to AppleScript's text item delimiters
	set AppleScript's text item delimiters to delim
	set dlist to (every text item of theString)
	set AppleScript's text item delimiters to oldelim
	return dlist
end stringtolist


Hi oscar.

There’s a ‘stringtolist()’ handler missing from your script.