Skip to content

Calendar Sequence API #322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 18, 2024
Merged
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
128 changes: 73 additions & 55 deletions Sources/FoundationEssentials/Calendar/Calendar.swift

Large diffs are not rendered by default.

24 changes: 17 additions & 7 deletions Sources/FoundationEssentials/Calendar/Calendar_Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,22 @@ struct CalendarCache : Sendable {
// MARK: - Concrete Classes

// _CalendarICU, if present
static var calendarICUClass: _CalendarProtocol.Type = {
static func calendarICUClass(identifier: Calendar.Identifier) -> _CalendarProtocol.Type? {
#if FOUNDATION_FRAMEWORK && canImport(FoundationICU)
_CalendarICU.self
#else
if let name = _typeByName("FoundationInternationalization._CalendarICU"), let t = name as? _CalendarProtocol.Type {
return t
} else {
// Use the default gregorian class
return _CalendarGregorian.self
if identifier == .gregorian {
// Use the default gregorian class
return _CalendarGregorian.self
} else {
return nil
}
}
#endif
}()
}

// MARK: - State

Expand Down Expand Up @@ -71,7 +75,9 @@ struct CalendarCache : Sendable {
return currentCalendar
} else {
let id = Locale.current._calendarIdentifier
let calendar = CalendarCache.calendarICUClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: id)!
let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
currentCalendar = calendar
return calendar
}
Expand All @@ -92,7 +98,9 @@ struct CalendarCache : Sendable {
if let cached = fixedCalendars[id] {
return cached
} else {
let new = CalendarCache.calendarICUClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: id)!
let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil)
fixedCalendars[id] = new
return new
}
Expand Down Expand Up @@ -129,6 +137,8 @@ struct CalendarCache : Sendable {

func fixed(identifier: Calendar.Identifier, locale: Locale?, timeZone: TimeZone?, firstWeekday: Int?, minimumDaysInFirstWeek: Int?, gregorianStartDate: Date?) -> any _CalendarProtocol {
// Note: Only the ObjC NSCalendar initWithCoder supports gregorian start date values. For Swift it is always nil.
return CalendarCache.calendarICUClass.init(identifier: identifier, timeZone: timeZone, locale: locale, firstWeekday: firstWeekday, minimumDaysInFirstWeek: minimumDaysInFirstWeek, gregorianStartDate: gregorianStartDate)
// If we cannot create the right kind of class, we fail immediately here
let calendarClass = CalendarCache.calendarICUClass(identifier: identifier)!
return calendarClass.init(identifier: identifier, timeZone: timeZone, locale: locale, firstWeekday: firstWeekday, minimumDaysInFirstWeek: minimumDaysInFirstWeek, gregorianStartDate: gregorianStartDate)
}
}
309 changes: 225 additions & 84 deletions Sources/FoundationEssentials/Calendar/Calendar_Enumerate.swift

Large diffs are not rendered by default.

