Skip to content

Commit a5e81fa

Browse files
committed
Double-double Date
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.
1 parent 57b6c0c commit a5e81fa

File tree

8 files changed

+288
-89
lines changed

8 files changed

+288
-89
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,30 +1448,39 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
14481448
}
14491449

14501450
func dateInterval(of component: Calendar.Component, for date: Date) -> DateInterval? {
1451-
let time = date.timeIntervalSinceReferenceDate
1451+
let approximateTime = date._time.head
14521452
var effectiveUnit = component
14531453
switch effectiveUnit {
14541454
case .calendar, .timeZone, .isLeapMonth, .isRepeatedDay:
14551455
return nil
14561456
case .era:
1457-
if time < -63113904000.0 {
1457+
if approximateTime < -63113904000.0 {
14581458
return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0 - inf_ti), duration: inf_ti)
14591459
} else {
14601460
return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0), duration: inf_ti)
14611461
}
14621462

14631463
case .hour:
1464-
let ti = Double(timeZone.secondsFromGMT(for: date))
1465-
var fixedTime = time + ti // compute local time
1466-
fixedTime = floor(fixedTime / 3600.0) * 3600.0
1467-
fixedTime = fixedTime - ti // compute GMT
1468-
return DateInterval(start: Date(timeIntervalSinceReferenceDate: fixedTime), duration: 3600.0)
1464+
// Local hours may not be aligned to GMT hours, so we have to apply
1465+
// the time zone adjustment before rounding down, then unapply it.
1466+
let offset = Double(timeZone.secondsFromGMT(for: date))
1467+
let start = ((date._time + offset)/3600).floor() * 3600 - offset
1468+
return DateInterval(
1469+
start: Date(start),
1470+
duration: 3600
1471+
)
14691472
case .minute:
1470-
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time / 60.0) * 60.0), duration: 60.0)
1473+
return DateInterval(
1474+
start: Date((date._time/60).floor() * 60),
1475+
duration: 60
1476+
)
14711477
case .second:
1472-
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time)), duration: 1.0)
1478+
return DateInterval(start: Date(date._time.floor()), duration: 1)
14731479
case .nanosecond:
1474-
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time * 1.0e+9) * 1.0e-9), duration: 1.0e-9)
1480+
return DateInterval(
1481+
start: Date((date._time*1e9).floor() / 1e9),
1482+
duration: 1e-9
1483+
)
14751484
case .year, .yearForWeekOfYear, .quarter, .month, .day, .dayOfYear, .weekOfMonth, .weekOfYear:
14761485
// Continue to below
14771486
break

Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ extension Calendar {
9393
/// value is used as a lower bound for ``nextBaseRecurrenceDate()``.
9494
let rangeLowerBound: Date?
9595

96-
/// The start date's nanoseconds component
97-
let startDateNanoseconds: TimeInterval
96+
/// The start date's fractional seconds component
97+
let fractionalSeconds: TimeInterval
9898

9999
/// How many occurrences have been found so far
100100
var resultsFound = 0
@@ -131,7 +131,10 @@ extension Calendar {
131131
}
132132
self.recurrence = recurrence
133133

134-
self.start = start
134+
// round start down to whole seconds, set aside fraction.
135+
let wholeSeconds = start._time.floor()
136+
fractionalSeconds = (start._time - wholeSeconds).head
137+
self.start = Date(wholeSeconds)
135138
self.range = range
136139

137140
let frequency = recurrence.frequency
@@ -233,9 +236,7 @@ extension Calendar {
233236
case .monthly: [.second, .minute, .hour, .day]
234237
case .yearly: [.second, .minute, .hour, .day, .month, .isLeapMonth]
235238
}
236-
var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start)
237-
238-
startDateNanoseconds = start.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 1)
239+
var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start)
239240

240241
let expansionChangesDay = dayOfYearAction == .expand || dayOfMonthAction == .expand || weekAction == .expand || weekdayAction == .expand
241242
let expansionChangesMonth = dayOfYearAction == .expand || monthAction == .expand || weekAction == .expand
@@ -427,11 +428,11 @@ extension Calendar {
427428
recurrence._limitTimeComponent(.second, dates: &dates, anchor: anchor)
428429
}
429430

