-
Notifications
You must be signed in to change notification settings - Fork 205
Double-double Date #1533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Double-double Date #1533
Conversation
Adapts Date to use a double-Double representation (the underlying representation is the sum of two double-precision values, giving about 106 bits of precision). Keeping a binary floating-point representation means that existing API that converts Date to/from Double values will not see a difference in behavior for simple cases (it will, however, become more accurate in some other cases). Every date computation that was previously exact is still exact. Many cases that previously might have rounded will now generate exact results. DateInterval gets the same treatment; both its start Date and duration are represented as double-Double quantities with this change. If we move forward with this change, we'll investigate adding new APIs to make it more convenient to specify high-precision Dates; this change merely makes it possible to do so.
Converted to draft as I want to add some additional comments and tests before this moves forward, but the basic shape of the change is correct, I think. |
@swift-ci test |
Previously these were disabled because of an API availability issue in 5.8/5.9; we're no longer developing for those targets, so let's re-enable these tests.
Also add some other documentation comments on the internal DoubleDouble type and rename init(head:tail:) to init(uncheckedHead:tail:) to better document that its precondition is not enforced in Release builds
@swift-ci test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
self.start = start | ||
// round start down to whole seconds, set aside fraction. | ||
let wholeSeconds = start._time.floor() | ||
fractionalSeconds = (start._time - wholeSeconds).head |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note for myself: we should/could probably add a helper method somewhere to decompose the floating point second into the whole number and the fractional part. IIRC we have quite a few places where we're doing it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a note to do this as part of the follow-on work.
case duration = "duration" | ||
} | ||
|
||
public init(from decoder: Decoder) throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is effectively identical to the current auto-generated implementation, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the idea!
Adapts Date to use a double-Double representation (the underlying representation is the sum of two double-precision values, giving about 106 bits of precision).
Previously Date was backed by a single Double measuring time since Jan 1 2001 in seconds. Because Double's precision is non-uniform, this means that times within a hundred days of the epoch are represented with approximately nanosecond precision, but as you get farther away from that date the precision decreases. For times close to today, it has been reduced to about 100ns.
The obvious thing would be to adopt an integer-based representation similar to C's timespec (32b nanoseconds, 64b seconds) or Swift's Duration (128b attoseconds). These representations suffer from a few difficulties:
Existing API on Date takes and produces
TimeInterval
(aka Double). Making Date use an integer representation internally would mean that existing users of the public API would suddently start getting different results for computations that were previously exact; even though we could add new API, and the overall system would be more precise, this would be a surprisingly subtle change for users to navigate.We have been told that some software interprets the raw bytes of Date as a Double for the purposes of fast serialization. These packages formally violate Foundation's API boundaries, but that doesn't help users of those packages who would abruptly be broken by switching to an integer representation.
Using DoubleDouble instead navigates these problems fairly elegantly.
Because DoubleDouble is still a floating-point type, it still suffers from non-uniform precision. However, because DoubleDouble is so fantastically precise, it can represent dates out to ±2.5 quadrillion years at ~nanosecond or better precision, so in practice this won't be much of an issue.
Existing API on Date will produce exactly the same result as it did previously in cases where those results were exact, minimizing surprises. In cases where the existing API was not exact, it will produce much more accurate results, even if users do not adopt new API, because its internal calculations are now more precise.
Software that (incorrectly) interprets the raw bytes of Date as a Double will get at least as accurate of a value as it did previously (and often a more accurate value).
DateInterval gets the same treatment, so that it can benefit from more accurate internal computations as well. Follow-on work may adapt/add-to Date's API to make it easier to specify Dates with high precision.