59 changes: 48 additions & 11 deletions Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ extension Date {
/// This helper records which components should take precedence.
enum ResolvedDateComponents {

// TODO: Day of Year
// case dayOfYear(year: Int, dayOfYear: Int)
case day(year: Int, month: Int, day: Int?, weekOfYear: Int?)
case weekdayOrdinal(year: Int, month: Int, weekdayOrdinal: Int, weekday: Int?)
case weekOfYear(year: Int, weekOfYear: Int?, weekday: Int?)
Expand Down Expand Up @@ -108,10 +110,12 @@ enum ResolvedDateComponents {

return (year, month)
}

init(dateComponents components: DateComponents) {
var (year, month) = Self.yearMonth(forDateComponent: components)
let minWeekdayOrdinal = 1

// TODO: Check day of year value here
if let d = components.day {
if components.yearForWeekOfYear != nil, let weekOfYear = components.weekOfYear {
if components.month == nil && weekOfYear >= 52 {
Expand Down Expand Up @@ -286,6 +290,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
case .yearForWeekOfYear: 140742..<140743
case .nanosecond: 0..<1000000000
case .isLeapMonth: 0..<2
case .dayOfYear: 1..<366
case .calendar, .timeZone:
nil
}
Expand Down Expand Up @@ -315,6 +320,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
case .yearForWeekOfYear: return 140742..<144684
case .nanosecond: return 0..<1000000000
case .isLeapMonth: return 0..<2
case .dayOfYear: return 1..<367
case .calendar, .timeZone:
return nil
}
Expand Down Expand Up @@ -592,6 +598,15 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
return 1..<max + 1
case .month:
return 1..<13
case .dayOfYear:
guard let year = dateComponent.year else {
return nil
}
if gregorianYearIsLeap(year) {
return 1..<367
} else {
return 1..<366
}
case .day: // day in month
guard let month = dateComponent.month, let year = dateComponent.year else {
return nil
Expand Down Expand Up @@ -713,7 +728,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
dc.day = minMaxRange(of: .day, in: dc)?.lowerBound
fallthrough

case .weekdayOrdinal, .weekday, .day:
case .weekdayOrdinal, .weekday, .day, .dayOfYear:
dc.hour = minMaxRange(of: .hour, in: dc)?.lowerBound
fallthrough

Expand Down Expand Up @@ -787,7 +802,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
return Date(timeIntervalSinceReferenceDate: floor(time))
case .nanosecond:
return Date(timeIntervalSinceReferenceDate: floor(time * 1.0e+9) * 1.0e-9)
case .year, .yearForWeekOfYear, .quarter, .month, .day, .weekOfMonth, .weekOfYear:
case .year, .yearForWeekOfYear, .quarter, .month, .day, .dayOfYear, .weekOfMonth, .weekOfYear:
// Continue to below
break
case .weekdayOrdinal, .weekday:
Expand Down Expand Up @@ -1331,7 +1346,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time)), duration: 1.0)
case .nanosecond:
return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time * 1.0e+9) * 1.0e-9), duration: 1.0e-9)
case .year, .yearForWeekOfYear, .quarter, .month, .day, .weekOfMonth, .weekOfYear:
case .year, .yearForWeekOfYear, .quarter, .month, .day, .dayOfYear, .weekOfMonth, .weekOfYear:
// Continue to below
break
case .weekdayOrdinal, .weekday:
Expand Down Expand Up @@ -1370,7 +1385,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
case .weekOfMonth:
upperBound = add(.weekOfMonth, to: start, amount: 1, inTimeZone: timeZone)

case .day:
case .day, .dayOfYear:
upperBound = add(.day, to: start, amount: 1, inTimeZone: timeZone)

default:
Expand Down Expand Up @@ -1837,6 +1852,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
}
if components.contains(.month) { dc.month = month }
if components.contains(.day) { dc.day = day }
if components.contains(.dayOfYear) { dc.dayOfYear = dayOfYear }
if components.contains(.hour) { dc.hour = hour }
if components.contains(.minute) { dc.minute = minute }
if components.contains(.second) { dc.second = second }
Expand Down Expand Up @@ -2012,7 +2028,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
// nothing to do for the below fields
case .calendar, .timeZone, .isLeapMonth:
return date
case .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfMonth, .weekOfYear, .nanosecond:
case .day, .dayOfYear, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfMonth, .weekOfYear, .nanosecond:
// Handle below
break
}
Expand Down Expand Up @@ -2042,11 +2058,7 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
amountInSeconds = kSecondsInWeek * amount
keepWallTime = true

case .day:
amountInSeconds = amount * kSecondsInDay
keepWallTime = true

case .weekday:
case .day, .dayOfYear, .weekday:
amountInSeconds = amount * kSecondsInDay
keepWallTime = true

Expand Down Expand Up @@ -2156,6 +2168,31 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable
capDay(in: &dc) // adding 1 month to Jan 31 should return Feb 29, not Feb 31
result = self.date(from: dc, inTimeZone: timeZone)!

case .dayOfYear:
var monthIncludingDayOfYear = monthBasedComponents
monthIncludingDayOfYear.insert(.dayOfYear)
let dc = dateComponents(monthIncludingDayOfYear, from: date, in: timeZone)
guard let year = dc.year, let dayOfYear = dc.dayOfYear else {
preconditionFailure("dateComponents(:from:in:) unexpectedly returns nil for requested component")
}

let range: Range<Int>
if gregorianYearIsLeap(year) {
// max is 366
range = 1..<367
} else {
// max is 365
range = 1..<366
}

let newDayOfYear = add(amount: amount, to: dayOfYear, wrappingTo: range)
// Clear the month and day from the date components. Keep the era, year, and time values (hour, min, etc.)
var adjustedDateComponents = dc
adjustedDateComponents.month = nil
adjustedDateComponents.day = nil
adjustedDateComponents.dayOfYear = newDayOfYear
result = self.date(from: adjustedDateComponents, inTimeZone: timeZone)!

