Skip to content

Commit 3aea22c

Browse files
committed
Add comment explaining rationale for DoubleDouble storage on Date
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
1 parent 70a7c54 commit 3aea22c

File tree

3 files changed

+116
-18
lines changed

3 files changed

+116
-18
lines changed

Sources/FoundationEssentials/Date.swift

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,64 @@ public typealias TimeInterval = Double
3434
A `Date` is independent of a particular calendar or time zone. To represent a `Date` to a user, you must interpret it in the context of a `Calendar`.
3535
*/
3636
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
37-
public struct Date : Comparable, Hashable, Equatable, Sendable {
38-
37+
public struct Date: Comparable, Hashable, Equatable, Sendable {
38+
/* Date is internally represented as a sum of two Doubles.
39+
40+
Previously Date was backed by a single Double measuring time since
41+
Jan 1 2001 in seconds. Because Double's precision is non-uniform, this
42+
means that times within a hundred days of the epoch are represented
43+
with approximately nanosecond precision, but as you get farther away
44+
from that date the precision decreases. For times close to the time
45+
at which this comment was written, accuracy has been reduced to about
46+
100ns.
47+
48+
The obvious thing would be to adopt an integer-based representation
49+
similar to C's timespec (32b nanoseconds, 64b seconds) or Swift's
50+
Duration (128b attoseconds). These representations suffer from a few
51+
difficulties:
52+
53+
- Existing API on Date takes and produces `TimeInterval` (aka Double).
54+
Making Date use an integer representation internally would mean that
55+
existing users of the public API would suddently start getting
56+
different results for computations that were previously exact; even
57+
though we could add new API, and the overall system would be more
58+
precise, this would be a surprisingly subtle change for users to
59+
navigate.
60+
61+
- We have been told that some software interprets the raw bytes of Date
62+
as a Double for the purposes of fast serialization. These packages
63+
formally violate Foundation's API boundaries, but that doesn't help
64+
users of those packages who would abruptly be broken by switching to
65+
an integer representation.
66+
67+
Using DoubleDouble instead navigates these problems fairly elegantly.
68+
69+
- Because DoubleDouble is still a floating-point type, it still suffers
70+
from non-uniform precision. However, because DoubleDouble is so
71+
fantastically precise, it can represent dates out to ±2.5 quadrillion
72+
years at ~nanosecond or better precision, so in practice this won't
73+
be much of an issue.
74+
75+
- Existing API on Date will produce exactly the same result as it did
76+
previously in cases where those results were exact, minimizing
77+
surprises. In cases where the existing API was not exact, it will
78+
produce much more accurate results, even if users do not adopt new
79+
API, because its internal calculations are now more precise.
80+
81+
- Software that (incorrectly) interprets the raw bytes of Date as a
82+
Double will get at least as accurate of a value as it did previously
83+
(and often a more accurate value). */
3984
internal var _time: DoubleDouble
40-
85+
4186
internal init(_ time: DoubleDouble) {
42-
self._time = time
87+
self._time = time
4388
}
89+
}
4490

91+
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
92+
extension Date {
4593
/// The number of seconds from 1 January 1970 to the reference date, 1 January 2001.
46-
public static let timeIntervalBetween1970AndReferenceDate : TimeInterval = 978307200.0
94+
public static let timeIntervalBetween1970AndReferenceDate: TimeInterval = 978307200.0
4795

4896
/// The number of seconds from 1 January 1601 to the reference date, 1 January 2001.
4997
internal static let timeIntervalBetween1601AndReferenceDate: TimeInterval = 12622780800.0
@@ -55,7 +103,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
55103

56104
/// Returns a `Date` initialized to the current date and time.
57105
public init() {
58-
_time = .init(head: Self.getCurrentAbsoluteTime(), tail: 0)
106+
_time = .init(uncheckedHead: Self.getCurrentAbsoluteTime(), tail: 0)
59107
}
60108

61109
/// Returns a `Date` initialized relative to the current date and time by a given number of seconds.
@@ -86,7 +134,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
86134

87135
/// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 2001 by a given number of seconds.
88136
public init(timeIntervalSinceReferenceDate ti: TimeInterval) {
89-
_time = .init(head: ti, tail: 0)
137+
_time = .init(uncheckedHead: ti, tail: 0)
90138
}
91139

92140
/**

Sources/FoundationEssentials/DateInterval.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable {
4242
}
4343
set {
4444
precondition(newValue >= 0, "Negative durations are not allowed")
45-
_duration = DoubleDouble(head: newValue, tail: 0)
45+
_duration = DoubleDouble(uncheckedHead: newValue, tail: 0)
4646
}
4747
}
4848

@@ -68,7 +68,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable {
6868
public init(start: Date, duration: TimeInterval) {
6969
precondition(duration >= 0, "Negative durations are not allowed")
7070
self.start = start
71-
_duration = DoubleDouble(head: duration, tail: 0)
71+
_duration = DoubleDouble(uncheckedHead: duration, tail: 0)
7272
}
7373

7474
/**

Sources/FoundationEssentials/DoubleDouble.swift

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,85 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
/// A numeric type that uses two Double values as its representation, providing
14+
/// about 106 bits of precision with the same exponent range as Double.
15+
///
16+
/// This type conforms to AdditiveArithmetic, Hashable and Comparable, but does
17+
/// not conform to FloatingPoint or Numeric; it implements only the API surface
18+
/// that is necessary to serve as an internal implementation detail of Date.
1319
internal struct DoubleDouble {
1420

1521
private let storage: (Double, Double)
1622

23+
/// A double-double value constructed by specifying the head and tail.
24+
///
25+
/// This is an unchecked operation because it does not enforce the
26+
/// invariant that head + tail == head in release builds, which is
27+
/// necessary for subsequent arithmetic operations to behave correctly.
1728
@_transparent
18-
init(head: Double, tail: Double) {
29+
init(uncheckedHead head: Double, tail: Double) {
30+
assert(!head.isFinite || head + tail == head)
1931
storage = (head, tail)
2032
}
2133

34+
/// The high-order Double.
35+
///
36+
/// This property does not have a setter because `head` should pretty much
37+
/// never be set independently of `tail`, so as to maintain the invariant
38+
/// that `head + tail == head`. You can use `init(uncheckedHead:tail:)`
39+
/// to directly construct DoubleDouble values, which will enforce the
40+
/// invariant in debug builds.
2241
@_transparent
2342
var head: Double { storage.0 }
2443

44+
/// The low-order Double.
45+
///
46+
/// This property does not have a setter because `tail` should pretty much
47+
/// never be set independently of `head`, so as to maintain the invariant
48+
/// that `head + tail == head`. You can use `init(uncheckedHead:tail:)`
49+
/// to directly construct DoubleDouble values, which will enforce the
50+
/// invariant in debug builds.
2551
@_transparent
2652
var tail: Double { storage.1 }
2753

54+
/// `a + b` represented as a normalized DoubleDouble.
55+
///
56+
/// Computed via the [2Sum algorithm](https://en.wikipedia.org/wiki/2Sum).
2857
@inlinable
2958
static func sum(_ a: Double, _ b: Double) -> DoubleDouble {
3059
let head = a + b
3160
let x = head - b
3261
let y = head - x
3362
let tail = (a - x) + (b - y)
34-
return DoubleDouble(head: head, tail: tail)
63+
return DoubleDouble(uncheckedHead: head, tail: tail)
3564
}
3665

66+
/// `a + b` represented as a normalized DoubleDouble.
67+
///
68+
/// Computed via the [Fast2Sum algorithm](https://en.wikipedia.org/wiki/2Sum).
69+
///
70+
/// - Precondition:
71+
/// `large` and `small` must be such that `sum(large:small:)`
72+
/// produces the same result as `sum(_:_:)` would. A sufficient condition
73+
/// is that `|large| >= |small|`, but this is not necessary, so we do not
74+
/// enforce it via an assert. Instead this function asserts that the result
75+
/// is the same as that produced by `sum(_:_:)` in Debug builds. This is
76+
/// unchecked in Release.
3777
@inlinable
3878
static func sum(large a: Double, small b: Double) -> DoubleDouble {
3979
let head = a + b
4080
let tail = a - head + b
41-
return DoubleDouble(head: head, tail: tail)
81+
let result = DoubleDouble(uncheckedHead: head, tail: tail)
82+
assert(!head.isFinite || result == sum(a, b))
83+
return result
4284
}
4385

86+
/// `a * b` represented as a normalized DoubleDouble.
4487
@inlinable
4588
static func product(_ a: Double, _ b: Double) -> DoubleDouble {
4689
let head = a * b
4790
let tail = (-head).addingProduct(a, b)
48-
return DoubleDouble(head: head, tail: tail)
91+
return DoubleDouble(uncheckedHead: head, tail: tail)
4992
}
5093
}
5194

@@ -72,7 +115,7 @@ extension DoubleDouble: Hashable {
72115
extension DoubleDouble: AdditiveArithmetic {
73116
@inlinable
74117
static var zero: DoubleDouble {
75-
Self(head: 0, tail: 0)
118+
Self(uncheckedHead: 0, tail: 0)
76119
}
77120

78121
@inlinable
@@ -83,6 +126,8 @@ extension DoubleDouble: AdditiveArithmetic {
83126
return sum(large: first.head, small: first.tail + tails.tail)
84127
}
85128

129+
/// Equivalent to `a + DoubleDouble(uncheckedHead: b, tail: 0)` but
130+
/// computed more efficiently.
86131
@inlinable
87132
static func +(a: DoubleDouble, b: Double) -> DoubleDouble {
88133
let heads = sum(a.head, b)
@@ -92,14 +137,16 @@ extension DoubleDouble: AdditiveArithmetic {
92137

93138
@inlinable
94139
prefix static func -(a: DoubleDouble) -> DoubleDouble {
95-
DoubleDouble(head: -a.head, tail: -a.tail)
140+
DoubleDouble(uncheckedHead: -a.head, tail: -a.tail)
96141
}
97142

98143
@inlinable
99144
static func -(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble {
100145
a + (-b)
101146
}
102147

148+
/// Equivalent to `a - DoubleDouble(uncheckedHead: b, tail: 0)` but
149+
/// computed more efficiently.
103150
@inlinable
104151
static func -(a: DoubleDouble, b: Double) -> DoubleDouble {
105152
a + (-b)
@@ -111,7 +158,7 @@ extension DoubleDouble {
111158
static func *(a: DoubleDouble, b: Double) -> DoubleDouble {
112159
let tmp = product(a.head, b)
113160
return DoubleDouble(
114-
head: tmp.head,
161+
uncheckedHead: tmp.head,
115162
tail: tmp.tail.addingProduct(a.tail, b)
116163
)
117164
}
@@ -120,17 +167,20 @@ extension DoubleDouble {
120167
static func /(a: DoubleDouble, b: Double) -> DoubleDouble {
121168
let head = a.head/b
122169
let residual = a.head.addingProduct(-head, b) + a.tail
123-
return DoubleDouble(head: head, tail: residual/b)
170+
return .sum(large: head, small: residual/b)
124171
}
125172
}
126173

127174
extension DoubleDouble {
175+
// This value rounded down to an integer.
128176
@inlinable
129177
func floor() -> DoubleDouble {
130178
let approx = head.rounded(.down)
179+
// If head was already an integer, round tail down and renormalize.
131180
if approx == head {
132181
return .sum(large: head, small: tail.rounded(.down))
133182
}
134-
return DoubleDouble(head: approx, tail: 0)
183+
// Head was not an integer; we can simply discard tail.
184+
return DoubleDouble(uncheckedHead: approx, tail: 0)
135185
}
136186
}

0 commit comments

Comments
 (0)