Is there a workaround for value(for component: Calendar.Component) seemingly being broken? Or a dynamic way to call the property version?
func dateComponentsValueShouldBeNil() {
let dateComponents = DateComponents(month: 3)
debugPrint("Month", dateComponents.month) // "Month" Optional(3)
debugPrint("Property", dateComponents.hour) // "Property" nil
debugPrint("Enumeration", dateComponents.value(for: .hour)) // "Enumeration" Optional(9223372036854775807)
}
DateComponents
instance (valued nil
there) are represented in the wrapped NSDateComponents
instance by the global int
property NSDateComponentUndefined
This is not a bug/broken method, but follows from the fact that DateComponents
is tighly connected to NSDateComponents
(which derives from NSObject
: not using optionals an nil
to specify objects with no value). Specifically, the former's value(...)
method is, principally, a wrapper for the latter's value(forComponent:)
method (API Reference: Foundation -> NSDateComponents)
value(forComponent:)
Returns the value for a given NSCalendarUnit value.
Declaration
func value(forComponent unit: NSCalendar.Unit) -> Int
This method cannot return nil
, but represents date components without a value by the global int
variable NSDateComponentUndefined
.
let dateComponents = DateComponents(month: 3)
debugPrint("Enumeration", dateComponents.value(for: .hour))
// "Enumeration" Optional(9223372036854775807)
debugPrint("NSDateComponentUndefined", NSDateComponentUndefined)
// "NSDateComponentUndefined" 9223372036854775807
From API Reference: Foundation -> NSDateComponents we read:
...
An
NSDateComponents
object is not required to define all the component fields. When a new instance ofNSDateComponents
is created the date components are set toNSDateComponentUndefined
.
Hence, the return for attempting to call value(...)
for a Calendar.Component
member of DateComponents
that has not been initialized is not gibberish, but the default undefined value NSDateComponentUndefined
(a global int
property which happens return/be set to Int64.max = 9223372036854775807
).
To dwell deeper into the details of this, we may visit the source of DateComponents
(swift-corelibs-foundation/Foundation/DateComponents.swift)
/// Set the value of one of the properties, using an enumeration value instead of a property name.
///
/// The calendar and timeZone and isLeapMonth properties cannot be set by this method.
public mutating func setValue(_ value: Int?, for component: Calendar.Component) {
_applyMutation {
$0.setValue(_setter(value), forComponent: Calendar._toCalendarUnit([component]))
}
}
/// Returns the value of one of the properties, using an enumeration value instead of a property name.
///
/// The calendar and timeZone and isLeapMonth property values cannot be retrieved by this method.
public func value(for component: Calendar.Component) -> Int? {
return _handle.map {
$0.value(forComponent: Calendar._toCalendarUnit([component]))
}
}
property _handle
above wraps NSDateComponents
internal var _handle: _MutableHandle<NSDateComponents>
in a _MutableHandle
(swift-corelibs-foundation/Foundation/Boxing.swift)
internal final class _MutableHandle<MutableType : NSObject> where MutableType : NSCopying {
fileprivate var _pointer : MutableType
// ...
/// Apply a closure to the reference type.
func map<ReturnType>(_ whatToDo : (MutableType) throws -> ReturnType) rethrows -> ReturnType {
return try whatToDo(_pointer)
}
// ...
}
From the signature of value(...)
above ReturnType
is inferred to as Int?
, and _toCalendarValue
is defined as (swift-corelibs-foundation/Foundation/Calendar.swift)
internal static func _toCalendarUnit(_ units: Set<Component>) -> NSCalendar.Unit {
let unitMap: [Component : NSCalendar.Unit] =
[.era: .era,
.year: .year,
.month: .month,
.day: .day,
.hour: .hour,
.minute: .minute,
.second: .second,
.weekday: .weekday,
.weekdayOrdinal: .weekdayOrdinal,
.quarter: .quarter,
.weekOfMonth: .weekOfMonth,
.weekOfYear: .weekOfYear,
.yearForWeekOfYear: .yearForWeekOfYear,
.nanosecond: .nanosecond,
.calendar: .calendar,
.timeZone: .timeZone]
var result = NSCalendar.Unit()
for u in units {
let _ = result.insert(unitMap[u]!)
}
return result
}
Hence, whatToDo
in the body of the return of value(...)
can be deciphered to be equivalent to a call to
NSDateComponents.value(forComponent: NSCalendar.Unit)
And as descriped in the top of this answer, this call can never return nil
(return type Int
).
If we, finally, return to the source of DateComponents
, we see that the following private getter and setter handles the "bridging" between NSDateComponentUndefined
and nil
.
/// Translate from the NSDateComponentUndefined value into a proper Swift optional
private func _getter(_ x : Int) -> Int? { return x == NSDateComponentUndefined ? nil : x }
/// Translate from the proper Swift optional value into an NSDateComponentUndefined
private func _setter(_ x : Int?) -> Int { if let xx = x { return xx } else { return NSDateComponentUndefined } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With