case .day:
let (_, monthStart, daysInMonth, inGregorianCutoverMonth) = dayOfMonthConsideringGregorianCutover(date, inTimeZone: timeZone)

Expand Down
57 changes: 54 additions & 3 deletions Sources/FoundationEssentials/Calendar/DateComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
internal var _year: Int?
internal var _month: Int?
internal var _day: Int?
internal var _dayOfYear: Int?
internal var _hour: Int?
internal var _minute: Int?
internal var _second: Int?
Expand Down Expand Up @@ -72,6 +73,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
self.weekOfMonth = weekOfMonth
self.weekOfYear = weekOfYear
self.yearForWeekOfYear = yearForWeekOfYear
self.dayOfYear = nil
}

package init?(component: Calendar.Component, value: Int) {
Expand Down Expand Up @@ -213,7 +215,16 @@ public struct DateComponents : Hashable, Equatable, Sendable {
get { _weekOfYear }
set { _weekOfYear = converted(newValue) }
}


/// A day of the year.
/// For example, in the Gregorian calendar, can go from 1 to 365 or 1 to 366 in leap years.
/// - note: This value is interpreted in the context of the calendar in which it is used.
@available(FoundationPreview 0.4, *)
public var dayOfYear: Int? {
get { _dayOfYear }
set { _dayOfYear = converted(newValue) }
}

/// This exists only for compatibility with NSDateComponents deprecated `week` value.
package var week: Int? {
get { _week }
Expand All @@ -236,7 +247,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
get { _isLeapMonth }
set { _isLeapMonth = newValue }
}

/// Returns a `Date` calculated from the current components using the `calendar` property.
public var date: Date? {
guard let calendar = _calendar else { return nil }
Expand Down Expand Up @@ -272,6 +283,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
case .weekOfYear: self.weekOfYear = value
case .yearForWeekOfYear: self.yearForWeekOfYear = value
case .nanosecond: self.nanosecond = value
case .dayOfYear: self.dayOfYear = value
case .calendar, .timeZone, .isLeapMonth:
// Do nothing
break
Expand All @@ -298,6 +310,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
case .weekOfYear: return self.weekOfYear
case .yearForWeekOfYear: return self.yearForWeekOfYear
case .nanosecond: return self.nanosecond
case .dayOfYear: return self.dayOfYear
case .calendar, .timeZone, .isLeapMonth:
return nil
}
Expand Down Expand Up @@ -349,7 +362,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
}

// This is similar to the list of units and keys\. in Calendar_Enumerate.swift, but this one does not include nanosecond or leap month
let units : [Calendar.Component] = [.era, .year, .quarter, .month, .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfMonth, .weekOfYear, .yearForWeekOfYear]
let units : [Calendar.Component] = [.era, .year, .quarter, .month, .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfMonth, .weekOfYear, .yearForWeekOfYear, .dayOfYear]

let newComponents = calendar.dateComponents(Set(units), from: date)

Expand All @@ -366,9 +379,31 @@ public struct DateComponents : Hashable, Equatable, Sendable {
if let weekOfMonth = _weekOfMonth, weekOfMonth != newComponents.weekOfMonth { return false }
if let weekOfYear = _weekOfYear, weekOfYear != newComponents.weekOfYear { return false }
if let yearForWeekOfYear = _yearForWeekOfYear, yearForWeekOfYear != newComponents.yearForWeekOfYear { return false }
if let dayOfYear = _dayOfYear, dayOfYear != newComponents.dayOfYear { return false }

return true
}

// MARK: -

/// Returns a new `DateComponents` where the subset of fields that can be scaled have been mulitplied by `value`.
internal func scaled(by value: Int) -> DateComponents {
var dc = self
if let era = _era { dc.era = era * value }
if let year = _year { dc.year = year * value }
if let month = _month { dc.month = month * value }
if let day = _day { dc.day = day * value }
if let hour = _hour { dc.hour = hour * value }
if let minute = _minute { dc.minute = minute * value }
if let second = _second { dc.second = second * value }
if let nanosecond = _nanosecond { dc.nanosecond = nanosecond * value }
if let quarter = _quarter { dc.quarter = quarter * value }
if let week = _week { dc.week = week * value }
if let weekOfMonth = _weekOfMonth { dc.weekOfMonth = weekOfMonth * value }
if let weekOfYear = _weekOfYear { dc.weekOfYear = weekOfYear * value }
if let yearForWeekOfYear = _yearForWeekOfYear { dc.yearForWeekOfYear = yearForWeekOfYear * value }
return dc
}

