StatusReport
StatusReport

Reputation: 3427

Why NSDate's dateWithTimeIntervalSince1970 modifies its input?

I'm running the following code:

for (int i = 0; i < 100; ++i) {
  NSDate *date = [NSDate date];
  NSTimeInterval interval = date.timeIntervalSince1970;
  NSDate *newDate = [NSDate dateWithTimeIntervalSince1970:interval];

  if (![date isEqualToDate:newDate]) {
    NSLog(@"Not equal!");
  }
}

Surprisingly, in many iterations the dates are not equal to each other. How can that be?

Upvotes: 2

Views: 422

Answers (2)

StatusReport
StatusReport

Reputation: 3427

Disassembling dateWithTimeIntervalSince1970: shows that it calls -initWithIntervalSinceReferenceDate::

CoreFoundation`+[NSDate dateWithTimeIntervalSince1970:]:
    0x10fd39430 <+0>:  pushq  %rbp
    0x10fd39431 <+1>:  movq   %rsp, %rbp
    0x10fd39434 <+4>:  pushq  %rbx
    0x10fd39435 <+5>:  pushq  %rax
    0x10fd39436 <+6>:  movsd  %xmm0, -0x10(%rbp)
    0x10fd3943b <+11>: movq   0x2d8146(%rip), %rsi      ; "alloc"
    0x10fd39442 <+18>: movq   0x29edc7(%rip), %rbx      ; (void *)0x000000010f35e940: objc_msgSend
    0x10fd39449 <+25>: callq  *%rbx
    0x10fd3944b <+27>: movsd  -0x10(%rbp), %xmm0        ; xmm0 = mem[0],zero 
    0x10fd39450 <+32>: addsd  0x1f9d58(%rip), %xmm0     ; _CFLog_os_trace_type_map + 16
    0x10fd39458 <+40>: movq   0x2d9041(%rip), %rsi      ; "initWithTimeIntervalSinceReferenceDate:"
    0x10fd3945f <+47>: movq   %rax, %rdi
    0x10fd39462 <+50>: callq  *%rbx
    0x10fd39464 <+52>: movq   0x2d81b5(%rip), %rsi      ; "autorelease"
    0x10fd3946b <+59>: movq   %rax, %rdi
    0x10fd3946e <+62>: movq   %rbx, %rax
    0x10fd39471 <+65>: addq   $0x8, %rsp
    0x10fd39475 <+69>: popq   %rbx
    0x10fd39476 <+70>: popq   %rbp
    0x10fd39477 <+71>: jmpq   *%rax
    0x10fd39479 <+73>: nopl   (%rax)

Additionally, the input to the initializer is secs - 978307200 (or, secs - NSTimeIntervalSince1970), which is the time difference between 1970 and the reference date. This computation, while subtracting an integer (double) from a double can change the fraction of the input value due to rounding errors. For example, here's a date that failed the test:

date: Sun Mar 25 17:54:39 2018 (543682479.8504179716),
newDate: Sun Mar 25 17:54:39 2018 (543682479.8504180908)

Since log2(543682479.8504179716 + NSTimeIntervalSince1970) ~ 30.5 and log2(543682479.8504179716) ~ 29.01, the exponent of the double value needs to be adjusted and the mantissa needs to be normalized, which may affect the fractional value.

The solution is to use the +dateWithTimeIntervalSinceReferenceDate: factory method instead, which directly initializes an NSDate without an additional computation.

Upvotes: 5

Amin Negm-Awad
Amin Negm-Awad

Reputation: 16660

The methods referring to 1970 are used for BSD-style date intervals since 1970. There is a resolution defined, IIRC milliseconds. Therefore the NSTimeValue value, is rounded intentionally to that specs. You cannot expect the value transformed and retransformed to be the same value because of this rounding.

Only use these methods, when you get a BSD time. This is documented:

This method is useful for creating NSDate objects from time_t values returned by BSD system functions.

https://developer.apple.com/documentation/foundation/nsdate/1591576-datewithtimeintervalsince1970

However, as said by StatusReport, dates are floating point numbers. Testing for equality is always dangerous. I. e. when you store a date with Core Data it is "rounded" to the resolution of the SQL column.

Upvotes: 0

Related Questions