From a5e81fa7c6ac6036a5c464a2d7af2eafd4a28fac Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Thu, 2 Oct 2025 16:42:03 +0100 Subject: [PATCH 1/5] 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. --- .../Calendar/Calendar_Gregorian.swift | 29 ++-- .../Calendar/Calendar_Recurrence.swift | 17 +-- Sources/FoundationEssentials/Date.swift | 41 +++--- .../FoundationEssentials/DateInterval.swift | 49 +++++-- .../FoundationEssentials/DoubleDouble.swift | 136 ++++++++++++++++++ .../GregorianCalendarTests.swift | 27 +++- .../CalendarTests.swift | 2 +- ...ianCalendarInternationalizationTests.swift | 76 +++++----- 8 files changed, 288 insertions(+), 89 deletions(-) create mode 100644 Sources/FoundationEssentials/DoubleDouble.swift diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift index b7809b9b0..4f5cc4885 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift @@ -1448,30 +1448,39 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable } func dateInterval(of component: Calendar.Component, for date: Date) -> DateInterval? { - let time = date.timeIntervalSinceReferenceDate + let approximateTime = date._time.head var effectiveUnit = component switch effectiveUnit { case .calendar, .timeZone, .isLeapMonth, .isRepeatedDay: return nil case .era: - if time < -63113904000.0 { + if approximateTime < -63113904000.0 { return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0 - inf_ti), duration: inf_ti) } else { return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0), duration: inf_ti) } case .hour: - let ti = Double(timeZone.secondsFromGMT(for: date)) - var fixedTime = time + ti // compute local time - fixedTime = floor(fixedTime / 3600.0) * 3600.0 - fixedTime = fixedTime - ti // compute GMT - return DateInterval(start: Date(timeIntervalSinceReferenceDate: fixedTime), duration: 3600.0) + // Local hours may not be aligned to GMT hours, so we have to apply + // the time zone adjustment before rounding down, then unapply it. + let offset = Double(timeZone.secondsFromGMT(for: date)) + let start = ((date._time + offset)/3600).floor() * 3600 - offset + return DateInterval( + start: Date(start), + duration: 3600 + ) case .minute: - return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time / 60.0) * 60.0), duration: 60.0) + return DateInterval( + start: Date((date._time/60).floor() * 60), + duration: 60 + ) case .second: - return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time)), duration: 1.0) + return DateInterval(start: Date(date._time.floor()), duration: 1) case .nanosecond: - return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time * 1.0e+9) * 1.0e-9), duration: 1.0e-9) + return DateInterval( + start: Date((date._time*1e9).floor() / 1e9), + duration: 1e-9 + ) case .year, .yearForWeekOfYear, .quarter, .month, .day, .dayOfYear, .weekOfMonth, .weekOfYear: // Continue to below break diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift index dd8740236..8d9a32abc 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift @@ -93,8 +93,8 @@ extension Calendar { /// value is used as a lower bound for ``nextBaseRecurrenceDate()``. let rangeLowerBound: Date? - /// The start date's nanoseconds component - let startDateNanoseconds: TimeInterval + /// The start date's fractional seconds component + let fractionalSeconds: TimeInterval /// How many occurrences have been found so far var resultsFound = 0 @@ -131,7 +131,10 @@ extension Calendar { } self.recurrence = recurrence - self.start = start + // round start down to whole seconds, set aside fraction. + let wholeSeconds = start._time.floor() + fractionalSeconds = (start._time - wholeSeconds).head + self.start = Date(wholeSeconds) self.range = range let frequency = recurrence.frequency @@ -233,9 +236,7 @@ extension Calendar { case .monthly: [.second, .minute, .hour, .day] case .yearly: [.second, .minute, .hour, .day, .month, .isLeapMonth] } - var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start) - - startDateNanoseconds = start.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 1) + var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start) let expansionChangesDay = dayOfYearAction == .expand || dayOfMonthAction == .expand || weekAction == .expand || weekdayAction == .expand let expansionChangesMonth = dayOfYearAction == .expand || monthAction == .expand || weekAction == .expand @@ -427,11 +428,11 @@ extension Calendar { recurrence._limitTimeComponent(.second, dates: &dates, anchor: anchor) } - if startDateNanoseconds > 0 { + if fractionalSeconds != 0 { // `_dates(startingAfter:)` above returns whole-second dates, // so we need to restore the nanoseconds value present in the original start date. for idx in dates.indices { - dates[idx] += startDateNanoseconds + dates[idx] += fractionalSeconds } } dates = dates.filter { $0 >= self.start } diff --git a/Sources/FoundationEssentials/Date.swift b/Sources/FoundationEssentials/Date.swift index 4db2a7367..2dece938b 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -36,7 +36,11 @@ public typealias TimeInterval = Double @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) public struct Date : Comparable, Hashable, Equatable, Sendable { - internal var _time : TimeInterval + internal var _time: DoubleDouble + + internal init(_ time: DoubleDouble) { + self._time = time + } /// The number of seconds from 1 January 1970 to the reference date, 1 January 2001. public static let timeIntervalBetween1970AndReferenceDate : TimeInterval = 978307200.0 @@ -51,17 +55,23 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { /// Returns a `Date` initialized to the current date and time. public init() { - _time = Self.getCurrentAbsoluteTime() + _time = .init(head: Self.getCurrentAbsoluteTime(), tail: 0) } /// Returns a `Date` initialized relative to the current date and time by a given number of seconds. public init(timeIntervalSinceNow: TimeInterval) { - self.init(timeIntervalSinceReferenceDate: timeIntervalSinceNow + Self.getCurrentAbsoluteTime()) + self.init(.sum( + Self.getCurrentAbsoluteTime(), + timeIntervalSinceNow + )) } /// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 1970 by a given number of seconds. public init(timeIntervalSince1970: TimeInterval) { - self.init(timeIntervalSinceReferenceDate: timeIntervalSince1970 - Date.timeIntervalBetween1970AndReferenceDate) + self.init(.sum( + timeIntervalSince1970, + -Date.timeIntervalBetween1970AndReferenceDate + )) } /** @@ -71,12 +81,12 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { - Parameter date: The reference date. */ public init(timeInterval: TimeInterval, since date: Date) { - self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate + timeInterval) + self.init(date._time + timeInterval) } /// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 2001 by a given number of seconds. public init(timeIntervalSinceReferenceDate ti: TimeInterval) { - _time = ti + _time = .init(head: ti, tail: 0) } /** @@ -85,7 +95,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { 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). */ public var timeIntervalSinceReferenceDate: TimeInterval { - return _time + return _time.head } /** @@ -100,7 +110,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { - SeeAlso: `timeIntervalSinceReferenceDate` */ public func timeIntervalSince(_ date: Date) -> TimeInterval { - return self.timeIntervalSinceReferenceDate - date.timeIntervalSinceReferenceDate + return (self._time - date._time).head } /** @@ -173,9 +183,9 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { /// Compare two `Date` values. public func compare(_ other: Date) -> ComparisonResult { - if _time < other.timeIntervalSinceReferenceDate { + if _time < other._time { return .orderedAscending - } else if _time > other.timeIntervalSinceReferenceDate { + } else if _time > other._time { return .orderedDescending } else { return .orderedSame @@ -184,27 +194,27 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { /// Returns true if the two `Date` values represent the same point in time. public static func ==(lhs: Date, rhs: Date) -> Bool { - return lhs.timeIntervalSinceReferenceDate == rhs.timeIntervalSinceReferenceDate + return lhs._time == rhs._time } /// Returns true if the left hand `Date` is earlier in time than the right hand `Date`. public static func <(lhs: Date, rhs: Date) -> Bool { - return lhs.timeIntervalSinceReferenceDate < rhs.timeIntervalSinceReferenceDate + return lhs._time < rhs._time } /// Returns true if the left hand `Date` is later in time than the right hand `Date`. public static func >(lhs: Date, rhs: Date) -> Bool { - return lhs.timeIntervalSinceReferenceDate > rhs.timeIntervalSinceReferenceDate + return lhs._time > rhs._time } /// Returns a `Date` with a specified amount of time added to it. public static func +(lhs: Date, rhs: TimeInterval) -> Date { - return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate + rhs) + return Date(lhs._time + rhs) } /// Returns a `Date` with a specified amount of time subtracted from it. public static func -(lhs: Date, rhs: TimeInterval) -> Date { - return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate - rhs) + return Date(lhs._time - rhs) } /// Add a `TimeInterval` to a `Date`. @@ -220,7 +230,6 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { public static func -=(lhs: inout Date, rhs: TimeInterval) { lhs = lhs - rhs } - } @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) diff --git a/Sources/FoundationEssentials/DateInterval.swift b/Sources/FoundationEssentials/DateInterval.swift index 04d2c55d1..b96d5e73a 100644 --- a/Sources/FoundationEssentials/DateInterval.swift +++ b/Sources/FoundationEssentials/DateInterval.swift @@ -12,30 +12,37 @@ /// 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. @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -public struct DateInterval : Comparable, Hashable, Codable, Sendable { +public struct DateInterval: Comparable, Hashable, Codable, Sendable { /// The start date. - public var start : Date + public var start: Date + + /// Underlying storage for `duration` + internal var _duration: DoubleDouble /// The end date. /// /// - precondition: `end >= start` - public var end : Date { + public var end: Date { get { - return start + duration + return Date(start._time + _duration) } set { precondition(newValue >= start, "Reverse intervals are not allowed") - duration = newValue.timeIntervalSinceReferenceDate - start.timeIntervalSinceReferenceDate + _duration = (newValue._time - start._time) } } - - /// The duration. + + /// The duration /// /// - precondition: `duration >= 0` - public var duration : TimeInterval { - willSet { + public var duration: TimeInterval { + get { + _duration.head + } + set { precondition(newValue >= 0, "Negative durations are not allowed") + _duration = DoubleDouble(head: newValue, tail: 0) } } @@ -43,7 +50,7 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable { public init() { let d = Date() start = d - duration = 0 + _duration = .zero } /// Initialize a `DateInterval` with the specified start and end date. @@ -52,7 +59,7 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable { public init(start: Date, end: Date) { precondition(end >= start, "Reverse intervals are not allowed") self.start = start - duration = end.timeIntervalSince(start) + _duration = end._time - start._time } /// Initialize a `DateInterval` with the specified start date and duration. @@ -61,7 +68,7 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable { public init(start: Date, duration: TimeInterval) { precondition(duration >= 0, "Negative durations are not allowed") self.start = start - self.duration = duration + _duration = DoubleDouble(head: duration, tail: 0) } /** @@ -162,6 +169,24 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable { public static func <(lhs: DateInterval, rhs: DateInterval) -> Bool { return lhs.compare(rhs) == .orderedAscending } + + enum CodingKeys: String, CodingKey { + case start = "start" + case duration = "duration" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let start = try container.decode(Date.self, forKey: .start) + let duration = try container.decode(TimeInterval.self, forKey: .duration) + self.init(start: start, duration: duration) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(start, forKey: .start) + try container.encode(duration, forKey: .duration) + } } @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) diff --git a/Sources/FoundationEssentials/DoubleDouble.swift b/Sources/FoundationEssentials/DoubleDouble.swift new file mode 100644 index 000000000..c3b27cc4f --- /dev/null +++ b/Sources/FoundationEssentials/DoubleDouble.swift @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +internal struct DoubleDouble { + + private let storage: (Double, Double) + + @_transparent + init(head: Double, tail: Double) { + storage = (head, tail) + } + + @_transparent + var head: Double { storage.0 } + + @_transparent + var tail: Double { storage.1 } + + @inlinable + static func sum(_ a: Double, _ b: Double) -> DoubleDouble { + let head = a + b + let x = head - b + let y = head - x + let tail = (a - x) + (b - y) + return DoubleDouble(head: head, tail: tail) + } + + @inlinable + static func sum(large a: Double, small b: Double) -> DoubleDouble { + let head = a + b + let tail = a - head + b + return DoubleDouble(head: head, tail: tail) + } + + @inlinable + static func product(_ a: Double, _ b: Double) -> DoubleDouble { + let head = a * b + let tail = (-head).addingProduct(a, b) + return DoubleDouble(head: head, tail: tail) + } +} + +extension DoubleDouble: Comparable { + @_transparent + static func ==(a: Self, b: Self) -> Bool { + a.head == b.head && a.tail == b.tail + } + + @_transparent + static func <(a: Self, b: Self) -> Bool { + a.head < b.head || a.head == b.head && a.tail < b.tail + } +} + +extension DoubleDouble: Hashable { + @_transparent + func hash(into hasher: inout Hasher) { + hasher.combine(head) + hasher.combine(tail) + } +} + +extension DoubleDouble: AdditiveArithmetic { + @inlinable + static var zero: DoubleDouble { + Self(head: 0, tail: 0) + } + + @inlinable + static func +(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble { + let heads = sum(a.head, b.head) + let tails = sum(a.tail, b.tail) + let first = sum(large: heads.head, small: heads.tail + tails.head) + return sum(large: first.head, small: first.tail + tails.tail) + } + + @inlinable + static func +(a: DoubleDouble, b: Double) -> DoubleDouble { + let heads = sum(a.head, b) + let first = sum(large: heads.head, small: heads.tail + a.tail) + return sum(large: first.head, small: first.tail) + } + + @inlinable + prefix static func -(a: DoubleDouble) -> DoubleDouble { + DoubleDouble(head: -a.head, tail: -a.tail) + } + + @inlinable + static func -(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble { + a + (-b) + } + + @inlinable + static func -(a: DoubleDouble, b: Double) -> DoubleDouble { + a + (-b) + } +} + +extension DoubleDouble { + @inlinable + static func *(a: DoubleDouble, b: Double) -> DoubleDouble { + let tmp = product(a.head, b) + return DoubleDouble( + head: tmp.head, + tail: tmp.tail.addingProduct(a.tail, b) + ) + } + + @inlinable + static func /(a: DoubleDouble, b: Double) -> DoubleDouble { + let head = a.head/b + let residual = a.head.addingProduct(-head, b) + a.tail + return DoubleDouble(head: head, tail: residual/b) + } +} + +extension DoubleDouble { + @inlinable + func floor() -> DoubleDouble { + let approx = head.rounded(.down) + if approx == head { + return .sum(large: head, small: tail.rounded(.down)) + } + return DoubleDouble(head: approx, tail: 0) + } +} diff --git a/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift b/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift index 9e3d3c05f..31377c924 100644 --- a/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift +++ b/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift @@ -277,7 +277,13 @@ private struct GregorianCalendarTests { func test(addField field: Calendar.Component, value: Int, to addingToDate: Date, wrap: Bool, expectedDate: Date, sourceLocation: SourceLocation = #_sourceLocation) { let components = DateComponents(component: field, value: value)! let result = gregorianCalendar.date(byAdding: components, to: addingToDate, wrappingComponents: wrap)! - #expect(result == expectedDate, sourceLocation: sourceLocation) + // These tests were written when Date used a 64b representation; + // we'll add new tests that validate the low-word of the 128b + // Date, but these old tests should continue passing if we only + // look at the high word as vended by tISRD. + #expect(result.timeIntervalSinceReferenceDate == + expectedDate.timeIntervalSinceReferenceDate, + sourceLocation: sourceLocation) } date = Date(timeIntervalSince1970: 825723300.0) @@ -399,7 +405,13 @@ private struct GregorianCalendarTests { func test(addField field: Calendar.Component, value: Int, to addingToDate: Date, wrap: Bool, expectedDate: Date, sourceLocation: SourceLocation = #_sourceLocation) { let components = DateComponents(component: field, value: value)! let result = gregorianCalendar.date(byAdding: components, to: addingToDate, wrappingComponents: wrap)! - #expect(result == expectedDate, sourceLocation: sourceLocation) + // These tests were written when Date used a 64b representation; + // we'll add new tests that validate the low-word of the 128b + // Date, but these old tests should continue passing if we only + // look at the high word as vended by tISRD. + #expect(result.timeIntervalSinceReferenceDate == + expectedDate.timeIntervalSinceReferenceDate, + sourceLocation: sourceLocation) } date = Date(timeIntervalSince1970: 62135596800.0) // 3939-01-01 @@ -826,7 +838,11 @@ private struct GregorianCalendarTests { let new_end = new?.end #expect(new_start == start, "interval start did not match", sourceLocation: sourceLocation) - #expect(new_end == end, "interval end did not match", sourceLocation: sourceLocation) + // These tests were written when Date used a 64b representation; + // we'll add new tests that validate the low-word of the 128b + // Date, but these old tests should continue passing if we only + // look at the high word as vended by tISRD. + #expect(new_end?.timeIntervalSinceReferenceDate == end?.timeIntervalSinceReferenceDate, "interval end did not match", sourceLocation: sourceLocation) } var date: Date @@ -838,7 +854,10 @@ private struct GregorianCalendarTests { test(.hour, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820458000.0)) test(.minute, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820454460.0)) test(.second, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820454401.0)) + // Legacy test from 64b Date; expected end is the same as start due to rounding. test(.nanosecond, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820454400.0)) + // Updated test for 128b Date to benefit from improved accuracy: + #expect(calendar.dateInterval(of: .nanosecond, for: date)?.end == Date(timeInterval: 1e-9, since: date)) test(.weekday, date, expectedStart: Date(timeIntervalSince1970: 820396800.0), end: Date(timeIntervalSince1970: 820483200.0)) test(.weekdayOrdinal, date, expectedStart: Date(timeIntervalSince1970: 820396800.0), end: Date(timeIntervalSince1970: 820483200.0)) test(.quarter, date, expectedStart: Date(timeIntervalSince1970: 812534400.0), end: Date(timeIntervalSince1970: 820483200.0)) @@ -869,7 +888,7 @@ private struct GregorianCalendarTests { test(.hour, date, expectedStart: Date(timeIntervalSince1970: -62135769600.0), end: Date(timeIntervalSince1970: -62135766000.0)) test(.minute, date, expectedStart: Date(timeIntervalSince1970: -62135769600.0), end: Date(timeIntervalSince1970: -62135769540.0)) test(.second, date, expectedStart: Date(timeIntervalSince1970: -62135769600.0), end: Date(timeIntervalSince1970: -62135769599.0)) - test(.nanosecond, date, expectedStart: Date(timeIntervalSince1970: -62135769600.00001), end: Date(timeIntervalSince1970: -62135769600.00001)) + test(.nanosecond, date, expectedStart: date, end: Date(timeInterval: 1e-9, since: date)) test(.weekday, date, expectedStart: Date(timeIntervalSince1970: -62135827200.0), end: Date(timeIntervalSince1970: -62135740800.0)) test(.weekdayOrdinal, date, expectedStart: Date(timeIntervalSince1970: -62135827200.0), end: Date(timeIntervalSince1970: -62135740800.0)) test(.quarter, date, expectedStart: Date(timeIntervalSince1970: -62143689600.0), end: Date(timeIntervalSince1970: -62135740800.0)) diff --git a/Tests/FoundationInternationalizationTests/CalendarTests.swift b/Tests/FoundationInternationalizationTests/CalendarTests.swift index 8f5b3db3a..a84a1022e 100644 --- a/Tests/FoundationInternationalizationTests/CalendarTests.swift +++ b/Tests/FoundationInternationalizationTests/CalendarTests.swift @@ -1419,7 +1419,7 @@ private struct CalendarTests { func test(_ start: Date, _ end: Date) throws { let components = c.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond, .weekOfMonth], from: start, to: end) let added = try #require(c.date(byAdding: components, to: start)) - #expect(added == end, "actual: \(s.format(added)), expected: \(s.format(end))") + #expect(added.timeIntervalSinceReferenceDate == end.timeIntervalSinceReferenceDate, "actual: \(s.format(added)), expected: \(s.format(end))") } // 2024-03-09T02:34:36-0800, 2024-03-17T03:34:36-0700, 10:34:36 UTC diff --git a/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift b/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift index 195c18912..3d389281c 100644 --- a/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift +++ b/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift @@ -1461,8 +1461,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828262987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829468987.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828262987.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 828871387.0) // 1996-04-07T03:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) @@ -1489,8 +1489,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828270187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829476187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828270187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 828874987.0) // 1996-04-07T04:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) @@ -1517,8 +1517,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828273787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829479787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828273787.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846403387.0) // 1996-10-27T01:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846403387.0)) @@ -1547,8 +1547,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847011787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846403387.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846403387.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846406987.0) // 1996-10-27T01:03:07-0800 // Previously this returns 1996-10-27T01:03:07-0700 @@ -1578,8 +1578,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847011787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846410587.0) // 1996-10-27T02:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) @@ -1606,8 +1606,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847015387.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846414187.0) // 1996-10-27T03:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) @@ -1634,8 +1634,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847018987.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 814953787.0) // 1995-10-29T01:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814953787.0)) @@ -1664,8 +1664,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815562187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814953787.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814953787.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 814957387.0) // 1995-10-29T01:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814957387.0)) @@ -1692,8 +1692,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815562187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814957387.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814957387.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 814960987.0) // 1995-10-29T02:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814960987.0)) @@ -1720,8 +1720,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814352587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815565787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814352587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814960987.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814960987.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 814964587.0) // 1995-10-29T03:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814964587.0)) @@ -1748,8 +1748,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814356187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815569387.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814356187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814964587.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814964587.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) } @Test func add_Wrap_DST() { @@ -1789,8 +1789,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828262987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829468987.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 830851387.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 828871387.0) // 1996-04-07T03:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) @@ -1817,8 +1817,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828270187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829476187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 830858587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 828874987.0) // 1996-04-07T04:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) @@ -1845,8 +1845,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828273787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829479787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 830862187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846406987.0) // 1996-10-27T01:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) @@ -1873,8 +1873,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846752587.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846410587.0) // 1996-10-27T02:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) @@ -1901,8 +1901,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846756187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) date = Date(timeIntervalSince1970: 846414187.0) // 1996-10-27T03:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) @@ -1929,8 +1929,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846759787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) } @Test func ordinality_DST() { @@ -2448,21 +2448,21 @@ private struct GregorianCalendarInternationalizationTests { dc_customCalendarAndTimeZone.timeZone = .init(secondsFromGMT: 28800) // calendar.timeZone = UTC+0, dc.calendar.timeZone = UTC-7, dc.timeZone = UTC+8 // expect local time in dc.timeZone (UTC+8) - #expect(gregorianCalendar.date(from: dc_customCalendarAndTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z + #expect(gregorianCalendar.date(from: dc_customCalendarAndTimeZone)!.timeIntervalSinceReferenceDate == 679024975.891) // 2022-07-09T02:02:55Z var dc_customCalendar = dc dc_customCalendar.calendar = dcCalendar dc_customCalendar.timeZone = nil // calendar.timeZone = UTC+0, dc.calendar.timeZone = UTC-7, dc.timeZone = nil // expect local time in calendar.timeZone (UTC+0) - #expect(gregorianCalendar.date(from: dc_customCalendar)! == Date(timeIntervalSinceReferenceDate: 679053775.891)) // 2022-07-09T10:02:55Z + #expect(gregorianCalendar.date(from: dc_customCalendar)!.timeIntervalSinceReferenceDate == 679053775.891) // 2022-07-09T10:02:55Z var dc_customTimeZone = dc_customCalendarAndTimeZone dc_customTimeZone.calendar = nil dc_customTimeZone.timeZone = .init(secondsFromGMT: 28800) // calendar.timeZone = UTC+0, dc.calendar = nil, dc.timeZone = UTC+8 // expect local time in dc.timeZone (UTC+8) - #expect(gregorianCalendar.date(from: dc_customTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z + #expect(gregorianCalendar.date(from: dc_customTimeZone)!.timeIntervalSinceReferenceDate == 679024975.891) // 2022-07-09T02:02:55Z let dcCalendar_noTimeZone = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: .gmt, firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil) var dc_customCalendarNoTimeZone_customTimeZone = dc @@ -2470,7 +2470,7 @@ private struct GregorianCalendarInternationalizationTests { dc_customCalendarNoTimeZone_customTimeZone.timeZone = .init(secondsFromGMT: 28800) // calendar.timeZone = UTC+0, dc.calendar.timeZone = nil, dc.timeZone = UTC+8 // expect local time in dc.timeZone (UTC+8) - #expect(gregorianCalendar.date(from: dc_customCalendarNoTimeZone_customTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z + #expect(gregorianCalendar.date(from: dc_customCalendarNoTimeZone_customTimeZone)!.timeIntervalSinceReferenceDate == 679024975.891) // 2022-07-09T02:02:55Z } @Test func dateFromComponents_componentsTimeZoneConversion() { From 4f8d3f2790b9051a799599c24e588b4d4a9c720c Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Thu, 2 Oct 2025 17:35:51 +0100 Subject: [PATCH 2/5] Add DoubleDouble.swift to CMakeLists --- Sources/FoundationEssentials/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index a5a1e9c79..36b6e34cb 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(FoundationEssentials ComparisonResult.swift Date.swift DateInterval.swift + DoubleDouble.swift FoundationEssentials.swift IndexPath.swift LockedState.swift From 541a205fc1db08280c9de31e8b4234bf0040895f Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Mon, 6 Oct 2025 09:59:33 -0400 Subject: [PATCH 3/5] Fix up ParseStrategy tests for Date to account for Double-Double. --- .../Formatting/ParseStrategy+RegexComponentTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift index 6e684a957..d6d464d14 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift @@ -213,7 +213,7 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 #expect(match.output.1 == "MergeableSetTests") #expect(match.output.2 == "started") // dateFormatter.date(from: "2021-07-08 10:19:35.418")! - #expect(match.output.3 == Date(timeIntervalSinceReferenceDate: 647432375.418)) + #expect(match.output.3.timeIntervalSinceReferenceDate == 647432375.418) } #endif From 70a7c54ded0343b3d5b753cbf09cf2c564958980 Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Mon, 6 Oct 2025 10:32:42 -0400 Subject: [PATCH 4/5] Re-enable some Regex component tests 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. --- .../Formatting/ParseStrategy+RegexComponentTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift index d6d464d14..f9370c88d 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift @@ -62,8 +62,6 @@ private struct ParseStrategyMatchTests { #expect(res.output.1 == expectedDate) } -// https://github.com/apple/swift-foundation/issues/60 -#if FOUNDATION_FRAMEWORK @Test func apiStatement() { let statement = """ @@ -215,7 +213,6 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 // dateFormatter.date(from: "2021-07-08 10:19:35.418")! #expect(match.output.3.timeIntervalSinceReferenceDate == 647432375.418) } -#endif @Test func variousDatesAndTimes() { func verify(_ str: String, _ strategy: Date.ParseStrategy, _ expected: String?, sourceLocation: SourceLocation = #_sourceLocation) { From 3aea22c86520a8fcef44078daeea761f97c56642 Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Tue, 7 Oct 2025 16:53:50 -0400 Subject: [PATCH 5/5] 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 --- Sources/FoundationEssentials/Date.swift | 62 +++++++++++++++-- .../FoundationEssentials/DateInterval.swift | 4 +- .../FoundationEssentials/DoubleDouble.swift | 68 ++++++++++++++++--- 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/Sources/FoundationEssentials/Date.swift b/Sources/FoundationEssentials/Date.swift index 2dece938b..453b7853f 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -34,16 +34,64 @@ public typealias TimeInterval = Double 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`. */ @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -public struct Date : Comparable, Hashable, Equatable, Sendable { - +public struct Date: Comparable, Hashable, Equatable, Sendable { + /* Date is internally represented as a sum of two Doubles. + + 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 the time + at which this comment was written, accuracy 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). */ internal var _time: DoubleDouble - + internal init(_ time: DoubleDouble) { - self._time = time + self._time = time } +} +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Date { /// The number of seconds from 1 January 1970 to the reference date, 1 January 2001. - public static let timeIntervalBetween1970AndReferenceDate : TimeInterval = 978307200.0 + public static let timeIntervalBetween1970AndReferenceDate: TimeInterval = 978307200.0 /// The number of seconds from 1 January 1601 to the reference date, 1 January 2001. internal static let timeIntervalBetween1601AndReferenceDate: TimeInterval = 12622780800.0 @@ -55,7 +103,7 @@ public struct Date : Comparable, Hashable, Equatable, Sendable { /// Returns a `Date` initialized to the current date and time. public init() { - _time = .init(head: Self.getCurrentAbsoluteTime(), tail: 0) + _time = .init(uncheckedHead: Self.getCurrentAbsoluteTime(), tail: 0) } /// 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 { /// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 2001 by a given number of seconds. public init(timeIntervalSinceReferenceDate ti: TimeInterval) { - _time = .init(head: ti, tail: 0) + _time = .init(uncheckedHead: ti, tail: 0) } /** diff --git a/Sources/FoundationEssentials/DateInterval.swift b/Sources/FoundationEssentials/DateInterval.swift index b96d5e73a..835157808 100644 --- a/Sources/FoundationEssentials/DateInterval.swift +++ b/Sources/FoundationEssentials/DateInterval.swift @@ -42,7 +42,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable { } set { precondition(newValue >= 0, "Negative durations are not allowed") - _duration = DoubleDouble(head: newValue, tail: 0) + _duration = DoubleDouble(uncheckedHead: newValue, tail: 0) } } @@ -68,7 +68,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable { public init(start: Date, duration: TimeInterval) { precondition(duration >= 0, "Negative durations are not allowed") self.start = start - _duration = DoubleDouble(head: duration, tail: 0) + _duration = DoubleDouble(uncheckedHead: duration, tail: 0) } /** diff --git a/Sources/FoundationEssentials/DoubleDouble.swift b/Sources/FoundationEssentials/DoubleDouble.swift index c3b27cc4f..b8d58ddf4 100644 --- a/Sources/FoundationEssentials/DoubleDouble.swift +++ b/Sources/FoundationEssentials/DoubleDouble.swift @@ -10,42 +10,85 @@ // //===----------------------------------------------------------------------===// +/// A numeric type that uses two Double values as its representation, providing +/// about 106 bits of precision with the same exponent range as Double. +/// +/// This type conforms to AdditiveArithmetic, Hashable and Comparable, but does +/// not conform to FloatingPoint or Numeric; it implements only the API surface +/// that is necessary to serve as an internal implementation detail of Date. internal struct DoubleDouble { private let storage: (Double, Double) + /// A double-double value constructed by specifying the head and tail. + /// + /// This is an unchecked operation because it does not enforce the + /// invariant that head + tail == head in release builds, which is + /// necessary for subsequent arithmetic operations to behave correctly. @_transparent - init(head: Double, tail: Double) { + init(uncheckedHead head: Double, tail: Double) { + assert(!head.isFinite || head + tail == head) storage = (head, tail) } + /// The high-order Double. + /// + /// This property does not have a setter because `head` should pretty much + /// never be set independently of `tail`, so as to maintain the invariant + /// that `head + tail == head`. You can use `init(uncheckedHead:tail:)` + /// to directly construct DoubleDouble values, which will enforce the + /// invariant in debug builds. @_transparent var head: Double { storage.0 } + /// The low-order Double. + /// + /// This property does not have a setter because `tail` should pretty much + /// never be set independently of `head`, so as to maintain the invariant + /// that `head + tail == head`. You can use `init(uncheckedHead:tail:)` + /// to directly construct DoubleDouble values, which will enforce the + /// invariant in debug builds. @_transparent var tail: Double { storage.1 } + /// `a + b` represented as a normalized DoubleDouble. + /// + /// Computed via the [2Sum algorithm](https://en.wikipedia.org/wiki/2Sum). @inlinable static func sum(_ a: Double, _ b: Double) -> DoubleDouble { let head = a + b let x = head - b let y = head - x let tail = (a - x) + (b - y) - return DoubleDouble(head: head, tail: tail) + return DoubleDouble(uncheckedHead: head, tail: tail) } + /// `a + b` represented as a normalized DoubleDouble. + /// + /// Computed via the [Fast2Sum algorithm](https://en.wikipedia.org/wiki/2Sum). + /// + /// - Precondition: + /// `large` and `small` must be such that `sum(large:small:)` + /// produces the same result as `sum(_:_:)` would. A sufficient condition + /// is that `|large| >= |small|`, but this is not necessary, so we do not + /// enforce it via an assert. Instead this function asserts that the result + /// is the same as that produced by `sum(_:_:)` in Debug builds. This is + /// unchecked in Release. @inlinable static func sum(large a: Double, small b: Double) -> DoubleDouble { let head = a + b let tail = a - head + b - return DoubleDouble(head: head, tail: tail) + let result = DoubleDouble(uncheckedHead: head, tail: tail) + assert(!head.isFinite || result == sum(a, b)) + return result } + /// `a * b` represented as a normalized DoubleDouble. @inlinable static func product(_ a: Double, _ b: Double) -> DoubleDouble { let head = a * b let tail = (-head).addingProduct(a, b) - return DoubleDouble(head: head, tail: tail) + return DoubleDouble(uncheckedHead: head, tail: tail) } } @@ -72,7 +115,7 @@ extension DoubleDouble: Hashable { extension DoubleDouble: AdditiveArithmetic { @inlinable static var zero: DoubleDouble { - Self(head: 0, tail: 0) + Self(uncheckedHead: 0, tail: 0) } @inlinable @@ -83,6 +126,8 @@ extension DoubleDouble: AdditiveArithmetic { return sum(large: first.head, small: first.tail + tails.tail) } + /// Equivalent to `a + DoubleDouble(uncheckedHead: b, tail: 0)` but + /// computed more efficiently. @inlinable static func +(a: DoubleDouble, b: Double) -> DoubleDouble { let heads = sum(a.head, b) @@ -92,7 +137,7 @@ extension DoubleDouble: AdditiveArithmetic { @inlinable prefix static func -(a: DoubleDouble) -> DoubleDouble { - DoubleDouble(head: -a.head, tail: -a.tail) + DoubleDouble(uncheckedHead: -a.head, tail: -a.tail) } @inlinable @@ -100,6 +145,8 @@ extension DoubleDouble: AdditiveArithmetic { a + (-b) } + /// Equivalent to `a - DoubleDouble(uncheckedHead: b, tail: 0)` but + /// computed more efficiently. @inlinable static func -(a: DoubleDouble, b: Double) -> DoubleDouble { a + (-b) @@ -111,7 +158,7 @@ extension DoubleDouble { static func *(a: DoubleDouble, b: Double) -> DoubleDouble { let tmp = product(a.head, b) return DoubleDouble( - head: tmp.head, + uncheckedHead: tmp.head, tail: tmp.tail.addingProduct(a.tail, b) ) } @@ -120,17 +167,20 @@ extension DoubleDouble { static func /(a: DoubleDouble, b: Double) -> DoubleDouble { let head = a.head/b let residual = a.head.addingProduct(-head, b) + a.tail - return DoubleDouble(head: head, tail: residual/b) + return .sum(large: head, small: residual/b) } } extension DoubleDouble { + // This value rounded down to an integer. @inlinable func floor() -> DoubleDouble { let approx = head.rounded(.down) + // If head was already an integer, round tail down and renormalize. if approx == head { return .sum(large: head, small: tail.rounded(.down)) } - return DoubleDouble(head: approx, tail: 0) + // Head was not an integer; we can simply discard tail. + return DoubleDouble(uncheckedHead: approx, tail: 0) } }