// MARK: -

Expand All @@ -390,6 +425,7 @@ public struct DateComponents : Hashable, Equatable, Sendable {
hasher.combine(_weekOfYear)
hasher.combine(_yearForWeekOfYear)
hasher.combine(_isLeapMonth)
hasher.combine(_dayOfYear)
}

// MARK: - Bridging Helpers
Expand All @@ -411,6 +447,10 @@ public struct DateComponents : Hashable, Equatable, Sendable {
lhs.nanosecond != rhs.nanosecond {
return false
}

if lhs.dayOfYear != rhs.dayOfYear {
return false
}

if !((lhs.isLeapMonth == false && rhs.isLeapMonth == nil) ||
(lhs.isLeapMonth == nil && rhs.isLeapMonth == false) ||
Expand Down Expand Up @@ -455,6 +495,7 @@ extension DateComponents : CustomStringConvertible, CustomDebugStringConvertible
if let r = quarter { c.append((label: "quarter", value: r)) }
if let r = weekOfMonth { c.append((label: "weekOfMonth", value: r)) }
if let r = weekOfYear { c.append((label: "weekOfYear", value: r)) }
if let r = dayOfYear { c.append((label: "dayOfYear", value: r)) }
if let r = yearForWeekOfYear { c.append((label: "yearForWeekOfYear", value: r)) }
if let r = isLeapMonth { c.append((label: "isLeapMonth", value: r)) }
return Mirror(self, children: c, displayStyle: Mirror.DisplayStyle.struct)
Expand All @@ -481,6 +522,7 @@ extension DateComponents : Codable {
case weekOfYear
case yearForWeekOfYear
case isLeapMonth
case dayOfYear
}

public init(from decoder: Decoder) throws {
Expand All @@ -505,6 +547,8 @@ extension DateComponents : Codable {

let isLeapMonth = try container.decodeIfPresent(Bool.self, forKey: .isLeapMonth)

let dayOfYear = try container.decodeIfPresent(Int.self, forKey: .dayOfYear)

self.init(calendar: calendar,
timeZone: timeZone,
era: era,
Expand All @@ -525,6 +569,10 @@ extension DateComponents : Codable {
if let isLeapMonth {
self.isLeapMonth = isLeapMonth
}

if let dayOfYear {
self.dayOfYear = dayOfYear
}
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -546,6 +594,7 @@ extension DateComponents : Codable {
try container.encodeIfPresent(self.weekOfYear, forKey: .weekOfYear)
try container.encodeIfPresent(self.yearForWeekOfYear, forKey: .yearForWeekOfYear)
try container.encodeIfPresent(self.isLeapMonth, forKey: .isLeapMonth)
try container.encodeIfPresent(self.dayOfYear, forKey: .dayOfYear)
}
}

Expand Down Expand Up @@ -582,6 +631,7 @@ extension DateComponents : ReferenceConvertible, _ObjectiveCBridgeable {
if let _weekOfMonth { ns.weekOfMonth = _weekOfMonth }
if let _weekOfYear { ns.weekOfYear = _weekOfYear }
if let _yearForWeekOfYear { ns.yearForWeekOfYear = _yearForWeekOfYear }
if let _dayOfYear { ns.dayOfYear = _dayOfYear }
if let _isLeapMonth { ns.isLeapMonth = _isLeapMonth }
if let _week { __NSDateComponentsSetWeek(ns, _week) }
return ns
Expand Down Expand Up @@ -611,6 +661,7 @@ extension DateComponents : ReferenceConvertible, _ObjectiveCBridgeable {
if ns.weekOfMonth != NSInteger.max { dc.weekOfMonth = ns.weekOfMonth }
if ns.weekOfYear != NSInteger.max { dc.weekOfYear = ns.weekOfYear }
if ns.yearForWeekOfYear != NSInteger.max { dc.yearForWeekOfYear = ns.yearForWeekOfYear }
if ns.dayOfYear != NSInteger.max { dc.dayOfYear = ns.dayOfYear }
if (__NSDateComponentsIsLeapMonthSet(ns)) {
dc.isLeapMonth = ns.isLeapMonth
}
Expand Down
Loading