1

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 

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the suggestion. I was wondering if there was something going on with time zones but it doesn't explain why the 1hr offset appears randomly. My test code creates 30 random dates using exactly the same code (admittedly complex but I'm learning at the moment so not very efficient!) before 'pruning' to remove the time. I've accessed the time components for display having first set the time zone to GMT. As the method of construction and display is consistent every time, I don't understand why the output alternates between 0h0m0s and 1h0m0s in a random fashion. Any ideas?
I assume your locale is set to Britisch timezone. That is a DST timezone. So some dates are created with dat, other not. If you print a plain date object this will resolve in +/- 1hour, as the nsdate itself is not aware of locale stuff. If you would print them through a NSDateFormatter, you will see only 0:00. And never force date zones. Let the calendar and local handle that.
every cocoa developer should watch this video: Performing Calendar Calculations
Thanks for the pointers but why do you say never to 'force' timezones? I thought the whole point of NSDateFormatter was to allow conversion between timezones? I'm afraid I am still rather puzzled by the random variation in output - granted, I may be going about this all wrong but surely the output would be consistently wrong and that isn't the case.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.