From 7566cd462073afbdf16ac846d9018d4d0c42c147 Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Fri, 10 Oct 2025 09:18:05 -0400 Subject: [PATCH] Double-double Date (#1533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapts Date to use a double-Double representation (the underlying representation is the sum of two double-precision values, giving about 106 bits of precision). Previously Date was backed by a single Double measuring time since Jan 1 2001 in seconds. Because Double's precision is non-uniform, this means that times within a hundred days of the epoch are represented with approximately nanosecond precision, but as you get farther away from that date the precision decreases. For times close to today, it has been reduced to about 100ns. The obvious thing would be to adopt an integer-based representation similar to C's timespec (32b nanoseconds, 64b seconds) or Swift's Duration (128b attoseconds). These representations suffer from a few difficulties: - Existing API on Date takes and produces TimeInterval (aka Double). Making Date use an integer representation internally would mean that existing users of the public API would suddently start getting different results for computations that were previously exact; even though we could add new API, and the overall system would be more precise, this would be a surprisingly subtle change for users to navigate. - We have been told that some software interprets the raw bytes of Date as a Double for the purposes of fast serialization. These packages formally violate Foundation's API boundaries, but that doesn't help users of those packages who would abruptly be broken by switching to an integer representation. Using DoubleDouble instead navigates these problems fairly elegantly. - Because DoubleDouble is still a floating-point type, it still suffers from non-uniform precision. However, because DoubleDouble is so fantastically precise, it can represent dates out to ±2.5 quadrillion years at ~nanosecond or better precision, so in practice this won't be much of an issue. - Existing API on Date will produce exactly the same result as it did previously in cases where those results were exact, minimizing surprises. In cases where the existing API was not exact, it will produce much more accurate results, even if users do not adopt new API, because its internal calculations are now more precise. - Software that (incorrectly) interprets the raw bytes of Date as a Double will get at least as accurate of a value as it did previously (and often a more accurate value). DateInterval gets the same treatment, so that it can benefit from more accurate internal computations as well. Follow-on work may adapt/add-to Date's API to make it easier to specify Dates with high precision. --- Sources/FoundationEssentials/CMakeLists.txt | 1 + .../Calendar/Calendar_Gregorian.swift | 29 ++- .../Calendar/Calendar_Recurrence.swift | 17 +- Sources/FoundationEssentials/Date.swift | 95 +++++++-- .../FoundationEssentials/DateInterval.swift | 49 +++-- .../FoundationEssentials/DoubleDouble.swift | 186 ++++++++++++++++++ .../GregorianCalendarTests.swift | 27 ++- .../CalendarTests.swift | 2 +- .../ParseStrategy+RegexComponentTests.swift | 5 +- ...ianCalendarInternationalizationTests.swift | 76 +++---- 10 files changed, 391 insertions(+), 96 deletions(-) create mode 100644 Sources/FoundationEssentials/DoubleDouble.swift 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 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..453b7853f 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -34,12 +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 { - - internal var _time : TimeInterval +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 + } +} +@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 @@ -51,17 +103,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(uncheckedHead: 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 +129,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(uncheckedHead: ti, tail: 0) } /** @@ -85,7 +143,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 +158,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 +231,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 +242,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 +278,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..835157808 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(uncheckedHead: 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(uncheckedHead: 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..b8d58ddf4 --- /dev/null +++ b/Sources/FoundationEssentials/DoubleDouble.swift @@ -0,0 +1,186 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// 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(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(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 + 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(uncheckedHead: 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(uncheckedHead: 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) + } + + /// 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) + 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(uncheckedHead: -a.head, tail: -a.tail) + } + + @inlinable + static func -(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble { + a + (-b) + } + + /// Equivalent to `a - DoubleDouble(uncheckedHead: b, tail: 0)` but + /// computed more efficiently. + @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( + uncheckedHead: 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 .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)) + } + // Head was not an integer; we can simply discard tail. + return DoubleDouble(uncheckedHead: 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/Formatting/ParseStrategy+RegexComponentTests.swift b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift index 6e684a957..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 = """ @@ -213,9 +211,8 @@ 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 @Test func variousDatesAndTimes() { func verify(_ str: String, _ strategy: Date.ParseStrategy, _ expected: String?, sourceLocation: SourceLocation = #_sourceLocation) { 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() {