swiftnsdatensdateformatternsdatecomponentsnsdatecomponentsformatter

NSDateComponentsFormatter's stringFromDate(_, toDate:) returns nil


Question

Why is string nil?

let formatter = NSDateComponentsFormatter()
let referenceDate = NSDate(timeIntervalSinceReferenceDate: 0)
let intervalDate = NSDate(timeInterval: 3628810, sinceDate: referenceDate)
let string = formatter.stringFromDate(referenceDate, toDate: intervalDate)

I'm expecting a string like "6w 10s" to be returned.

(6 weeks is 3,628,800 seconds.)


Attempted Troubleshooting

To troubleshoot, I tried setting allowedUnits:

formatter.allowedUnits = .YearCalendarUnit | .MonthCalendarUnit | .WeekCalendarUnit | .DayCalendarUnit | .HourCalendarUnit | .MinuteCalendarUnit | .SecondCalendarUnit

Which results in this error:

"NSInvalidArgumentException", "Specifying positional units with gaps is ambiguous, and therefore unsupported"

I don't know what a "positional unit" is (outside of football), and I don't think I'm specifying any gaps.


Notes

I'm not using stringFromTimeInterval() because that will return different results depending on the system's current date. (i.e., February only has 28/29 days in a month.) I want to calculate the time interval from the NSDate reference date.

I'm using Xcode 6.1.1 (6A2008a). Here's a Playground screenshot if that helps:

enter image description here


Solution

  • From the headerdoc:

    /* Bitmask of units to include. Set to 0 to get the default behavior.
       Note that, especially if the maximum number of units is low, unit
       collapsing is on, or zero dropping is on, not all allowed units may
       actually be used for a given NSDateComponents. Default value is the
       components of the passed-in NSDateComponents object, or years | 
       months | weeks | days | hours | minutes | seconds if passed an
       NSTimeInterval or pair of NSDates.
    
       Allowed units are:
    
        NSCalendarUnitYear
        NSCalendarUnitMonth
        NSCalendarUnitWeekOfMonth (used to mean "quantity of weeks")
        NSCalendarUnitDay
        NSCalendarUnitHour
        NSCalendarUnitMinute
        NSCalendarUnitSecond
    
       Specifying any other NSCalendarUnits will result in an exception.
     */
    var allowedUnits: NSCalendarUnit
    

    So:

    let formatter = NSDateComponentsFormatter()
    formatter.calendar = NSCalendar.currentCalendar()
    formatter.allowedUnits = nil
    formatter.allowedUnits |= .CalendarUnitYear
    formatter.allowedUnits |= .CalendarUnitMonth
    formatter.allowedUnits |= .CalendarUnitWeekOfMonth
    formatter.allowedUnits |= .CalendarUnitDay
    formatter.allowedUnits |= .CalendarUnitHour
    formatter.allowedUnits |= .CalendarUnitMinute
    formatter.allowedUnits |= .CalendarUnitSecond
    
    let referenceDate = NSDate(timeIntervalSinceReferenceDate: 0)
    let intervalDate = NSDate(timeInterval: 214458810, sinceDate: referenceDate)
    let string = formatter.stringFromDate(referenceDate, toDate: intervalDate)
    // -> "6y 9m 2w 4d 3:53:30"
    

    When we omit one of them in the middle:

    formatter.allowedUnits = nil
    formatter.allowedUnits |= .CalendarUnitYear
    formatter.allowedUnits |= .CalendarUnitMonth
    formatter.allowedUnits |= .CalendarUnitWeekOfMonth
    formatter.allowedUnits |= .CalendarUnitDay
    // formatter.allowedUnits |= .CalendarUnitHour
    formatter.allowedUnits |= .CalendarUnitMinute
    formatter.allowedUnits |= .CalendarUnitSecond
    

    'Specifying positional units with gaps is ambiguous, and therefore unsupported' error :) That is the "gap" means, I think.