Skip to content

Commit 5ef0061

Browse files
authored
Merge pull request #1542 from swiftlang/automerge/merge-main-2025-10-11_09-03
Merge `main` into `future`
2 parents 344f413 + 7566cd4 commit 5ef0061

File tree

10 files changed

+391
-96
lines changed

10 files changed

+391
-96
lines changed

Sources/FoundationEssentials/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ add_library(FoundationEssentials
2121
ComparisonResult.swift
2222
Date.swift
2323
DateInterval.swift
24+
DoubleDouble.swift
2425
FoundationEssentials.swift
2526
IndexPath.swift
2627
LockedState.swift

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: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +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-
39-
internal var _time : TimeInterval
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). */
84+
internal var _time: DoubleDouble
85+
86+
internal init(_ time: DoubleDouble) {
87+
self._time = time
88+
}
89+
}
4090

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

4496
/// The number of seconds from 1 January 1601 to the reference date, 1 January 2001.
4597
internal static let timeIntervalBetween1601AndReferenceDate: TimeInterval = 12622780800.0
@@ -51,17 +103,23 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
51103

52104
/// Returns a `Date` initialized to the current date and time.
53105
public init() {
54-
_time = Self.getCurrentAbsoluteTime()
106+
_time = .init(uncheckedHead: Self.getCurrentAbsoluteTime(), tail: 0)
55107
}
56108

57109
/// Returns a `Date` initialized relative to the current date and time by a given number of seconds.
58110
public init(timeIntervalSinceNow: TimeInterval) {
59-
self.init(timeIntervalSinceReferenceDate: timeIntervalSinceNow + Self.getCurrentAbsoluteTime())
111+
self.init(.sum(
112+
Self.getCurrentAbsoluteTime(),
113+
timeIntervalSinceNow
114+
))
60115
}
61116

62117
/// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 1970 by a given number of seconds.
63118
public init(timeIntervalSince1970: TimeInterval) {
64-
self.init(timeIntervalSinceReferenceDate: timeIntervalSince1970 - Date.timeIntervalBetween1970AndReferenceDate)
119+
self.init(.sum(
120+
timeIntervalSince1970,
121+
-Date.timeIntervalBetween1970AndReferenceDate
122+
))
65123
}
66124

67125
/**
@@ -71,12 +129,12 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
71129
- Parameter date: The reference date.
72130
*/
73131
public init(timeInterval: TimeInterval, since date: Date) {
74-
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate + timeInterval)
132+
self.init(date._time + timeInterval)
75133
}
76134

77135
/// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 2001 by a given number of seconds.
78136
public init(timeIntervalSinceReferenceDate ti: TimeInterval) {
79-
_time = ti
137+
_time = .init(uncheckedHead: ti, tail: 0)
80138
}
81139

82140
/**
@@ -85,7 +143,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
85143
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).
86144
*/
87145
public var timeIntervalSinceReferenceDate: TimeInterval {
88-
return _time
146+
return _time.head
89147
}
90148

91149
/**
@@ -100,7 +158,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
100158
- SeeAlso: `timeIntervalSinceReferenceDate`
101159
*/
102160
public func timeIntervalSince(_ date: Date) -> TimeInterval {
103-
return self.timeIntervalSinceReferenceDate - date.timeIntervalSinceReferenceDate
161+
return (self._time - date._time).head
104162
}
105163

106164
/**
@@ -173,9 +231,9 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
173231

174232
/// Compare two `Date` values.
175233
public func compare(_ other: Date) -> ComparisonResult {
176-
if _time < other.timeIntervalSinceReferenceDate {
234+
if _time < other._time {
177235
return .orderedAscending
178-
} else if _time > other.timeIntervalSinceReferenceDate {
236+
} else if _time > other._time {
179237
return .orderedDescending
180238
} else {
181239
return .orderedSame
@@ -184,27 +242,27 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
184242

185243
/// Returns true if the two `Date` values represent the same point in time.
186244
public static func ==(lhs: Date, rhs: Date) -> Bool {
187-
return lhs.timeIntervalSinceReferenceDate == rhs.timeIntervalSinceReferenceDate
245+
return lhs._time == rhs._time
188246
}
189247

190248
/// Returns true if the left hand `Date` is earlier in time than the right hand `Date`.
191249
public static func <(lhs: Date, rhs: Date) -> Bool {
192-
return lhs.timeIntervalSinceReferenceDate < rhs.timeIntervalSinceReferenceDate
250+
return lhs._time < rhs._time
193251
}
194252

195253
/// Returns true if the left hand `Date` is later in time than the right hand `Date`.
196254
public static func >(lhs: Date, rhs: Date) -> Bool {
197-
return lhs.timeIntervalSinceReferenceDate > rhs.timeIntervalSinceReferenceDate
255+
return lhs._time > rhs._time
198256
}
199257

200258
/// Returns a `Date` with a specified amount of time added to it.
201259
public static func +(lhs: Date, rhs: TimeInterval) -> Date {
202-
return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate + rhs)
260+
return Date(lhs._time + rhs)
203261
}
204262

205263
/// Returns a `Date` with a specified amount of time subtracted from it.
206264
public static func -(lhs: Date, rhs: TimeInterval) -> Date {
207-
return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate - rhs)
265+
return Date(lhs._time - rhs)
208266
}
209267

210268
/// Add a `TimeInterval` to a `Date`.
@@ -220,7 +278,6 @@ public struct Date : Comparable, Hashable, Equatable, Sendable {
220278
public static func -=(lhs: inout Date, rhs: TimeInterval) {
221279
lhs = lhs - rhs
222280
}
223-
224281
}
225282

226283
@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(uncheckedHead: 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(uncheckedHead: 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)