Formatting Dates

Converting dates to and from ISO 8601 format strings – and more specifically RFC 3339 format strings – is easily done using a date formatter, but there is a catch.

Here’s a pair of handlers:

use framework "Foundation"
use scripting additions

set theDate to current application's NSDate's |date|() -- now
set dateString to my rfc3339FromDate:theDate
--> "2017-11-27T06:38:31Z"
set theDate to (my dateFromRfc3339String:dateString) as date

on rfc3339FromDate:aDate
	set theFormatter to current application's NSDateFormatter's new()
	theFormatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"en_US_POSIX")
	theFormatter's setTimeZone:(current application's NSTimeZone's timeZoneWithAbbreviation:"GMT") -- skip for local time
	theFormatter's setDateFormat:"yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX"
	return (theFormatter's stringFromDate:aDate) as text
end rfc3339FromDate:

on dateFromRfc3339String:theString
	set theFormatter to current application's NSDateFormatter's new()
	theFormatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"en_US_POSIX")
	theFormatter's setTimeZone:(current application's NSTimeZone's timeZoneWithAbbreviation:"GMT") -- skip for local time
	theFormatter's setDateFormat:"yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX"
	return theFormatter's dateFromString:theString
end dateFromRfc3339String:

The catch: the handlers deal in NSDates. Under 10.11 and later, you can pass AppleScript dates, and coerce the result to AppleScript dates, but under earlier versions you need to add code to do the conversion.

Note that setting the locale to en_US_POSIX matters, otherwise the date format string might be interpreted differently depending on your locale.

See https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table for details of the various characters’ meaning in a format string.

You can of course use similar methods to format date strings however you wish.

2 Likes

Thanks for sharing, Mark.

If anyone wants a simple conversion from AppleScript date to date formated as ISO, but at the local time. you can use this:


set todayDate to current date
set todayISO to (todayDate as «class isot» as string)
-->2017-12-02T17:39:21

I often use this to just get the date in ISO format:

set dateISOStr to text 1 thru 10 of (todayDate as «class isot» as string)
2 Likes

For the clock-watchers…

Using «class isot» is considerably faster, although of course no help if you need time zone information.

But if you are using the date formatter method, and you’re likely to call the handler multiple times, using a property will save considerable overhead. So:

use framework "Foundation"
use scripting additions
property theFormatter : missing value

-- your code in here

on rfc3339FromDate:aDate
	if theFormatter is missing value then
		set theFormatter to current application's NSDateFormatter's new()
		theFormatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"en_US_POSIX")
		theFormatter's setTimeZone:(current application's NSTimeZone's timeZoneWithAbbreviation:"GMT") -- skip for local time
		theFormatter's setDateFormat:"yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX"
	end if
	return (theFormatter's stringFromDate:aDate) as text
end rfc3339FromDate:

on dateFromRfc3339String:theString
	if theFormatter is missing value then
		set theFormatter to current application's NSDateFormatter's new()
		theFormatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"en_US_POSIX")
		theFormatter's setTimeZone:(current application's NSTimeZone's timeZoneWithAbbreviation:"GMT") -- skip for local time
		theFormatter's setDateFormat:"yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX"
	end if
	return theFormatter's dateFromString:theString
end dateFromRfc3339String:

In some simple tests that speeds things up by a factor of about 6x. Creating date formatters is a relatively time consuming thing in Objective-C.

Mind you, even without this change you’re probably looking at around half a millisecond per handler call.

3 Likes

ISO 8601 vs RFC 339

I had not heard of “RFC 339”, so I was initially confused.
I could not find an “official” statement of difference, but did find this:

What’s the difference between ISO 8601 and RFC 3339 Date Formats? – StackOverflow.com

RFC 3339 is listed as a profile of ISO 8601.

Most notably RFC 3339 requires a complete representation of date and time (only fractional seconds are optional). The RFC also has some small, subtle differences.

