Why do my simple date calculations sometimes fail in Swift 3.1?

I have a unit test that looks like this:

  func testManyYearsAgo() {
    for year in 2...77 {
      let earlierTime = calendar.date(byAdding: .year, value: 0 - year, to: now)
//      print(year)
//      print(dateDifference.itWasEstimate(baseDate: now, earlierDate: earlierTime!))
      XCTAssertEqual(dateDifference.itWasEstimate(baseDate: now, earlierDate: earlierTime!), "\(year) years ago")
    }
  }

      

now

defined above as soon as Date()

calendar

Calendar.current

He is testing a class that looks something like this:

class DateDifference {
  func itWasEstimate(baseDate: Date, earlierDate: Date) -> String {

    let calendar = Calendar.current
    let requestedComponent: Set<Calendar.Component> = [ .year, .month, .day, .hour, .minute, .second]
    let timeDifference = calendar.dateComponents(requestedComponent, from: baseDate, to: earlierDate)

    if timeDifference.year! < 0 {
      if timeDifference.year! == -1 {
        return "Last year"
      } else {
        return "\(abs(timeDifference.year!)) years ago"
      }
    }

    return ""
  }
}

      

When I run a unit test, I usually (but not always) get an error like:

XCTAssertEqual failed: ("30 years ago") is not equal to ("31 years ago")

      

These errors usually start after the year value exceeds 12.

If I uncomment the print instructions it works fine no matter how many times I run the code.

This leads me to believe that there might be some strange asynchronous thing going on there, but I can't tell by looking. I'm relatively new to rapid development, so there might be something fundamental I'm missing.

+3


source to share


2 answers


Here's a self-contained reproducible example demonstrating the Problem:

var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "en_US_POSIX")
calendar.timeZone = TimeZone(secondsFromGMT: 0)!

let formatter = DateFormatter()
formatter.calendar = calendar
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"

let d1 = DateComponents(calendar: calendar, year: 2017, month: 1, day: 1, hour: 0,
                        minute: 0, second: 0, nanosecond: 456 * Int(NSEC_PER_MSEC)).date!
print("d1:", formatter.string(from: d1))

let d2 = calendar.date(byAdding: .year, value: -20, to: d1)!
print("d2:", formatter.string(from: d2))

let comps: Set<Calendar.Component> = [ .year, .month, .day, .hour, .minute, .second, .nanosecond]
let diff = calendar.dateComponents(comps, from: d1, to: d2)
print(diff)
print("difference in years:", diff.year!)

      

Output

d1: 2017-01-01 01: 00: 00.456
d2: 1997-01-01 01: 00: 00.456
year: -19 month: -11 day: -30 hour: -23 minute: -59 second: -59 nanosecond: -999999756 isLeapMonth: false 
difference in years: -19


Due to rounding errors ( Date

uses a binary floating point number as internal representation), the difference is calculated as a tiny bit less than 20 years old, and the year difference looks like -19 instead of the expected -20.

As a workaround, you can round the dates to full seconds, which seems to fix the problem:

    let baseDate = Date(timeIntervalSinceReferenceDate: baseDate
        .timeIntervalSinceReferenceDate.rounded())
    let earlierDate = Date(timeIntervalSinceReferenceDate: earlierDate
        .timeIntervalSinceReferenceDate.rounded())

      

You may also want to consider posting a bug report to Apple.

+1


source


I debugged a bit and found that sometimes it timeDifference

goes offline for 1 day.

What I did, I put this line after initialization timeDifference

:

print("\(timeDifference.year!) \(timeDifference.month!) \(timeDifference.day!)")

      

The expected output was something like this

-2 0 0
-3 0 0
-4 0 0
-5 0 0
-6 0 0
-7 0 0
...

      

However, the actual output contains something like:



-38 0 0
-38 -11 -30
-39 -11 -30
-40 -11 -30
...
-55 -11 -30
-57 0 0

      

Apparently, after a few years month

, day

they become -11

and -30

accordingly.

How do you fix this?

Unfortunately, I cannot find the root cause of this problem. However, I came up with a brute force solution:

if timeDifference.year! < 0 {
    if timeDifference.year! == -1 {
        return "Last year"
    } else {
        if timeDifference.month == -11 && timeDifference.day == -30 {
            return "\(abs(timeDifference.year!) + 1) years ago"
        } else {
            return "\(abs(timeDifference.year!)) years ago"
        }
    }
}

      

I check if the time difference is disabled by 1 day. If so, add 1 to abs year

.

+1


source







All Articles