Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/FoundationEssentials/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ add_library(FoundationEssentials
ComparisonResult.swift
Date.swift
DateInterval.swift
DoubleDouble.swift
FoundationEssentials.swift
IndexPath.swift
LockedState.swift
Expand Down
29 changes: 19 additions & 10 deletions Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand Down
95 changes: 76 additions & 19 deletions Sources/FoundationEssentials/Date.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
))
}

/**
Expand All @@ -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)
}

/**
Expand All @@ -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
}

/**
Expand All @@ -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
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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`.
Expand All @@ -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, *)
Expand Down
49 changes: 37 additions & 12 deletions Sources/FoundationEssentials/DateInterval.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,45 @@

/// 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)
}
}

/// Initializes a `DateInterval` with start and end dates set to the current date and the duration set to `0`.
public init() {
let d = Date()
start = d
duration = 0
_duration = .zero
}

/// Initialize a `DateInterval` with the specified start and end date.
Expand All @@ -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.
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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, *)
Expand Down
Loading
Loading