10

I'm trying to work with dates and create dates in the future, but daylight savings keeps getting in the way and messing up my times.

Here is my code to move to midnight of the first day of the next month for a date:

+ (NSDate *)firstDayOfNextMonthForDate:(NSDate*)date
{
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    calendar.timeZone = [NSTimeZone systemTimeZone];
    calendar.locale = [NSLocale currentLocale];

    NSDate *currentDate = [NSDate dateByAddingMonths:1 toDate:date];
    NSDateComponents *components = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
                                                    fromDate:currentDate];

    [components setDay:1];
    [components setHour:0];
    [components setMinute:0];
    [components setSecond:0];

    return [calendar dateFromComponents:components];
}

+ (NSDate *) dateByAddingMonths: (NSInteger) monthsToAdd toDate:(NSDate*)date
{
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    calendar.timeZone = [NSTimeZone systemTimeZone];
    calendar.locale = [NSLocale currentLocale];

    NSDateComponents * months = [[NSDateComponents alloc] init];
    [months setMonth: monthsToAdd];

    return [calendar dateByAddingComponents: months toDate: date options: 0];
}

Which give the dates when I run the method iteratively on a date:

2013-02-01 00:00:00 +0000
2013-03-01 00:00:00 +0000
2013-03-31 23:00:00 +0000 should be 2013-04-01 00:00:00 +0000
2013-04-30 23:00:00 +0000 should be 2013-05-01 00:00:00 +0000

My initial thought was to not use systemTimeZone but that didn't seem to make a difference. Any ideas for how I can make the time constant and not take into account the change in daylight savings?

Josh
  • 3,445
  • 5
  • 37
  • 55
  • Your code works fine when I try it. I wonder if the problem lies within whatever code or classes you're using to print the log messages that you posted above. Do you have some code that's using a fixed offset to convert from GMT back to your local time? – Jay Slupesky Jan 28 '13 at 20:36
  • I'm not actually converting it back to local time, just `self.currentDate = [NSDate firstDayOfNextMonthForDate:self.currentDate]; NSLog(%@, self.currentDate);` Can I ask, are you testing on the simulator or on a device? – Josh Jan 28 '13 at 20:46

4 Answers4

8

For a given calendar date/time, it is not possible as a general rule to predict what actual time (seconds since the epoch) that represents. Time zones change and DST rules change. It's a fact of life. DST has a tortured history in Australia. DST rules have been very unpredictable in Israel. DST rules recently changed in the US causing huge headaches for Microsoft who was storing seconds rather than calendar dates.

Never save NSDate when you mean NSDateComponents. If you mean "the first of May 2013 in London," then save "the first of May 2013 in London" in your database. Then calculate an NSDate off of that as close to the actual event as possible. Do all your calendar math using NSDateComponents if you care about calendar things (like months). Only do NSDate math if you really only care about seconds.

EDIT: For lots of very useful background, see the Date and Time Programming Guide.

