2

I have created a calendar in my app, using the date object this way:

    NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit  | NSMinuteCalendarUnit)fromDate:[NSDate date]];
    NSInteger day    = [weekdayComponents day];
    NSInteger month  = [weekdayComponents month]; 
    NSInteger year   = [weekdayComponents year];

    m_dateFormatter.dateFormat = @"dd/MM/yyyy";
    [gregorian setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
    NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
    [timeZoneComps setDay:day];
    [timeZoneComps setMonth:month];
    [timeZoneComps setYear:year];
    [timeZoneComps setHour:00];
    [timeZoneComps setMinute:00];
    [timeZoneComps setSecond:01];

    m_currentDate         = [gregorian dateFromComponents:timeZoneComps];

When the user wants to go next month, I highlight the first date of that month. So, in this case, the date will be 1-06-2014,00:00:01.

Here is the code:

    - (void)showNextMonth
    {  
        // Move the date context to the next month
        NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
        NSDateComponents *dateComps = [[NSDateComponents alloc] init];
        [dateComps setMonth:1];

        m_currentMonthContext =[gregorian dateByAddingComponents:dateComps toDate:m_currentMonthContext options:0];


        NSDateComponents *weekdayComponents1 = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:m_currentMonthContext];

        NSInteger nextMonth = [weekdayComponents1 month];
        NSInteger nextyear  = [weekdayComponents1 year];

        NSDateComponents *weekdayComponents2 = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:m_currentDate];

        NSInteger currentDay   = [weekdayComponents2 day];
        NSInteger currentMonth = [weekdayComponents2 month];
        NSInteger currentYear  = [weekdayComponents2 year];

        NSInteger selectedDay = 1;

        if(nextMonth == currentMonth && nextyear == currentYear)
        {
            selectedDay = currentDay;
        }

        NSInteger month = nextMonth;

        [gregorian setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];

        NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
        [timeZoneComps setDay:selectedDay];
        [timeZoneComps setMonth:month];
        [timeZoneComps setYear:nextyear];
        [timeZoneComps setHour:00];
        [timeZoneComps setMinute:00];
        [timeZoneComps setSecond:01];

        m_currentMonthContext =[gregorian dateFromComponents:timeZoneComps];

        [self createCalendar];
    }

When m_currentMonthContext is calculated on the second to last line of the above method, its value is 1-06-2014,00:00:01.

createCalendar implementation:

-(void)createCalendar
{
    NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit)fromDate:m_currentMonthContext];

    NSInteger month = [weekdayComponents month];
    NSInteger year  = [weekdayComponents year];     
}

Here I get month as 5 and year as 2014, but the date is 1-06-2014. This happens only in US time zone, in all other time zones it is working fine.

So I want to know how to handle timezones effectively, or in other sense, how to make sure that NSDate does not change even if time zone changes.

Carl Veazey
  • 18,392
  • 8
  • 66
  • 81
Ranjit
  • 4,576
  • 11
  • 62
  • 121
  • What you mean by How to manage NSDate for different timezones? How about list down some case studies? eg: UserA moves from timeZoneA to timeZoneB. What would the date should be expected? and etc.. – Ricky May 24 '14 at 14:12
  • Hey Hi ricky, I want my NSDate object to not change irrespective of the timezone I am – Ranjit May 24 '14 at 14:18
  • You mean that if I am from South East Asia and I am flying to USA and I still see the Date based on South East Asia? So, you want your app to stick to only one date no matter where the user is? – Ricky May 24 '14 at 14:22
  • yes ricky you are right – Ranjit May 24 '14 at 14:24
  • @Ranjit I tried to clarify your question a little bit, by moving some key details from inside code blocks that seemed to be part of the question body itself, please correct it if I made an error here, thanks. – Carl Veazey May 24 '14 at 15:03

2 Answers2

2

The proximate cause is that the time zone is not consistently set on the calendar when calculating dates and date components. Sometimes you set the time zone to UTC, and sometimes not, which is going to cause inconsistencies, as sometimes offsets for local time will be applied, and sometimes not.

