Skip to content

Commit 2d98e90

Browse files
authored
[Gregorian Calendar] Implement dateInterval(of:for:) (swiftlang#356)
This mostly follows the implementation of Calendar_ICU.
1 parent 7a1aa26 commit 2d98e90

File tree

3 files changed

+338
-3
lines changed

3 files changed

+338
-3
lines changed

Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,20 @@ enum ResolvedDateComponents {
6464
// Pick the year field between yearForWeekOfYear and year and resovles era
6565
static func yearMonth(forDateComponent components: DateComponents) -> (year: Int, month: Int) {
6666
var rawYear: Int
67+
// Don't adjust for era if week is also specified
68+
var adjustEra = true
6769
if let yearForWeekOfYear = components.yearForWeekOfYear {
70+
if components.weekOfYear != nil {
71+
adjustEra = false
72+
}
6873
rawYear = yearForWeekOfYear
6974
} else if let year = components.year {
7075
rawYear = year
7176
} else {
7277
rawYear = 1
7378
}
7479

75-
if components.era == 0 /* BC */{
80+
if adjustEra && components.era == 0 /* BC */{
7681
rawYear = 1 - rawYear
7782
}
7883

@@ -153,6 +158,8 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
153158
let gregorianStartYear: Int
154159
let gregorianStartDate: Date
155160

161+
let inf_ti : TimeInterval = 4398046511104.0
162+
156163
// Only respects Gregorian identifier
157164
init(identifier: Calendar.Identifier, timeZone: TimeZone?, locale: Locale?, firstWeekday: Int?, minimumDaysInFirstWeek: Int?, gregorianStartDate: Date?) {
158165

@@ -503,7 +510,6 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
503510
func start(of unit: Calendar.Component, at: Date) -> Date? {
504511
let capped = at.capped
505512

506-
let inf_ti : TimeInterval = 4398046511104.0
507513
let time = capped.timeIntervalSinceReferenceDate
508514

509515
var effectiveUnit = unit
@@ -1047,7 +1053,89 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
10471053
}
10481054

10491055
func dateInterval(of component: Calendar.Component, for date: Date) -> DateInterval? {
1050-
fatalError()
1056+
1057+
let capped = date.capped
1058+
let time = capped.timeIntervalSinceReferenceDate
1059+
var effectiveUnit = component
1060+
switch effectiveUnit {
1061+
case .calendar, .timeZone, .isLeapMonth:
1062+
return nil
1063+
case .era:
1064+
if time < -63113904000.0 {
1065+
return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0 - inf_ti), duration: inf_ti)
1066+
} else {
1067+
return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0), duration: inf_ti)
1068+
}
1069+
1070+
case .hour:
1071+
let ti = Double(timeZone.secondsFromGMT(for: capped))
1072+
var fixedTime = time + ti // compute local time
1073+
fixedTime = floor(fixedTime / 3600.0) * 3600.0
1074+
fixedTime = fixedTime - ti // compute GMT
1075+
return DateInterval(start: Date(timeIntervalSinceReferenceDate: fixedTime), duration: 3600.0)
1076+
case .minute:
1077+
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time / 60.0) * 60.0), duration: 60.0)
1078+
case .second:
1079+
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time)), duration: 1.0)
1080+
case .nanosecond:
1081+
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time * 1.0e+9) * 1.0e-9), duration: 1.0e-9)
1082+
case .year, .yearForWeekOfYear, .quarter, .month, .day, .weekOfMonth, .weekOfYear:
1083+
// Continue to below
1084+
break
1085+
case .weekdayOrdinal, .weekday:
1086+
// Continue to below, after changing the unit
1087+
effectiveUnit = .day
1088+
break
1089+
}
1090+
1091+
let start = firstInstant(of: effectiveUnit, at: capped)
1092+
1093+
var upperBound: Date
1094+
switch effectiveUnit {
1095+
case .era:
1096+
let newUDate = add(.era, to: start, amount: 1, inTimeZone: timeZone)
1097+
guard newUDate != start else {
1098+
// Probably because we are at the limit of era.
1099+
return DateInterval(start: start, duration: inf_ti)
1100+
}
1101+
upperBound = start
1102+
1103+
case .year:
1104+
upperBound = add(.year, to: start, amount: 1, inTimeZone: timeZone)
1105+
1106+
case .yearForWeekOfYear:
1107+
upperBound = add(.yearForWeekOfYear, to: start, amount: 1, inTimeZone: timeZone)
1108+
1109+
case .quarter:
1110+
upperBound = add(.month, to: start, amount: 3, inTimeZone: timeZone)
1111+
1112+
case .month:
1113+
upperBound = add(.month, to: start, amount: 1, inTimeZone: timeZone)
1114+
1115+
case .weekOfYear: /* kCFCalendarUnitWeek_Deprecated */
1116+
upperBound = add(.weekOfYear, to: start, amount: 1, inTimeZone: timeZone)
1117+
1118+
case .weekOfMonth:
1119+
upperBound = add(.weekOfMonth, to: start, amount: 1, inTimeZone: timeZone)
1120+
1121+
case .day:
1122+
upperBound = add(.day, to: start, amount: 1, inTimeZone: timeZone)
1123+
1124+
default:
1125+
upperBound = start
1126+
}
1127+
1128+
// move back to 0h0m0s, in case the start of the unit wasn't at 0h0m0s
1129+
upperBound = firstInstant(of: .day, at: upperBound)
1130+
1131+
if let tzTransition = timeZoneTransitionInterval(at: upperBound, timeZone: timeZone) {
1132+
return DateInterval(start: start, end: upperBound - tzTransition.duration)
1133+
} else if upperBound > start {
1134+
return DateInterval(start: start, end: upperBound)
1135+
} else {
1136+
// Out of range
1137+
return nil
1138+
}
10511139
}
10521140

10531141
func isDateInWeekend(_ date: Date) -> Bool {

0 commit comments

Comments
 (0)