3

How can I get the exact difference (in decimal) between 2 values of NSDate.

Eg. Jan 15 2016 to Jul 15 2017 = 1.5 Years.

I can use something like: NSCalendar.currentCalendar().components(NSCalendarUnit.CalendarUnitYear, fromDate: date1, toDate: date1, options: nil).year
but this gives me absolute values. i.e. for above example it would give me 1 Year. Is it possible to get exact values correct to at least a few decimal places?

rgamber
  • 5,749
  • 10
  • 55
  • 99
  • 3
    You need to define what a fractional year means. Is the fractional part of the year based on the number of days between the two dates (those two dates are 366 days apart (2016 is a leap year). Or does it mean exactly 6 months (and various groups of 6 months have a different number of days). – rmaddy May 15 '16 at 20:26
  • Ah, okay. I assumed that since the **from** and the **to** dates are specified, Swift has that knowledge of which year is leap, which months are 28, 30, 31 days, etc. If that's not the case then I will have to implement my own solution based on assumptions. So basically Swift won't give decimal accurate values to me, right? – rgamber May 15 '16 at 20:28
  • 1
    Of course all of that information is known. You missed my point. You first need to come to a desired definition for a fractional year. There is more than one way to calculate a partial year. – rmaddy May 15 '16 at 20:30
  • I guess I have some reading to do. Not familiar with the partial year/ fractional year concepts. Can you point me to some reading links or examples? – rgamber May 15 '16 at 20:33
  • I think I get what you mean. So if I get 365 days difference, then to get the conversion I would need to define if 365 days = 1 year or 366 days = 1 year. – rgamber May 15 '16 at 20:42
  • No, that's not really the issue (though it's a small part of it). Let's take a simpler example. Two dates: Jan 15, 2016 - Feb 15, 2106. That's one month, right? That's 1/12 of a year so you could say 0.08333 years. But it's also 31 days. With 365 days per year, that's 0.08493 years. With 366 days per year, that's 0.84699 years. That's three possible values with one example. Then what about the dates Feb 15, 2016 - Mar 15, 2016. Still one month, right? But now it's also only 29 days. See the problem? – rmaddy May 15 '16 at 20:51
  • Having said all of that, update your question with what your goal is. Why do you need the date difference in fractional years? The answer to that can help people point you to the most relevant solution. – rmaddy May 15 '16 at 20:52
  • Yeah, I see what you mean! (Btw, Jan 15, 2016 - Feb 15, 2106, that is over 90 years and a month :) ). I was just trying to show some extra information to the user in my app when the user selects multiple travel dates. But its not a necessity, so I can definitely do with the absolute values without having to go deeper. Thanks for the help! – rgamber May 15 '16 at 20:55
  • Out of curiosity, how will you use this result? Is this something you'll use in, for example, other date-based calculations? Or is it something you'll display to the user to tell them how far apart two dates are? – Tom Harrington May 15 '16 at 23:07
  • Right, to tell the user how far apart the dates are. – rgamber May 15 '16 at 23:08
  • BTW - you should look into [the `YEARFRAC` function](https://www.google.com/search?q=yearfrac) found in Excel and other spreadsheet programs. – rmaddy May 17 '16 at 03:43

4 Answers4

5

The terms you've used here are misleading. When you say "absolute" you mean "integral." And when you say "exact" you mean "within some desired precision."

Let's say the precision you wanted was 2 decimal places, so we'd need to measure a year to 1%. That's larger than a day, so tracking days is sufficient. If you needed more precision, then you could expand this technique, but if you push it too far, "year" gets more tricky, and you have to start asking what you mean by "a year."

Avoid asking this question when you can. Many answers here say things like "there are 365.25 days in a year." But try adding "365.25 * 24 hours" to "right now" and see if you get "the same date and time next year." While it may seem correct "on average," it is actually wrong 100% of the time for calendar dates. (It works out here because it's within 1%, but so would 365, 366, or even 363.)

We avoid this madness by saying "1% is close enough for this problem."

// What calendar do you *really* mean here? The user's current calendar, 
// or the Gregorian calendar? The below code should work for any calendar,
// because every calendar's year is made up of some number of days, but it's
// worth considering if you really mean (and are testing) arbitrary calendars.
// If you mean "Gregorian," then use NSCalendar(identifier: NSCalendarIdentifierGregorian)!
let calendar = NSCalendar.currentCalendar()

// Determine how many integral days are between the dates
let diff = calendar.components(.Day, fromDate: date1, toDate: date2, options: [])

// Determine how many days are in a year. If you really meant "Gregorian" above, and
// so used calendarWithIdentifer rather than currentCalendar, you can estimate 365 here.
// Being within one day is inside the noise floor of 1%.
// Yes, this is harder than you'd think. This is based on MartinR's code: http://stackoverflow.com/a/16812482/97337
var startOfYear: NSDate? = nil
var lengthOfYear = NSTimeInterval(0)
calendar.rangeOfUnit(.Year, startDate: &startOfYear, interval: &lengthOfYear, forDate: date1)
let endOfYear = startOfYear!.dateByAddingTimeInterval(lengthOfYear)
let daysInYear = calendar.components(.Day, fromDate: startOfYear!, toDate: endOfYear, options: []).day

// Divide
let fracDiff = Double(diff.day) / Double(daysInYear)

That said, in most cases you shouldn't be doing this. Since iOS 8, the preferred tool is NSDateComponentsFormatter. You won't get this precise format (i.e. fractional years), but you'll get a nicely localized result that takes most issues into account across different cultures.

let formatter = NSDateComponentsFormatter()
formatter.unitsStyle = .Full
formatter.includesApproximationPhrase = true
formatter.allowedUnits = [.Year, .Month]
formatter.allowsFractionalUnits = true

formatter.stringFromDate(date1, toDate: date2)
// About 1 year, 6 months
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
3

Since you mentioned that your goal is something you can display to users as a meaningful indication of the time between two dates, you might find it easier to use NSDateComponentsFormatter. For example:

let dateStr1 = "Jan 15 2016"
let dateStr2 = "Jul 15 2017"

let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "MMM dd yyyy"

if let date1 = dateFormatter.dateFromString(dateStr1),
    let date2 = dateFormatter.dateFromString(dateStr2) {
    let dateComponentsFormatter = NSDateComponentsFormatter()
    dateComponentsFormatter.allowedUnits = [.Year, .Month]
    dateComponentsFormatter.unitsStyle = .Full

    let difference = dateComponentsFormatter.stringFromDate(date1, toDate: date2)
}

This gives you a string that reads "1 year, 6 months". It's not exactly what you specified as your goal, but it's a clear indication for users and avoids a lot of complexity. There's a property on NSDateComponentsFormatter called allowsFractionalUnits that's supposed to lead to results like "1.5 years", but it doesn't seem to work right now. (Even if you limit the allowedUnits to only .Year, you still don't get a fractional year. I'm off to file a bug with Apple...). You can tweak allowedUnits to get whatever granularity you like, and use includesApproximationPhrase to have the class add a localized version of "About..." to the resulting string if it's not precise. If you have some flexibility in your final format, this would be a really good solution.

Community
  • 1
  • 1
Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Yes, I appreciate the help! Getting the value split into different components was straight forward :) I was just trying to show the exact value for one particular component. Thanks though! – rgamber May 15 '16 at 23:26
  • Updated my question title to avoid confusion! – rgamber May 15 '16 at 23:51
2

There isn't a perfect answer to this question. Different years are slightly different lengths. You have to make some assumptions.

If you assume 365.2425 days per year, with each day having 24 hours, then the calculation is trivial:

let secondsPerYear: NSTimeInterval = NSTimeInterval(365.2425 * 24 * 60 * 60)
let secondsBetweenDates = 
  date2.timeIntervalSinceReferenceDate - date1.timeIntervalSinceReferenceDate;
let yearsBetweenDates = secondsBetweenDates / secondPerYear

But there are lots of edge cases and weirdness to deal with. Because of leap years, some years have 365 days, and some have 366. Then there's leap seconds.

If you get rid of months in @CodeDifferent's answer then you'll get an answer that allows for leap days between the dates.

But, as Code Different pointed out, his answer as written actually gives answers that seem more accurate, even though they are not. (A difference of 3 months will always yield .25 years, and will ignore longer/shorter months. Is that the right thing to do? Depends on your goal and your assumptions.)

Duncan C
  • 128,072
  • 22
  • 173
  • 272
1

According to NASA, there are 365.2422 days per year on average. Here, I round that up to 365.25 days per year:

let components = NSCalendar.currentCalendar().components([.Year, .Month, .Day], fromDate: fromDate, toDate: toDate, options: [])

var totalYears = Double(components.year)
totalYears += Double(components.month) / 12.0
totalYears += Double(components.day) / 365.25

Obviously, this depends on your assumptions. If you want to count of leap days between fromDate and toDate, it will be more complicated.

Some sample outputs:

From date      To date        Total Years
------------   ------------   ------------
Jan 15, 2016   Jul 15, 2017   1.5
Jan 15, 2016   Apr 14, 2016   0.25
Jan 15, 2016   Aug 15, 2017   1.5833
Jan 15, 2016   Jan 14, 2018   1.9988
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • Including months in the calculation seems like it introduces error needlessly. Months vary from 28 to 31 days in length. Why not ask for the number of years and days between the 2 dates and skip months entirely? – Duncan C May 15 '16 at 21:38
  • 1
    @DuncanC unfortunately that's not how the human mind works. For examples, there are 91 days between `Apr 1` and `Jul 1`; 92 days between `Jul 1` and `Oct 1`. However, most would say they are both 3-month difference. We even have a name for that: *a quarter*, or `0.25` of a year. Calculating date is complicated. – Code Different May 15 '16 at 21:45
  • Agreed about it being complicated. It depends on the goal, really. If you want an estimate of the fractional difference between dates that's as accurate as possible, leaving out months would be better. Your point about expectations is a good one, though. We expect the difference from Feb 1 to March 1 to be 1/12 of a year, and the difference between March 1 and April 1 to also be 1/12 of a year, even though the first range is 28/29 days and the 2nd range is 31 days. – Duncan C May 15 '16 at 21:53