Skip to content

Commit 3ff24a4

Browse files
authored
[Gregorian Calendar] Implement dateComponents(_:from:to:) (swiftlang#378)
* [Gregorian Calendar] Implement `dateComponents(_:from:to:)` Implementation follows that of Calendar_ICU and ICU's Calendar::fieldDifference. Like many calendar algorithms, we get the difference by iterative search: We start advancing from `start` until we reach or pass `end`, striding using the queried component. If we land on `end` exactly, return the striding count as-is. If we pass `end`, do a binary search to find the optimal stride. * Support date of year
1 parent ef08590 commit 3ff24a4

File tree

2 files changed

+581
-2
lines changed

2 files changed

+581
-2
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ enum ResolvedDateComponents {
151151

152152
}
153153

154+
155+
/// Internal-use error for indicating unexpected situations when finding dates.
156+
enum GregorianCalendarError : Error {
157+
case overflow(Calendar.Component, Date /* failing start date */, Date /* failing end date */)
158+
case notAdvancing(Date /* next */, Date /* previous */)
159+
}
160+
154161
/// This class is a placeholder and work-in-progress to provide an implementation of the Gregorian calendar.
155162
internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable {
156163

@@ -2525,9 +2532,167 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
25252532
}
25262533
}
25272534

2528-
2535+
// MARK: Differences
2536+
2537+
// Calendar::fieldDifference
2538+
func difference(inComponent component: Calendar.Component, from start: Date, to end: Date) throws -> (difference: Int, newStart: Date) {
2539+
guard end != start else {
2540+
return (0, start)
2541+
}
2542+
2543+
switch component {
2544+
case .calendar, .timeZone, .isLeapMonth:
2545+
preconditionFailure("Invalid arguments")
2546+
2547+
case .era:
2548+
// Special handling since `add` below doesn't work with `era`
2549+
let currEra = dateComponent(.era, from: start)
2550+
let goalEra = dateComponent(.era, from: end)
2551+
2552+
return (goalEra - currEra, start)
2553+
case .nanosecond:
2554+
let diffInNano = end.timeIntervalSince(start).remainder(dividingBy: 1) * 1.0e+9
2555+
let diff = diffInNano < Double(Int32.max) ? Int(diffInNano) : Int(Int32.max)
2556+
let advanced = add(component, to: start, amount: diff, inTimeZone: timeZone)
2557+
return (diff, advanced)
2558+
2559+
case .year, .month, .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .quarter, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .dayOfYear:
2560+
// continue to below
2561+
break
2562+
}
2563+
2564+
let forward = end > start
2565+
var max = forward ? 1 : -1
2566+
var min = 0
2567+
while true {
2568+
let ms = add(component, to: start, amount: max, inTimeZone: timeZone)
2569+
guard forward ? (ms > start) : (ms < start) else {
2570+
throw GregorianCalendarError.notAdvancing(start, ms)
2571+
}
2572+
2573+
if ms == end {
2574+
return (max, ms)
2575+
} else if (forward && ms > end) || (!forward && ms < end) {
2576+
break
2577+
} else {
2578+
min = max
2579+
max <<= 1
2580+
guard forward ? max >= 0 : max < 0 else {
2581+
throw GregorianCalendarError.overflow(component, start, end)
2582+
}
2583+
}
2584+
}
2585+
2586+
// Binary search
2587+
while (forward && (max - min) > 1) || (!forward && (min - max > 1)) {
2588+
let t = min + (max - min) / 2
2589+
2590+
let ms = add(component, to: start, amount: t, inTimeZone: timeZone)
2591+
if ms == end {
2592+
return (t, ms)
2593+
} else if (forward && ms > end) || (!forward && ms < end) {
2594+
max = t
2595+
} else {
2596+
min = t
2597+
}
2598+
}
2599+
2600+
let advanced = add(component, to: start, amount: min, inTimeZone: timeZone)
2601+
2602+
return (min, advanced)
2603+
}
2604+
25292605
func dateComponents(_ components: Calendar.ComponentSet, from start: Date, to end: Date) -> DateComponents {
2530-
fatalError()
2606+
let cappedStart = start.capped
2607+
let cappedEnd = end.capped
2608+
2609+
let subseconds = cappedStart.timeIntervalSinceReferenceDate.remainder(dividingBy: 1)
2610+
2611+
var curr = cappedStart - subseconds
2612+
let goal = cappedEnd - subseconds
2613+
func orderedComponents(_ components: Calendar.ComponentSet) -> [Calendar.Component] {
2614+
var comps: [Calendar.Component] = []
2615+
if components.contains(.era) {
2616+
comps.append(.era)
2617+
}
2618+
if components.contains(.year) {
2619+
comps.append(.year)
2620+
}
2621+
if components.contains(.yearForWeekOfYear) {
2622+
comps.append(.yearForWeekOfYear)
2623+
}
2624+
if components.contains(.quarter) {
2625+
comps.append(.quarter)
2626+
}
2627+
if components.contains(.month) {
2628+
comps.append(.month)
2629+
}
2630+
if components.contains(.weekOfYear) {
2631+
comps.append(.weekOfYear)
2632+
}
2633+
if components.contains(.weekOfMonth) {
2634+
comps.append(.weekOfMonth)
2635+
}
2636+
if components.contains(.day) {
2637+
comps.append(.day)
2638+
}
2639+
if components.contains(.weekday) {
2640+
comps.append(.weekday)
2641+
}
2642+
if components.contains(.weekdayOrdinal) {
2643+
comps.append(.weekdayOrdinal)
2644+
}
2645+
if components.contains(.hour) {
2646+
comps.append(.hour)
2647+
}
2648+
if components.contains(.minute) {
2649+
comps.append(.minute)
2650+
}
2651+
if components.contains(.second) {
2652+
comps.append(.second)
2653+
}
2654+
2655+
if components.contains(.nanosecond) {
2656+
comps.append(.nanosecond)
2657+
}
2658+
2659+
return comps
2660+
}
2661+
2662+
var dc = DateComponents()
2663+
2664+
for component in orderedComponents(components) {
2665+
switch component {
2666+
case .era, .year, .month, .day, .dayOfYear, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfYear, .yearForWeekOfYear, .weekOfMonth, .nanosecond:
2667+
do {
2668+
let (diff, newStart) = try difference(inComponent: component, from: curr, to: goal)
2669+
dc.setValue(diff, for: component)
2670+
curr = newStart
2671+
} catch let error as GregorianCalendarError {
2672+
#if FOUNDATION_FRAMEWORK
2673+
switch error {
2674+
2675+
case .overflow(_, _, _):
2676+
Logger(Calendar.log).error("Overflowing in dateComponents(from:start:end:). start: \(start.timeIntervalSinceReferenceDate, privacy: .public) end: \(end.timeIntervalSinceReferenceDate, privacy: .public) component: \(component, privacy: .public)")
2677+
case .notAdvancing(_, _):
2678+
Logger(Calendar.log).error("Not advancing in dateComponents(from:start:end:). start: \(start.timeIntervalSinceReferenceDate, privacy: .public) end: \(end.timeIntervalSinceReferenceDate, privacy: .public) component: \(component, privacy: .public)")
2679+
}
2680+
#endif
2681+
dc.setValue(0, for: component)
2682+
} catch {
2683+
preconditionFailure("Unknown error: \(error)")
2684+
}
2685+
2686+
case .timeZone, .isLeapMonth, .calendar:
2687+
// No leap month support needed here, since these are quantities, not values
2688+
break
2689+
case .quarter:
2690+
// Currently unsupported so always return 0
2691+
dc.quarter = 0
2692+
}
2693+
}
2694+
2695+
return dc
25312696
}
25322697

25332698
#if FOUNDATION_FRAMEWORK

0 commit comments

Comments
 (0)