430-
if startDateNanoseconds > 0 {
431+
if fractionalSeconds != 0 {
431432
// `_dates(startingAfter:)` above returns whole-second dates,
432433
// so we need to restore the nanoseconds value present in the original start date.
433434
for idx in dates.indices {
434-
dates[idx] += startDateNanoseconds
435+
dates[idx] += fractionalSeconds
435436
}
436437
}
437438
dates = dates.filter { $0 >= self.start }

Sources/FoundationEssentials/Date.swift

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ public typealias TimeInterval = Double
3636
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
3737
public struct Date : Comparable, Hashable, Equatable, Sendable {
3838

39-
internal var _time : TimeInterval
39+
internal var _time: DoubleDouble
40+
41+
internal init(_ time: DoubleDouble) {
42+
self._time = time
43+
}
4044

4145
/// The number of seconds from 1 January 1970 to the reference date, 1 January 2001.
4246
public static let timeIntervalBetween1970AndReferenceDate : TimeInterval = 978307200.0
@@ -51,17 +55,23 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
5155

5256
/// Returns a `Date` initialized to the current date and time.
5357
public init() {
54-
_time = Self.getCurrentAbsoluteTime()
58+
_time = .init(head: Self.getCurrentAbsoluteTime(), tail: 0)
5559
}
5660

5761
/// Returns a `Date` initialized relative to the current date and time by a given number of seconds.
5862
public init(timeIntervalSinceNow: TimeInterval) {
59-
self.init(timeIntervalSinceReferenceDate: timeIntervalSinceNow + Self.getCurrentAbsoluteTime())
63+
self.init(.sum(
64+
Self.getCurrentAbsoluteTime(),
65+
timeIntervalSinceNow
66+
))
6067
}
6168

6269
/// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 1970 by a given number of seconds.
6370
public init(timeIntervalSince1970: TimeInterval) {
64-
self.init(timeIntervalSinceReferenceDate: timeIntervalSince1970 - Date.timeIntervalBetween1970AndReferenceDate)
71+
self.init(.sum(
72+
timeIntervalSince1970,
73+
-Date.timeIntervalBetween1970AndReferenceDate
74+
))
6575
}
6676

6777
/**
@@ -71,12 +81,12 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
7181
- Parameter date: The reference date.
7282
*/
7383
public init(timeInterval: TimeInterval, since date: Date) {
74-
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate + timeInterval)
84+
self.init(date._time + timeInterval)
7585
}
7686

7787
/// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 2001 by a given number of seconds.
7888
public init(timeIntervalSinceReferenceDate ti: TimeInterval) {
79-
_time = ti
89+
_time = .init(head: ti, tail: 0)
8090
}
8191

8292
/**
@@ -85,7 +95,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
8595
This property's value is negative if the date object is earlier than the system's absolute reference date (00:00:00 UTC on 1 January 2001).
8696
*/
8797
public var timeIntervalSinceReferenceDate: TimeInterval {
88-
return _time
98+
return _time.head
8999
}
90100

91101
/**
@@ -100,7 +110,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
100110
- SeeAlso: `timeIntervalSinceReferenceDate`
101111
*/
102112
public func timeIntervalSince(_ date: Date) -> TimeInterval {
103-
return self.timeIntervalSinceReferenceDate - date.timeIntervalSinceReferenceDate
113+
return (self._time - date._time).head
104114
}
105115

106116
/**
@@ -173,9 +183,9 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
173183

174184
/// Compare two `Date` values.
175185
public func compare(_ other: Date) -> ComparisonResult {
176-
if _time < other.timeIntervalSinceReferenceDate {
186+
if _time < other._time {
177187
return .orderedAscending
178-
} else if _time > other.timeIntervalSinceReferenceDate {
188+
} else if _time > other._time {
179189
return .orderedDescending
180190
} else {
181191
return .orderedSame
@@ -184,27 +194,27 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
184194

185195
/// Returns true if the two `Date` values represent the same point in time.
186196
public static func ==(lhs: Date, rhs: Date) -> Bool {
187-
return lhs.timeIntervalSinceReferenceDate == rhs.timeIntervalSinceReferenceDate
197+
return lhs._time == rhs._time
188198
}
189199

190200
/// Returns true if the left hand `Date` is earlier in time than the right hand `Date`.
191201
public static func <(lhs: Date, rhs: Date) -> Bool {
192-
return lhs.timeIntervalSinceReferenceDate < rhs.timeIntervalSinceReferenceDate
202+
return lhs._time < rhs._time
193203
}
194204

195205
/// Returns true if the left hand `Date` is later in time than the right hand `Date`.
196206
public static func >(lhs: Date, rhs: Date) -> Bool {
197-
return lhs.timeIntervalSinceReferenceDate > rhs.timeIntervalSinceReferenceDate
207+
return lhs._time > rhs._time
198208
}
199209

200210
/// Returns a `Date` with a specified amount of time added to it.
201211
public static func +(lhs: Date, rhs: TimeInterval) -> Date {
202-
return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate + rhs)
212+
return Date(lhs._time + rhs)
203213
}
204214

205215
/// Returns a `Date` with a specified amount of time subtracted from it.
206216
public static func -(lhs: Date, rhs: TimeInterval) -> Date {
207-
return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate - rhs)
217+
return Date(lhs._time - rhs)
208218
}
209219

210220
/// Add a `TimeInterval` to a `Date`.
@@ -220,7 +230,6 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
220230
public static func -=(lhs: inout Date, rhs: TimeInterval) {
221231
lhs = lhs - rhs
222232
}
223-
224233
}
225234

226235
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)

Sources/FoundationEssentials/DateInterval.swift

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,45 @@
1212

1313
/// DateInterval represents a closed date interval in the form of [startDate, endDate]. It is possible for the start and end dates to be the same with a duration of 0. DateInterval does not support reverse intervals i.e. intervals where the duration is less than 0 and the end date occurs earlier in time than the start date.
1414
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
15-
public struct DateInterval : Comparable, Hashable, Codable, Sendable {
15+
public struct DateInterval: Comparable, Hashable, Codable, Sendable {
1616

1717
/// The start date.
18-
public var start : Date
18+
public var start: Date
19+
20+
/// Underlying storage for `duration`
21+
internal var _duration: DoubleDouble
1922

2023
/// The end date.
2124
///
2225
/// - precondition: `end >= start`
23-
public var end : Date {
26+
public var end: Date {
2427
get {
25-
return start + duration
28+
return Date(start._time + _duration)
2629
}
2730
set {
2831
precondition(newValue >= start, "Reverse intervals are not allowed")
29-
duration = newValue.timeIntervalSinceReferenceDate - start.timeIntervalSinceReferenceDate
32+
_duration = (newValue._time - start._time)
3033
}
3134
}
32-
33-
/// The duration.
35+
36+
/// The duration
3437
///
3538
/// - precondition: `duration >= 0`
36-
public var duration : TimeInterval {
37-
willSet {
39+
public var duration: TimeInterval {
40+
get {
41+
_duration.head
42+
}
43+
set {
3844
precondition(newValue >= 0, "Negative durations are not allowed")
45+
_duration = DoubleDouble(head: newValue, tail: 0)
3946
}
4047
}
4148

4249
/// Initializes a `DateInterval` with start and end dates set to the current date and the duration set to `0`.
4350
public init() {
4451
let d = Date()
4552
start = d
46-
duration = 0
53+
_duration = .zero
4754
}
4855

4956
/// Initialize a `DateInterval` with the specified start and end date.
@@ -52,7 +59,7 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable {
5259
public init(start: Date, end: Date) {
5360
precondition(end >= start, "Reverse intervals are not allowed")
5461
self.start = start
55-
duration = end.timeIntervalSince(start)
62+
_duration = end._time - start._time
5663
}
5764

5865
/// Initialize a `DateInterval` with the specified start date and duration.
@@ -61,7 +68,7 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable {
6168
public init(start: Date, duration: TimeInterval) {
6269
precondition(duration >= 0, "Negative durations are not allowed")
6370
self.start = start
64-
self.duration = duration
71+
_duration = DoubleDouble(head: duration, tail: 0)
6572
}
6673

6774
/**
@@ -162,6 +169,24 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable {
162169
public static func <(lhs: DateInterval, rhs: DateInterval) -> Bool {
163170
return lhs.compare(rhs) == .orderedAscending
164171
}
172+
173+
enum CodingKeys: String, CodingKey {
174+
case start = "start"
175+
case duration = "duration"
176+
}
177+
178+
public init(from decoder: Decoder) throws {
179+
let container = try decoder.container(keyedBy: CodingKeys.self)
180+
let start = try container.decode(Date.self, forKey: .start)
181+
let duration = try container.decode(TimeInterval.self, forKey: .duration)
182+
self.init(start: start, duration: duration)
183+
}
184+
185+
public func encode(to encoder: Encoder) throws {
186+
var container = encoder.container(keyedBy: CodingKeys.self)
187+
try container.encode(start, forKey: .start)
188+
try container.encode(duration, forKey: .duration)
189+
}
165190
}
166191

167192
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)

0 commit comments

Comments
 (0)