rustproofFish
rustproofFish

Reputation: 999

Constructing NSDate using DateComponent gives inconsistent results

I've created an extension for NSDate which removes the time component to allow equality checks for NSDate based on date alone. I have achieved this by taking the original NSDate object, obtaining the day, month and year using the DateComponent class and then constructing a new NSDate using the information obtained. Although the NSDate objects obtained look correct when printed to the console (i.e. timestamp is 00:00:00) and using the NSDate.compare function on two identical dates returns NSComparisonResult.OrderedSame, if you deconstruct them using DateComponent once more, some of them have the hour set to 1. This appears to be a random event with this error being present about 55% of the time. Forcing the hour, minute and second properties of DateComponent to zero before constructing the new NSDate rather than assuming they will default to these values does not rectify the situation. Ensuring the timezone is set helps a little but again does not fix it.

I am guessing there may be a rounding error somewhere (possibly in my test code), I've fluffed the conversion or there is a Swift bug but would appreciate comments. Code and output from a unit test below.

Code as follows:

extension NSDate {

    // creates a NSDate object with time set to 00:00:00 which allows equality checks on dates alone
    var asDateOnly: NSDate {
        get {
            let userCalendar = NSCalendar.currentCalendar()
            let dayMonthYearUnits: NSCalendarUnit = .CalendarUnitDay | .CalendarUnitMonth | .CalendarUnitYear
            var dateComponents = userCalendar.components(dayMonthYearUnits, fromDate: self)
            dateComponents.timeZone = NSTimeZone(name: "GMT")
//            dateComponents.hour = 0
//            dateComponents.minute = 0
//            dateComponents.second = 0
            let result = userCalendar.dateFromComponents(dateComponents)!
            return result
        }
    }

Test func:

func testRemovingTimeComponentFromRandomNSDateObjectsAlwaysResultsInNSDateSetToMidnight() {
    var dates = [NSDate]()
    let dateRange = NSDate.timeIntervalSinceReferenceDate()
    for var i = 0; i < 30; i++ {
        let randomTimeInterval = Double(arc4random_uniform(UInt32(dateRange)))
        let date = NSDate(timeIntervalSinceReferenceDate: randomTimeInterval).asDateOnly
        let dateStrippedOfTime = date.asDateOnly

        // get the hour, minute and second components from the stripped date
        let userCalendar = NSCalendar.currentCalendar()
        var hourMinuteSecondUnits: NSCalendarUnit = .CalendarUnitHour | .CalendarUnitMinute | .CalendarUnitSecond
        var dateComponents = userCalendar.components(hourMinuteSecondUnits, fromDate: dateStrippedOfTime)
        dateComponents.timeZone = NSTimeZone(name: "GMT")
        XCTAssertTrue((dateComponents.hour == 0) && (dateComponents.minute == 0) && (dateComponents.second == 0), "Time components were not set to zero - \nNSDate: \(date) \nIndex: \(i) H: \(dateComponents.hour) M: \(dateComponents.minute) S: \(dateComponents.second)")
    }
}

Output:

testRemovingTimeComponentFromRandomNSDateObjectsAlwaysResultsInNSDateSetToMidnight] : XCTAssertTrue failed - Time components were not set to zero - 
NSDate: 2009-06-19 00:00:00 +0000 
Index: 29 H: 1 M: 0 S: 0

Upvotes: 1

Views: 406

Answers (1)

vikingosegundo
vikingosegundo

Reputation: 52227

I am sure that your test dates you created randomly will contain dates that live in DST (Daylight Saving Time) hence the 1 hour offset — indeed a clock would show 0:00.


Your code is anyway overly complicated and not timezone aware, as you overwrite it.

Preparation: create to dates on the same day with 5 hours apart.

var d1 = NSDate()
let cal = NSCalendar.currentCalendar()
d1 = cal.dateBySettingUnit(NSCalendarUnit.CalendarUnitHour, value: 12, ofDate: d1, options: nil)!
d1 = cal.dateBySettingUnit(NSCalendarUnit.CalendarUnitMinute, value: 0, ofDate: d1, options: nil)!

d1 is noon at users location today.

let comps = NSDateComponents()
comps.hour = 5;

var d2 = cal.dateByAddingComponents(comps, toDate: d1, options: nil)!

d2 five hours later

Comparison: This comparison will yield equal, as the dates are on the same day

let b = cal.compareDate(d1, toDate: d2, toUnitGranularity: NSCalendarUnit.CalendarUnitDay)
if b == .OrderedSame {
    println("equal")
} else {
    println("not equal")
}

The following will yield non equal, as the dates are not in the same hour

let b = cal.compareDate(d1, toDate: d2, toUnitGranularity: NSCalendarUnit.CalendarUnitHour)
if b == .OrderedSame {
    println("equal")
} else {
    println("not equal")
}

Display the dates with a date formatter, as it will take DST and timezones in account.

Upvotes: 1

Related Questions