In detail, in your situation, m_currentMonthContext is an NSDate which represents the UTC time one second after midnight on June 1st, 2014. In your createCalendar method, you create a calendar that is the local time of the user, and calculate the components for such a date. In all time zones in the US, it is still the month of May one second after midnight on June 1st, 2014 UTC. An example in code, that can be run in isolation:

    NSCalendar *utcCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    [utcCalendar setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
    NSCalendar *localCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDate *june = [NSDate dateWithTimeIntervalSince1970:1401580801];
    NSDateComponents *utcComponents = [utcCalendar components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:june];
    NSDateComponents *localComponents = [localCalendar components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:june];
    NSLog(@"utc : %@", utcComponents);
    NSLog(@"local: %@", localComponents);

Here in MDT time zone, this logs:

utc : Calendar Year: 2014 Month: 6 Leap month: no Day: 1

local: Calendar Year: 2014 Month: 5 Leap month: no Day: 31

To recap, you're keeping a date in memory that's been calculated to represent a certain calendar date in UTC time, and then calculating the calendar date in the user's local time, but it seems you have an incorrect expectation that calendars for different time zones will interpret the same date the same way.

So, what to do? Your example is pretty complex, but it seems there's no need at all to store date components sometimes in UTC time zone and sometimes not - be consistent. Now, it also seems to me that you can be much much simpler in your code if you just want to find the first day of the next month.:

    NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *comps = [cal components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:[NSDate date]];
    [comps setMonth:[comps month] + 1];
    [comps setDay:1];

I tested this with December 15th, 2014, and it worked to create January 1st, 2015 in my local time. Hopefully that is consistent behavior.

To sum up - it's very likely a bug to not use a consistent calendar for your date component calculations. Sometimes having UTC and sometimes local is going to cause you nightmares. It seems like you should always calculate in local time, but I don't know the whole context of your application so can't make a blanket statement for that. Also, it should be safe to not rely on incessant conversions between dates and date components, and instead have the date component be your source of truth. That is, I mean it seems convoluted to convert date components to dates always to store in instance variables, but then to immediately convert the dates back into date components every time they're used - it seems better to just work with date components as much as possible.

Carl Veazey
  • 18,392
  • 8
  • 66
  • 81
  • Hello @Carl, thanks for your valuable source of info, you say that I have used some where UTC and somewhere Local time, I didnt get that, where I have done that? 2) What do you mean by inconsistent conversions between dates and dateComponents, can you point it out? 3)Can I use defaultTimeZone instead of UTC? – Ranjit May 24 '14 at 15:23
  • @Ranjit 1) One example: second to last line of `-showNextMonth` you assign to `m_currentMonthContext` a date derived from a calendar which you've set to be the UTC time zone. Then in `-createCalendar`, you instantiate a new calendar and don't set the time zone explicitly, so it defaults to be UTC time. Do you see that you are doing that, and does it make sense why you shouldn't? 2) I said "Incessant", to imply a lot - all the instance variables are stored as `NSDate` but it seems you almost only ever use them to calculate date components. Edited to clarify. 3) Where? – Carl Veazey May 24 '14 at 15:35
  • Hey thanks, so you mean to say that i should explicitly use UTC in create calendar?. I was thinking of using defaulttimeZone where i have used UTC, for example in my initialize a date function above.hope you got it. – Ranjit May 24 '14 at 15:41
  • That would probably just paper over the bugs, but I don't know exactly what your broad use case is so maybe it would work in the context of your application. Why are you using UTC **anywhere at all**, though, if you are displaying everything based on what makes sense to the user's calendar? – Carl Veazey May 24 '14 at 15:44
  • I thought of using UTC because it is universal.anything wrong with it? – Ranjit May 24 '14 at 16:10
  • Ah, interesting! It's called Coordinated Universal Time but it's not truly universal - it's more of a baseline time zone than a universal time zone. So maybe what you need to do is not specify a time zone for your calendars in this case, and instead rely on the user's calendar settings instead. That is, remove all setting of the explicit time zone, and see if that helps. – Carl Veazey May 24 '14 at 16:24
0

From the comment, I hope I understand your question correctly. You can try this code:-

NSDate * nowDate = [NSDate date];
NSLog(@"nowDate: %@",nowDate);

NSDateFormatter *df = [NSDateFormatter new];
[df setDateFormat:@"dd/MM/yyyy HH:mm"];
df.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:[NSTimeZone localTimeZone].secondsFromGMT];

NSString *localDate = [df stringFromDate:nowDate];
NSLog(@"localDate: %@", localDate);

Output:

2014-05-24 23:03:06.205 TestTimeZone[10214:60b] nowDate: 2014-05-24 15:03:06 +0000

2014-05-24 23:03:06.209 TestTimeZone[10214:60b] localDate: 24/05/2014 23:03

[NSDate date] always return GMT+0 date, no matter where is your timezone. May be just use this? At the same time I used NSDateFormatter to set to my local date based on my laptop. You can try to change to a few different timezones on your mac while running the above code on simulator. [NSDate date] might be just what you need.

Ricky
  • 10,485
  • 6
  • 36
  • 49
  • 2
    It's not exactly correct to say `[NSDate date]` always returns GMT, since `NSDate` has absolutely no concept of a time zone. – Carl Veazey May 24 '14 at 15:40
  • Really? May be I am lack of knowledge on that. So far, I have tested on different time zones on my mac, it still displays as +0000 which I believe it is GMT + 0. Unless we assign a timezone to NSDate, the default **[NSDate date]** is always GMT + 0 right? I might be wrong. – Ricky May 25 '14 at 10:26
  • That's what it prints when you log it, which is actually instantiating an `NSDateFormatter` with a certain time zone and locale to print it - but `NSDate` is just a time stamp, basically. – Carl Veazey May 25 '14 at 16:24