Converting Dates

asobjc
foundation
how-to

(Mark Alldritt) #1

AppleScriptObjC has lots of useful methods for dealing with dates, but the ability to freely convert to and from AppleScript dates only arrived with macOS 10.11. If you are writing scripts that need to be able to run on 10.10 or 10.9, you can use the handlers in this script to do the conversions:

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

set newDate to its makeNSDateFrom:(current date)
its makeASDateFrom:newDate

on makeNSDateFrom:theASDate
	set {theYear, theMonth, theDay, theSeconds} to theASDate's {year, month, day, time}
	if theYear < 0 then set theYear to -theYear
	set theCalendar to current application's NSCalendar's currentCalendar()
	set newDate to theCalendar's dateWithEra:(theYear ≥ 0) |year|:theYear |month|:(theMonth as integer) |day|:theDay hour:0 minute:0 |second|:theSeconds nanosecond:0
	return newDate
end makeNSDateFrom:

on makeASDateFrom:theNSDate
	set theCalendar to current application's NSCalendar's currentCalendar()
	set comps to theCalendar's componentsInTimeZone:(missing value) fromDate:theNSDate
	tell (current date) to set {theASDate, year, day, its month, day, time} to ¬
		{it, comps's |year|(), 1, comps's |month|(), comps's |day|(), (comps's hour()) * hours + (comps's minute()) * minutes + (comps's |second|())}
	return theASDate
end makeASDateFrom:

Whether you use these handlers or rely on the 10.11-and-later conversions, you should be aware that AppleScript dates are always rounded to the nearest second.


(Nigel Garvey) #2

This looks like an attempt to handle BC dates, but in fact it turns them into AD ones. Not that that’s very likely to reduce its usefulness. :wink:

Here is an attempt to produce the same results as the scripting bridge with both AD and BC dates. I haven’t tried it with every possible date in every known time zone, but it’s looking hopeful so far ….

use AppleScript version "2.3.1" -- Mavericks (10.9.something) or later
use framework "Foundation"
use scripting additions

set ASDate to (current date) - 1000000 * days --> A date in January 721 BC on the day of posting.

set newNSDate to makeNSDateFrom(ASDate)
set newASDate to makeASDateFrom(newNSDate)
return {ASDate, newNSDate, newASDate}

on makeNSDateFrom(theASDate)
	-- If the date's before about 1800, its time string's likely to be out with its time. To match the result from a scripting bridge conversion, we need an AS date with properties matching the string from the original.
	-- Get the same date in a known recent year.
	copy theASDate to recentDate
	set recentDate's year to 2000
	-- Derive dates in the current year from the original and recent dates' time strings and get the diffence between them.
	set timeAdjustment to (date (theASDate's time string)) - (date (recentDate's time string))
	-- Subtract a day from the result if it's > 0.
	if (timeAdjustment > 0) then set timeAdjustment to timeAdjustment - days
	-- Add this figure to the original date to get one with the required properties.
	set theASDate to theASDate + timeAdjustment
	-- Get the properties!
	set {y, m, d, t} to theASDate's {year, month, day, time}
	-- Positive year numbers, including 0 (1BC), are in era 1; negative in era 0.
	set theEra to (y > -1) as integer
	-- If the year number's negative, get the positive BC equivalent instead.
	if (theEra is 0) then set y to 1 - y
	
	-- Create and return the corresponding NSDate.
	set theCalendar to current application's NSCalendar's currentCalendar()
	return theCalendar's dateWithEra:(theEra) |year|:(y) |month|:(m as integer) |day|:(d) hour:(0) minute:(0) |second|:(t) nanosecond:(0)
end makeNSDateFrom