For example truncated representations of years with only two digits are not allowed – RFC 3339 requires 4-digit years, and the RFC only allows a period character to be used as the decimal point for fractional seconds.

The RFC also allows the “T” to be replaced by a space (or other character), while the standard only allows it to be omitted (and only when there is agreement between all parties using the representation).

I wouldn’t worry too much about the differences between the two, but on the off-chance your use case runs in to them, it’d be worth your while taking a glance at:

RFC 3339
Wikipedia entry on ISO 8601

So, Mark, if I understand things correctly (please correct me if not), then your handlers produce a date format that meets both standards.

One other note: I read that if the “Z” is not at the end of the ISO 8601 date/time, then it is assumed to be in local time, which fits what AppleScript provides.

I’m not sure if this is pure ISO 8601 or not, but I like this format:
2017-12-02 19:51 GMT-0600

which shows me the time in my local time, but let’s other know the TZ offset.

It would be nice if your handler had a parameter for TZ. Something like:
L, Z, or ±nnn
for Local, Zulu, or specific time offset in HHMM format.

Obviously you’re welcome to make your own for your own use, but the names of the handlers — rfc3339FromDate: and dateFromRfc3339String: — imply conformance to RFC 3339.

One of the places I think people might want to use these sorts of handlers is in parsing JSON. There’s no standard defined for JSON dates, but many claim to be using RFC3339.

However, many are actually using "yyyy'-'MM'-'dd'T'HH':'mm':'ssXX" rather than "yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX" (the difference is that RFC 3339 specifies time zones other than GMT as -06:00 rather than -0600). But as long as you stick to GMT-based values, the difference doesn’t matter.

The joy of standards that aren’t…

1 Like

Shane, I’m assuming that the locale ID will need to vary with the country. Is this correct?
If so, two questions:

  1. Can get get the script user’s locale ID for use in this statement?
  2. If we want to force the locale to be elsewhere, where can we get the list of locales by country?

I’ve done quite a bit of searching, but as usual Apple has not seen fit to publish lists of actual parameter values, at least not so I can find them. :wink:

It was easy to find this: localeWithLocaleIdentifier:

but no parameter values seem to be available.

Thanks.

Not for ISO 8601 and RFC 3339 formats. The reason for specifying “en_US_POSIX” is that if you leave the user’s locale (which will be the default if you don’t specify a locale), formatters can overrule the format string. In particular, HH can be interpreted differently depending on the locale’s settings.

Normally you can use:

current application's NSLocale's currentLocale()

Use:

current application's NSLocale's availableLocaleIdentifiers()

To get the identifier of the current locale:

current application's NSLocale's currentLocale()'s localeIdentifier()

So:

current application's NSLocale's localeWithLocaleIdentifier:"en_AU" -- for example
1 Like

Hey Mark @alldritt, how do you use your handlers to get a date string from a custom (not now) date?
Also, how do you convert an AppleScript date to a NSDate in 10.12+, specifically Mojave?

This fails:

use framework "Foundation"
use scripting additions

set dateStr to "6/1/2019"
set myDate to date dateStr
###  set theDate to current application's NSDate's |date|() -- now

### This Fails ###
#  ERROR:  Can’t get myDate of NSDate.
set theDate to current application's NSDate's myDate
set dateString to my rfc3339FromDate:theDate

--> "2017-11-27T06:38:31Z"


