将第二个添加到日期的结果是一分钟;解决方法

Result of adding second to date is one minute off; workaround

我正在向 Foundation 的日期实例添加一秒,但结果偏差了整整一分钟。

var calendar = Calendar(identifier: .iso8601)
calendar.locale = Locale(identifier: "en")
calendar.timeZone = TimeZone(identifier: "GMT")!

let date1 = Date(timeIntervalSinceReferenceDate: -62544967141.9)
let date2 = calendar.date(byAdding: DateComponents(second: 1),
                          to: date1,
                          wrappingComponents: true)!

ISO8601DateFormatter().string(from: date1) // => 0019-01-11T22:00:58Z
ISO8601DateFormatter().string(from: date2) // => 0019-01-11T21:59:59Z

有趣的是,以下其中一项会使错误消失:

我的代码中不需要亚秒级精度,所以我创建了这个允许我放弃它的扩展。

extension Date {
  func roundedToSeconds() -> Date {
    return Date(timeIntervalSinceReferenceDate: round(timeIntervalSinceReferenceDate))
  }
}

我想知道这个:

Why does this error happen?

我会说这是 Core Foundation (CF) 中的错误。

Calendar.date(byAdding:to:wrappingComponents:) 向下调用内部 Core Foundation 函数 _CFCalendarAddComponentsV, which in turn uses the ICU Calendar C API。 ICU 将时间表示为自 Unix 纪元以来的浮点毫秒数,而 CF 使用自 NeXT 参考日期以来的浮点秒数。所以CF在调用ICU之前必须先把它的representation转换成ICU的representation,再转换回return结果给你

以下是它如何从 CF 时间戳转换为 ICU 时间戳:

    double startingInt;
    double startingFrac = modf(*atp, &startingInt);
    UDate udate = (startingInt + kCFAbsoluteTimeIntervalSince1970) * 1000.0;

modf 函数将浮点数拆分为整数和小数部分。让我们插入您的示例日期:

var startingInt: Double = 0
var startingFrac: Double = modf(date1.timeIntervalSinceReferenceDate, &startingInt)
print(startingInt, startingFrac)

// Output:
-62544967141.0 -0.9000015258789062

接下来,CF调用__CFCalendarAdd将-62544967141加一秒。请注意,-62544967141 位于一分钟间隔 -62544967200 ..< -62544967140.0 内。所以当CF在-62544967141上加一秒时,它得到-62544967140,这将在下一轮一分钟间隔内。由于您指定了换行组件,因此不允许 CF 更改日期的分钟部分,因此它换行到原始循环一分钟间隔的开始,-62544967200。

最后,CF将ICU时间转换回CF时间,加上原始时间的小数部分:

    *atp = (udate / 1000.0) - kCFAbsoluteTimeIntervalSince1970 + startingFrac + (nanosecond * 1.0e-9);

所以returns -62544967200 + -0.9000015258789062 = -62544967200.9,正好比输入时间早59秒

Am I doing something wrong?

不,错误在 CF 中,不在您的代码中。

Is there any issue with my workaround?

如果您不需要亚秒级精度,您的解决方法应该没问题。

I can reproduce it with more recent dates but so far only with negative reference dates, e.g. Date(timeIntervalSinceReferenceDate: -1008899941.9), which is 1969-01-11T22:00:58Z.

在一分钟间隔的最后一秒出现任何负数 timeIntervalSinceReferenceDate 应该会导致问题。该错误有效地使时间 0 之前的第一轮整分钟从 -60.99999999999999 到 -1.0,但它应该从 -60.0 到 -5e324。所有更负的圆分钟间隔都类似地偏移。