on makeASDateFrom(theNSDate)
	-- Get the relevant components from the NSDate.
	set theCalendar to current application's NSCalendar's currentCalendar()
	set comps to theCalendar's componentsInTimeZone:(missing value) fromDate:theNSDate
	tell comps to set {theEra, y, m, d, h, min, s} to {its era(), its |year|(), its |month|(), its |day|(), its hour(), its minute(), its |second|()}
	if (theEra is 1) then
		-- AD dates and those in 1BC are straightforward.
		tell (current date) to set {theASDate, its day, its year, its month, its day, its hours, its minutes, its seconds} to {it, 1, y, m, d, h, min, s}
	else
		-- 2BC and earlier will need negative years, which can't be set directly in AS dates.
		-- Get an AS date representing the first instant of 1AD. (Its date string may be several seconds out.)
		tell (current date) to set {eraStartDate, its day, its year, its month, its time} to {it, 1, 1, 1, 0}
		-- Get an AS date roughly halfway through the AD year with the same number as the BC year number from the components,
		copy eraStartDate to ADDate
		tell ADDate to set {its year, its month} to {y, July}
		-- Subtract to get a date somewhere in the middle of the BC year.
		set theASDate to eraStartDate - (ADDate - eraStartDate)
		-- Set the other properties of that year to those from the components.
		tell theASDate to set {its day, its month, its day, its hours, its minutes, its seconds} to {1, m, d, h, min, s}
	end if
	-- If necessary, perform the reverse of the time adjustment in the handler above.
	copy theASDate to recentDate
	set recentDate's year to 2000
	set timeAdjustment to ((date (theASDate's time string)) - (date (recentDate's time string)))
	if (timeAdjustment > 0) then set timeAdjustment to timeAdjustment - days
	set theASDate to theASDate - timeAdjustment
	
	return theASDate
end makeASDateFrom

For side-by-side testing, here’s the same thing using the scripting bridge:

use AppleScript version "2.5" -- El Captitan (10.11) or later
use framework "Foundation"
use scripting additions

set ASDate to (current date) - 1000000 * days

set newNSDate to (current application's NSDate's dateWithTimeInterval:(0) sinceDate:(ASDate))
set newASDate to newNSDate as date
return {ASDate, newNSDate, newASDate}

(Shane Stanley) #3

Yes, it probably shouldn’t bother. The thing with AS dates is that they’re quite likely to represent something other than the date they show for any dates before your time zone adopted its current GMT offset.

Even generating them mathematically to get around that is problematic:

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

set nowDate to current date
set nowNSDate to current application's NSDate's |date|()
set ASDate to nowDate - 10000 * days --> A date in 1990 on the day of posting.
set bridgedDate to current application's NSDate's dateWithTimeInterval:(0) sinceDate:(ASDate)
set bridgedDate2 to current application's NSDate's dateWithTimeInterval:(-10000 * days) sinceDate:nowDate
set theNSDate to current application's NSDate's dateWithTimeInterval:(-10000 * days) sinceDate:nowNSDate
return {bridgedDate, bridgedDate2, theNSDate}

When I run that here, where we are observing daylight saving’s time, bridgedDate is an hour later than bridgedDate2 and theNSDate.


(Nigel Garvey) #4

It’s an hour earlier than them here. :slight_smile: But I presume that the time zone adjustment is applied to the AS date actually bridged and that the time interval is a fixed amount. So bridgedDate is an hour ahead because it’s derived from the result of subtracting 864,000,000 seconds from an AppleScript date in your current daylight saving time. Your standard time was in force 864,000,000 seconds ago, so the result’s an hour out. The other two results are both derived from NSDates bridged directly from the original daylight saving time AppleScript date.


(Shane Stanley) #5

If you treat AS dates as NSDates generated by some app and converted to AS date format, they sort of work. Beyond that, I reckon you’re on shaky ground. Change your time (medium) format in System Preferences so that it shows the time zone. When I do that and run this:

set nowDate to current date
set otherDate to nowDate - 180 * days
return {nowDate, otherDate}

I get:

{
	date "Monday, 18 December 2017 at 11:49:00 am AEDT", 
	date "Wednesday, 21 June 2017 at 11:49:00 am AEST"
}

That just doesn’t make sense.


(Nigel Garvey) #6

Well AppleScript dates don’t do time zones. Something else which does is inserting them into the strings, based on your location and the time of year in which the dates occur.

(By the way, I’ve updated my previous post to try and make it clearer. Edit: And again the following day.)