on rfc3339FromDate:aDate
  set theFormatter to current application's NSDateFormatter's new()
  theFormatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"en_US_POSIX")
  theFormatter's setTimeZone:(current application's NSTimeZone's timeZoneWithAbbreviation:"GMT") -- skip for local time
  theFormatter's setDateFormat:"yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX"
  return (theFormatter's stringFromDate:aDate) as text
end rfc3339FromDate:

You can pass any date — an NSDate or an AppleScript date — in 10.11 or later.

A variant, using NSISO8601DateFormatter and the NSISO8601DateFormatWithInternetDateTime option.

( Perhaps Shane will know what versions of macOS support this, and what its limitations, if any, might be )

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

-- iso8601StringFromDate :: Date -> String
on iso8601StringFromDate(dte)
    tell current application
        set fmt to (its NSISO8601DateFormatWithInternetDateTime as integer)
        tell init() of alloc of its NSISO8601DateFormatter
            set formatOptions to fmt
            stringFromDate_(dte) as string
        end tell
    end tell
end iso8601StringFromDate

-- dateFromiso8601String :: String -> Date
on dateFromiso8601String(strISO8601)
    tell current application
        set fmt to (its NSISO8601DateFormatWithInternetDateTime as integer)
        tell init() of alloc of its NSISO8601DateFormatter
            set formatOptions to fmt
            dateFromString_(strISO8601) as date
        end tell
    end tell
end dateFromiso8601String


on run
    dateFromiso8601String(iso8601StringFromDate(current date))
end run

NSISO8601DateFormatter requires 10.12 or later. But there was a glitch in the .bridgesupport file in 10.12 where the enums for it were left out, so you either have to define their values yourself in the script via properties (Script Debugger code-completion includes them in 10.12), or use the raw enums. Otherwise it’s effectively 10.13 or later.

I apologize Shane but under 10.13.6 the code posted by JTMichaelTX fails with error "Il est impossible d’obtenir myDate of NSDate.
If I replace
set theDate to current application’s NSDate’s myDate
by
set theDate to myDate
The script reach its end.

myDate is reported as : (date dimanche 6 janvier 2019 à 00:00:00) which is correct.

The final dateString is set to : “2019-01-05T23:00:00Z” which seems curious from my point of view.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) jeudi 25 juillet 2019 15:21:40

That’s right — you only need to involve current application’s NSDate for Cocoa dates, not AppleScript dates. Under 10.11 and greater, you can just pass an AppleScript date as-is.

In what way?

I wonder why 6 janvier 00h00 becomes 5 janvier 23h00.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) jeudi 25 juillet 2019 16:38:05

Oops. Now I understand, the result is the Greenwich time.

If JTMichaelTX really need to build an NSDate from as Applescript date,he may use a handler given by Shane in Everyday AppleScriptObjC 3ed

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on makeNSDateFrom:theASDate
	set {theYear, theMonth, theDay, theSeconds} to theASDate's {year, month, day, time}
	set theCalendar to current application's NSCalendar's currentCalendar()
	set newDate to theCalendar's dateWithEra:1 |year|:theYear |month|:(theMonth as integer) |day|:theDay hour:0 minute:0 |second|:theSeconds nanosecond:0
	--log (newDate's |description|() as text)
	return newDate
end makeNSDateFrom:
set dateStr to "6/1/2019"
set theDate to date dateStr
log theDate (*date dimanche 6 janvier 2019 à  00:00:00*)
set theNSDate to its makeNSDateFrom:theDate
log (theNSDate's |description|() as text) (*2019-01-05 23:00:00 +0000*)

It seems that I forgot the correct syntax to encapsulate code here.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) jeudi 25 juillet 2019 18:54:22

Three backquotes on a line before and after. I’ve fixed it above.

Although there’s no stringWithString: or arrayWithArray: style method for converting dates, as long as you’re running 10.11 or later, you can use this:

set theDate to current application's NSDate's dateWithTimeInterval:0 sinceDate:ASOrNSDate

Thanks Shane for both messages.

Just to be sure, are you speaking of the character whose ID is 8216 or the one whose ID is 8217?

I ask because Apple doesn’t use your naming.
character ID 8216 is named LEFT SINGLE QUOTATION MARK
character ID 8217 is named RIGHT SINGLE QUOTATION MARK

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) vendredi 26 juillet 2019 10:37:07

The one whose id is 96. You should be able to go back and click the pencil icon to edit your message — that will show exactly.

Thanks.
It’s named GRAVE ACCENT in the tool displaying the Emojis and symbols.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) vendredi 26 juillet 2019 12:01:41