# How to calculate time span?

I’m trying to get a string describing the interval between to dates and I guess I’m using the wrong tool.

Why, when the interval is greater than 68 years, the result looses 1 year per year?
Here is an example of what I mean:

``````set inter1 to my getInterval:"1/1/1951" --> 68a 0m 1sem
set inter2 to my getInterval:"1/1/1950" --> -67a 0m 3sem
set inter2 to my getInterval:"1/1/1940" --> -57a 0m 3sem

on getInterval:thedate
set thedate to current application's NSDate's dateWithTimeInterval:0 sinceDate:(date thedate as date)
set timeInterval to ((current application's NSDate's new())'s timeIntervalSinceDate:thedate)
set theFormatter to current application's NSDateComponentsFormatter's new()
theFormatter's setAllowedUnits:(4 + 8 + 4096)
return (theFormatter's stringFromTimeInterval:timeInterval) as text
end getInterval:
``````

That looks like a bug in `NSDateComponentsFormatter`, pure and simple. If you want to log it with Apple, here’s the Objective-C code you can include to demonstrate it:

``````    NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init];
formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleFull;
formatter.includesApproximationPhrase = YES;
formatter.includesTimeRemainingPhrase = YES;
formatter.allowedUnits = NSCalendarUnitYear;
NSTimeInterval interval = 2.14e+9;
NSString* outputString = [formatter stringFromTimeInterval:interval];
NSLog(@"%f, %@", interval, outputString); // "2140000000.000000, About 67 years remaining"
interval = 2.15e+9;
outputString = [formatter stringFromTimeInterval:interval];
NSLog(@"%f, %@", interval, outputString); // "2150000000.000000, About -67 years remaining"
``````

Meanwhile, the `stringFromDate:toDate:` method seems to be OK (in Mojave):

``````use AppleScript version "2.4"
use framework "Foundation"

set inter1 to my getInterval:"1/1/1951" --> "68y 0mo 1w"
set inter2 to my getInterval:"1/1/1950" --> "69y 0mo 1w"
set inter3 to my getInterval:"1/1/1940" --> "79y 0mo 1w"
set inter4 to my getInterval:"1/1/0001" --> "2,018y 0mo 1w"

on getInterval:thedate
set thedate to current application's NSDate's dateWithTimeInterval:0 sinceDate:(date thedate as date)
set theFormatter to current application's NSDateComponentsFormatter's new()
theFormatter's setAllowedUnits:(4 + 8 + 4096)

return (theFormatter's stringFromDate:thedate toDate:(current application's NSDate's |date|())) as text
end getInterval:
``````
1 Like

@ShaneStanley
Thanks for the objC code.
Bug report done!

@NigelGarvey
The `stringFromDate:toDate:` method is a perfect workaround.
Thank you too!

FWIW a slight generalisation:

``````use AppleScript version "2.4"
use framework "Foundation"
use framework "JavaScriptCore"

-- Unit keys: any gapless substring of "ymwdhns" (years, months, month-weeks, days, hours, mins, secs)
-- NB 'n' = minutes
on run

set xs to {"1951-02-01", "1950-02-01", "1940-02-01", "0001-02-01"}

-- "ym" = years months
set units to "ym"

map(compose(timeDiffString(units)'s |λ|(current date), dateFromISO), xs)

--> {"67y 11mo", "68y 11mo", "78y 11mo", "2,017y 11mo"}

-- "ymw" = years months weeks
set units2 to "ymw"

map(compose(timeDiffString(units2)'s |λ|(current date), dateFromISO), xs)

--> {"67y 11mo 1w", "68y 11mo 1w", "78y 11mo 1w", "2,017y 11mo 1w"}
end run

-- String of Unit keys -> To Date -> From Date -> Interval String

-- timeDiffString :: String -> Date -> Date -> String
on timeDiffString(ks)
set recUnitOptions to {y:4, m:8, w:4096, d:16, h:32, n:64, s:128}
set fmtr to current application's NSDateComponentsFormatter's new()
script unitFlags
on |λ|(a, k)
set mb to lookupDict(k, recUnitOptions)
if Nothing of mb then
a
else
a + (Just of mb)
end if
end |λ|
end script
fmtr's setAllowedUnits:foldl(unitFlags, 0, characters of ks)
script
on |λ|(dteTo)
script
on |λ|(dteFrom)
(fmtr's stringFromDate:dteFrom toDate:dteTo) as text
end |λ|
end script
end |λ|
end script
end timeDiffString

-- GENERIC FUNCTIONS -------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- bindJSC (>>=) :: JSC a -> (a -> JSC b) -> JSC b
on bindJSC(mJSC, mf)
{type:"JSC", jsc:mJSC's jsc, value:¬
unwrap((mJSC's jsc's ¬
evaluateScript:(|λ|(value of mJSC) of mReturn(mf)))'s toObject())}
end bindJSC

-- compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
on compose(f, g)
script
property mf : mReturn(f)
property mg : mReturn(g)
on |λ|(x)
mf's |λ|(mg's |λ|(x))
end |λ|
end script
end compose

-- dateFromISO :: ISO8601 String -> Date
on dateFromISO(s)
script dateFromString
on |λ|(v)
"new Date('" & v & "')"
end |λ|
end script
value of bindJSC(pureJSC(s), dateFromString)
end dateFromISO

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
tell mReturn(f)
set v to startValue
set lng to length of xs
repeat with i from 1 to lng
set v to |λ|(v, item i of xs, i, xs)
end repeat
return v
end tell
end foldl

-- Just :: a -> Maybe a
on Just(x)
{type:"Maybe", Nothing:false, Just:x}
end Just

-- lookupDict :: a -> Dict -> Maybe b
on lookupDict(k, dct)
set ca to current application
set v to (ca's NSDictionary's dictionaryWithDictionary:dct)'s objectForKey:k
if v ≠ missing value then
Just(item 1 of ((ca's NSArray's arrayWithObject:v) as list))
else
Nothing()
end if
end lookupDict

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
tell mReturn(f)
set lng to length of xs
set lst to {}
repeat with i from 1 to lng
set end of lst to |λ|(item i of xs, i, xs)
end repeat
return lst
end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn

-- pureJSC :: a -> {type::String, jsc::JSContext, value::a}
on pureJSC(x)
{type:"JSC", jsc:current application's JSContext's new(), value:x}
end pureJSC

-- Nothing :: Maybe a
on Nothing()
{type:"Maybe", Nothing:true}
end Nothing

-- unwrap :: NSObject -> a
on unwrap(objCValue)
if objCValue is missing value then
missing value
else
set ca to current application
item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
end if
end unwrap
``````

The bug occurs when timeInterval ≥ (2 ^ 31). There’s no problem up to (2 ^ 31) -1.

So they’re converting the input to a signed integer, but only 32-bit. Pretty weird bug for something only introduced in macOS 10.10.

Yeah. I presume your Objective-C code demonstrates that it’s a formatter bug and not a scripting bridge one?

Here for fun is a less versatile, but apparently reliable vanilla method. There’s a possible issue with the ASObjC one in that, although the units of interest are years, months, and weeks, the calculation is based on the interval between the dates in seconds. The input dates’ times are always 0, whereas the dates from which they’re subtracted have the times when the script’s run. So if an input date is an exact number of calendar weeks, months, or years into the future, the result will be less than what I assume would be needed (unless the script’s run on the stroke of midnight, of course). So, running the script today, 12th January 2019:

This is getroundable, of course. But the script below is based only on the relevant calendar units and produces the following results anyway:

``````set inter1 to my getInterval:"1/1/1951" --> "68y 0mo 1w"
set inter2 to my getInterval:"1/1/1950" --> "69y 0mo 1w"
set inter3 to my getInterval:"1/1/1940" --> "79y 0mo 1w"
set inter4 to my getInterval:"1/1/0001" --> "2018y 0mo 1w"
set inter5 to my getInterval:"12/1/2020" --> "-1y 0mo 0w"

on getInterval:theDate
set theDate to date theDate
set now to (current date)

set {year:dateYear, month:dateMonth, day:dateDay} to theDate
set {year:nowYear, month:nowMonth, day:nowDay} to now

if (theDate comes after now) then
set yearDiff to dateYear - nowYear
set monthDiff to dateMonth - nowMonth
set dayDiff to dateDay - nowDay
set prefix to "-"
else
set yearDiff to nowYear - dateYear
set monthDiff to nowMonth - dateMonth
set dayDiff to nowDay - dateDay
set prefix to ""
end if

if (dayDiff < 0) then
set dayDiff to ((now - nowDay * days)'s day) - dateDay + nowDay
set monthDiff to monthDiff - 1
end if
set weekDiff to ((dayDiff div 7) as text) & "w"
if (monthDiff < 0) then
set monthDiff to 12 + monthDiff
set yearDiff to yearDiff - 1
end if
if ((monthDiff is 0) and (yearDiff is 0)) then
set monthDiff to ""
else
set monthDiff to (monthDiff as text) & "mo "
end if
if (yearDiff is 0) then
set yearDiff to ""
else
set yearDiff to (yearDiff as text) & "y "
end if

return prefix & yearDiff & monthDiff & weekDiff
end getInterval:
``````

@NigelGarvey

Ah, this good old Mister Applescript! Who says that he’s not capable?

Anyway, I prefer to rely on AppleScriptObjC, because the handler is intended to be part of a script library.
Here is what I was able to do to cover my needs:

``````use AppleScript version "2.5"
use framework "Foundation"

set result1 to my dateSpan:"1/1/1950" toDate:(current application's NSDate's |date|()) fullString:true objC:true --> 69 ans, 1 semaine et 3 jours
set result2 to my dateSpan:(current date) toDate:"1/1/2089" fullString:false objC:false --> 69a 11m 2sem 6j
set result3 to my dateSpan:(current date) toDate:((current date) + 45 * days) fullString:true objC:false --> 1 mois et 2 semaines
set result4 to my dateSpan:(current date) toDate:((current date) + 45 * days) fullString:false objC:false --> 1m 2sem 0j
set result5 to my dateSpan:((current date) - 3 * days) toDate:(current date) fullString:true objC:false --> 3 jours
set result6 to my dateSpan:((current date) - 27.45 * hours) toDate:(current date) fullString:true objC:false --> 1 jour, 3 heures et 27 minutes
set result6 to my dateSpan:((current date) - 27.45 * hours) toDate:(current date) fullString:false objC:false --> 1j 03:27:00
set result7 to my dateSpan:((current date) - 3) toDate:(current date) fullString:true objC:false --> 00:00:03
set result8 to my dateSpan:((current date) - 63) toDate:(current date) fullString:false objC:false --> 00:01:03

on dateSpan:theDate1 toDate:theDate2 fullString:wFull objC:wObjc
if class of theDate1 = text then set theDate1 to current application's NSDate's dateWithTimeInterval:0 sinceDate:((date theDate1) as date)
if class of theDate1 ≠ class "NSDate" then set theDate1 to current application's NSDate's dateWithTimeInterval:0 sinceDate:theDate1
if class of theDate2 = text then set theDate2 to current application's NSDate's dateWithTimeInterval:0 sinceDate:((date theDate2) as date)
if class of theDate2 ≠ class "NSDate" then set theDate2 to current application's NSDate's dateWithTimeInterval:0 sinceDate:theDate2
set theDate1 to current application's NSDate's dateWithTimeInterval:0 sinceDate:theDate1
set theDate2 to current application's NSDate's dateWithTimeInterval:0 sinceDate:theDate2
set theFormatter to current application's NSDateComponentsFormatter's new()
set timeInterval to (theDate1's timeIntervalSinceDate:theDate2)

if timeInterval < (-1 * days) or timeInterval > (1 * days) then
set wFull to (wFull as integer) * 3 -- full string if true
set wZero to ((wFull as integer) * 4) + 2 -- omits all zero values if true
theFormatter's setUnitsStyle:wFull -- NSDateComponentsFormatterUnitsStyleFull = 3 -- NSDateComponentsFormatterUnitsStyleAbbreviated = 1
theFormatter's setZeroFormattingBehavior:wZero -- NSDateComponentsFormatterZeroFormattingBehaviorDropAll = 14
if timeInterval < (-7 * days) or timeInterval > (7 * days) then theFormatter's setAllowedUnits:(4 + 8 + 4096 + 16) --  NSCalendarUnitYear, NSCalendarUnitMonth,NSCalendarUnitWeekOfMonth, NSCalendarUnitDay
else
theFormatter's setAllowedUnits:(32 + 64 + 128) -- NSCalendarUnitHour, NSCalendarUnitMinute, NSCalendarUnitSecond
end if

if timeInterval < 0 then set theResult to (theFormatter's stringFromDate:theDate1 toDate:theDate2)
if timeInterval ≥ 0 then set theResult to (theFormatter's stringFromDate:theDate2 toDate:theDate1)
if wObjc = true then return theResult
return theResult as text
end dateSpan:toDate:fullString:objC:
``````

… one way being to use NSCalendar to get a version of the current date with its time set to 0:

``````use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

set inter1 to my getInterval:"1/1/1951" --> "68y 0mo 1w"
set inter2 to my getInterval:"1/1/1950" --> "69y 0mo 1w"
set inter3 to my getInterval:"1/1/1940" --> "79y 0mo 1w"
set inter4 to my getInterval:"1/1/0001" --> "2018y 0mo 1w"
set inter5 to my getInterval:"12/1/2020" --> "-1y 0mo 0w"

on getInterval:theDate
set theDate to current application's NSDate's dateWithTimeInterval:0 sinceDate:(date theDate as date)
set theFormatter to current application's NSDateComponentsFormatter's new()
theFormatter's setAllowedUnits:(4 + 8 + 4096)
set today to current application's NSCalendar's currentCalendar's startOfDayForDate:(current application's NSDate's |date|())

return (theFormatter's stringFromDate:theDate toDate:(today)) as text
end getInterval:
``````

Yep. Although we’re passing a real anyway, I wanted to be doubly () sure.