Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 3.0 - Broken func value(for component: Calendar.Component) -> Int?

Tags:

xcode8

swift3

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)
}
like image 806
Phil Larson Avatar asked Sep 13 '16 14:09

Phil Larson


1 Answers

Non-defined date components in a 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 of NSDateComponents is created the date components are set to NSDateComponentUndefined.

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 } }
like image 90
dfrib Avatar answered Jan 02 '23 11:01

dfrib