And one more side note about calendar components: when I say "the first of May 2013 in London," that does not mean "midnight on the first of May." Don't go adding calendar components you don't actually mean.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Ok I understand what you're saying here. Is there a way to 'natively' store the date components in core data? I can create an attribute with type 'Date' very easily. Is it possible to have an attribute with type 'Date Components'? Or would I have to have different attributes for day, month, year, timezone etc.? – Josh Jan 28 '13 at 21:19
  • Core Data doesn't provide a good thing for the old `NSCalenderDate`. The best you can do is wrap an `NSDateComponents` into an Entity, or otherwise serialize it as a custom object (or turn it into an `NSData` or the like). – Rob Napier Jan 28 '13 at 21:20
  • Hmmm I find dates very hard to work with. There doesn't seem to be any definitive source any where for how to store dates and how to use dates. You've touched on a very important topic, I think!!! – Josh Jan 28 '13 at 21:22
  • It's something I've struggled with in many project and watched even more explode over. The good news is that `NSDateComponents` will store pretty much everything you need, and it does conform to `NSCoding`. So that means you can store it pretty easily. – Rob Napier Jan 28 '13 at 21:24
  • BTW, I do highly recommend the Date and Time Programming Guide. The guys who wrote that (and who wrote the date functionality in Cocoa) really "get" calendars. It'd be nice if we could make calendars really easy. Unfortunately, like language, phone numbers, names, and just about everything else having to do with human interaction, they are extremely sloppy things. – Rob Napier Jan 28 '13 at 21:26
  • I'll have to take another look at the Date and Time Programming Guide. It has been far too long since I've looked at it and I've learnt a huge amount since then. I think I'm gonna have to change the accepted answer to yours as it provides deeper insight into the problems I'm facing. Even though what I'm doing actually works currently in my situation, there's no guarantee it will continue to work as expected. – Josh Jan 28 '13 at 21:29
  • 2
    I want to extremely recommend this post: [**Working with Date and Time**](http://realmacsoftware.com/blog/working-with-date-and-time). The apple docs in this case were somewhat confusing for me and this post simply answers all those annoying calculation and presenting handling cases. Explained very well and very conveniently. – Aviel Gross Dec 04 '13 at 08:57
  • From @AvielGross's link: "If you only care about the date and not the time set it to noon instead of the default of midnight." Excellent advice that I use regularly (when forced to work with a time component I don't care about). Saves a lot of grief. I've seen that "YYYY" vs "yyyy" issue lead to some wacky bugs, too. Good stuff there. – Rob Napier Dec 04 '13 at 15:35
4

Remember that what your program is printing to the log is the GMT time, not your local time. Therefore, it's correct for dates after the switch to DST in your local time zone that the GMT will have shifted by one hour.

Jay Slupesky
  • 1,893
  • 13
  • 14
  • I undertand that the log is printing the GMT time, not the local time so that it should change when DST comes into effect. I guess the real question is, even though it's printing out 'strangely' will comparing dates etc. be affected? – Josh Jan 28 '13 at 20:57
  • No, I think comparing GMT dates is what you want since GMT is unaffected by DST. – Jay Slupesky Jan 28 '13 at 21:04
  • Ok, sorry if I'm being slow here but can I just check: If I create some dates now and save them to core data, then further down the line, when it switches to DST and I create more dates, then when they are saved they can be compared to the dates created initially and they won't be out by an hour? Basically everything I'm saving is in GMT so the dates will be equal regardless of whether I create the date when it's DST or not? – Josh Jan 28 '13 at 21:08
  • Yes, that's what I'm saying. All your dates saved in Core Data will be in GMT and that's a good thing for you. It will insulate you from having to worry about DST. – Jay Slupesky Jan 28 '13 at 21:11
  • It will only insulate you from worrying about DST if you actually want "X" seconds since the epoch, no matter what "time" that means. If you care about nominal, calendar dates, you cannot ignore DST. It changes too much. – Rob Napier Jan 28 '13 at 21:17
3

I had the same problem, and people saying it's not actually a problem (as I saw on some related threads) doesn't help the situation. This kind of problem hurts whenever you have to deal with timezones and DST, and I always feel that I have to re-learn it every time.

I was dealing with epoch times as much as possible (it's a charting application), but there were times when I needed to use NSDate and NSCalendar (namely, for formatting the axis labels, and for marking calendar months, quarters and years). I struggled with this for a day or so, trying to set various timezones on calendars and suchlike.

In the end I found that the following line of code in my app delegate helped immensely:-

// prevent DST bugs by setting default timezone for app
    if let utcZone = NSTimeZone(abbreviation: "UTC") {
        NSTimeZone.setDefaultTimeZone(utcZone)
    }

On top of this, the source data had to be sanitised, so whenever I used an NSDateFormatter on incoming data, I made sure I set its time zone to the correct one for the data source (in my case, it was GMT). This gets rid of nasty DST issues in the data source and ensures all the resultant NSDates can be converted nicely into epoch times without worrying about DST.

Echelon
  • 7,306
  • 1
  • 36
  • 34
0

Another solution would be to check the hour component of the date, and in case it is 1 or 23 (instead of 0) just change the date by using Calendar.current.date(byAdding: .hour, value: -1, to: currentDate) or Calendar.current.date(byAdding: .hour, value: 1, to: currentDate) respectively.