Search code examples
swiftdategregorian-calendar

Add days to a date and computing the difference doesn't lead to expected result


I'm not sure what I'm doing wrong here. If you run this code in a Xcode Playground (I'm using 11.1) you will see it failing for random dates that, in my system, happen to be somewhere after 6500 days added to the current date. However those dates aren't fixed.

(1...10000).forEach { days in
    let calendar = Calendar(identifier: .gregorian)
    var dayComponent = DateComponents()
    dayComponent.day = days
    let date = Date()
    let someFutureDate = calendar.date(byAdding: dayComponent, to: date)!

    let difference = calendar.dateComponents([.day], from: date, to: someFutureDate)

    if (days != difference.day!) {
        print("WAT \(date) - \(someFutureDate) \(days) \(difference.day!)")
    }
}

Some output (failed from 130 to 160 times in my tests, output vary):

WAT 2019-10-31 14:01:47 +0000 - 2038-01-21 14:01:47 +0000 6657 6656
WAT 2019-10-31 14:01:47 +0000 - 2038-01-26 14:01:47 +0000 6662 6661
WAT 2019-10-31 14:01:47 +0000 - 2038-02-02 14:01:47 +0000 6669 6668
WAT 2019-10-31 14:01:47 +0000 - 2038-02-05 14:01:47 +0000 6672 6671
WAT 2019-10-31 14:01:47 +0000 - 2038-02-12 14:01:47 +0000 6679 6678
WAT 2019-10-31 14:01:47 +0000 - 2038-02-14 14:01:47 +0000 6681 6680
[snip]

Solution

  • That is due to rounding errors (note that a Date is internally represented as a floating point number holding the number of seconds since January 1, 2001). If you retrieve more components of the difference

    let difference = calendar.dateComponents([.day, .hour, .minute, .second, .nanosecond],
                                             from: date, to: someFutureDate)
    

    then you'll notice that it is in the first example something like

    day: 6656 hour: 23 minute: 59 second: 59 nanosecond: 999999755 
    

    which is almost the expected 6657 days. A possible fix seems to be to remove the “fractional part” of the date with

    let date = calendar.date(bySetting: .nanosecond, value: 0, of: Date())!
    

    or

    let date = Date(timeIntervalSinceReferenceDate:
                    Date().timeIntervalSinceReferenceDate.rounded())
    

    With this change, I could not observe a difference anymore.