Working with Date and Time

Written by James on February 20th, 2013. Posted in Engineering

It’s highly likely that somewhere in your app you may need to work with dates and times; whether that be parsing, displaying or performing calendrical calculations. There are some important considerations to make and most certainly a few gotchas to watch out for. This post aims to give an overview of some of the Cocoa classes available to you and some tips and tricks on working with dates and times.

At Your Disposal

NSDate

The NSDate class is essentially a wrapper for an NSTimeInterval. This NSTimeInterval is nothing more than a double used to specify the number of seconds since the reference date in time. As a double it yields sub-millisecond precision over a range of 10,000 years. An NSDate represents a point in time in relation to this reference date. The references date remains a fixed point in time but can be interpreted differently depending on the calendar.

  • Gregorian calendar, 1 January 2001 00:00:00 +0000.
  • Buddhist calendar, 1 January 2544 00:00:00 +0000.

NSTimeZone

A time zone in the world is a geopolitical region where by the government have defined a set of rules for how local time should be calculated from the reference time zone of the world (GMT). In it’s simplest terms this is just an offset by a number of seconds, but there is a lot more that NSTimeZone accounts for. Daylight saving time is one of these as well as the fact that governments can changes the rules of their time zone between different years.

NSCalendar

There are many calendars in the world; Gregorian, Buddhist, Japanese, etc. Each define arithmetic properties such as the number of months in a year and the first weekday. An NSCalendar knows the mapping of an NSTimeInterval to and from a calendar date and also how to perform calendrical calculations such as “what’s the day 10 days from now”.

NSLocale

An NSLocale encapsulates a set of default regional settings for the current user. However the user can override these settings so you should not assume them. The settings define the way date and time should be formatted when presented to the user.

NSDateComponents

An object containing a set of calendar components such as month, day, hour, etc. It’s good to remember that an NSDateComponents can represent both an absolute and relative time.

  • Absolute, 14:45 in the afternoon.
  • Relative, 14 hours and 45 minutes.

You can use an NSCalendar object to convert an NSDateComponents to an NSDate, which kindly handles unit roll overs for you. For example an instance with components 2012 years, 13 months and 5 days becomes 5th January 2013.

NSDateFormatter

An NSDateFormatter is used for converting dates both to and from human-readable form, as well as machine readable form, i.e. a string with a schema. New objects are initialised with the locale settings of the current user. There are some predefined format styles for varying length outputs, e.g. NSDateFormatterShortStyle, NSDateFormatterLongStyle, or you can customise the output with a format string.

Tips & Tricks

Calendrical Calculations

The most important tip I can give you is to use the system algorithms for calendrical calculations. Do not try and write your own algorithms, as there are a lot of specific cases to account for such as:

  • The leap day in the Gregorian calendar, 29th February. Happens mostly every four years, the next omitted leap year is 2100.
  • The leap month in the Hebrew Calendar. Months are numbered 1-13 and 7 can be a leap month.
  • Time zone transitions. A forward transition causes an hour skip and a backward transition causes an hour to occur twice. In the UK the daylight saving time happens at 01:00 or 02:00 where as in Brazil the transition is at midnight.
  • Dateline transitions. In 2011 Samoa had a time zone change and moved from one side of the international date line to the other. At the end of the day on 29th December 2011, it became 31st December 2011, skipping 30th December 2011 entirely.
  • Japanese Imperial Eras. When a new Emperor comes in to power a new era begins and the year is set to 1. This means that a year can be set back to 1 during any point in the current year.

It’s also favourable to avoid stress boundaries. If you only care about the date and not the time set it to noon instead of the default of midnight. Similarly if you only care about the time and not the date use the reference date and set your time.

Another important fact is that 1 day does not always equal 86,400 seconds! If for instance you did [date dateByAddingTimeInterval:86400] where date is 30th March 2013 11:50 PM, you will get a results of 31st March 2013 00:50 AM. The issue here being that the daylight saving time transition has occurred but was not accounted for. Instead create an NSDateComponents and set the day components [dateComponents setDay:1]. Now you can add the components and daylight saving time transition is correctly handled.

NSDate *start = ...; // start: 30th March 2013 11:50 PM
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
[dateComponents setDay:1];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDate *end = [calendar dateByAddingComponents:dateComponents toDate:start options:0];
// end: 31st March 2013 11:50 PM

When performing calendrical calculation you should always work relative to your starting date and not to any intermediate date. For example taking 31st January, adding one month, and then adding a further month to the result would mean the following.

31st January -> 28th February -> 28th March

This is because there is no 31st February so the result returned is the last day of February. So when adding an additional month to this date the day number is preserved and we’re returned 28th March.

So as mentioned, always work relative to your starting date, and in this example add two months to 31st January to get 31st March.

31st January -> -> 31st March

Displaying Dates

To display a date to the user you’ll use an NSDateFormatter. Remember that these are always initialised with the locale settings of the current user, as well as with any customisations the user may have made.

Some user preferences override even a specifically set date format, for instance a user may prefer to see a 24-hour clock format. Setting the date format may seem like the correct way to get the output you’re after. However although the following is a valid date format for here in the UK, it isn’t for China.

[dateFormatter setDateFormat:@"dd/MM/yyyy"];

It’s always best to use the predefined format styles which will be based on the locale settings of the current user, or what they have customised in their system preferences.

[dateFormatter setDateStyle:NSDateFormatterShortStyle];

When these format styles don’t suit and you need to use a date format, use the method +dateFormatFromTemplate:options:locale: to generate a locale specific format.

NSString *format = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0 locale:[NSLocale currentLocale]];
[dateFormatter setDateFormat:format];
  • en_US locale generates @"EEE, MMM d" format, “Wed, Feb 20”.
  • de_DE locale generates @"EEE, d. MMM" format, “Mi., 20. Feb”.

This also works perfectly for the AM/PM of time.

NSString *format = [NSDateFormatter dateFormatFromTemplate:@"j" options:0 locale:[NSLocale currentLocale]];
[dateFormatter setDateFormat:format];
  • en_US locale generates @"h a" format, “6 PM”.
  • de_DE locale generates @"HH 'Uhr'" format, “18 Uhr”.

Parsing Dates

When parsing a date make sure you explicitly configured your NSDateFormatter with the correct calendar to ensure you get the correct result.

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
[dateFormatter setCalendar:calendar];
[dateFormatter setDateFormat:@"yyyy-MM-dd"];
NSDate *date = [dateFormatter dateFromString:@"2013-02-20"];

When parsing fixed-format dates like those from a web server, setting the date formatter locale to en_US_POSIX often works well. This locale is specifically designed to yield US English results regardless of both user and system preferences and will remain fixed even if the US changes the way it formats dates in the future.

NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setLocale:locale];

Working with Time Zones

Ensure you set the appropriate time zone when storing and parsing a date. The time in the string 2013-02-20 13:00:00 is 1pm in GMT. But if the time zone is not correctly set and a user in New York parsing the date it will be interpreted as 1pm EST, the equivalent of 6pm GMT.

[dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSDate *date = [dateFormatter dateFromString:@"2013-02-20 13:00:00"];

Working with Week-based Calendars

All calendars can be interpreted in a week-based fashion; a cyclic period of 7 days. There are two properties that define a week-based calendar.

  • The weekday which is the beginning of the week (and year).
  • Minimum number of days a straddling week needs in the new year to be considered the first week of that new year.

Below illustrates the different ways, including in a week-based fashion, to represent the date 20th February 2013 in the Gregorian calendar. Note: Weekday 1 is a Sunday.

  • {Year, Day} : {2013, 51}
  • {Year, Month, Day} : {2013, 2, 20}
  • {YearForWeekOfYear, WeekOfYear, Weekday} : {2013, 8, 4}

Do not mix an ordinary year number with week-based components, nor a week-based year number with ordinary components. For week based use the following NSCalendar component types.

NSWeekOfYearCalendarUnit
NSWeekOfMonthCalendarUnit
NSYearForWeekOfYearCalendarUnit

Also when using a date format string using the correct format is important.

  • @"YYYY" is week-based calendar year.
  • @"yyyy" is ordinary calendar year.

Unit Lengths

Finally NSCalendar has some methods to determine unit lengths. For instance if in your application you want to draw a calendar, you’d use the following method on your calendar object to determine how many months to draw.

NSRange months = [calendar maximumRangeOfUnit:NSMonthCalendarUnit];

You can ask for a range too, so you could use the following to determine how many days are in the month of the specified date. This also means you don’t need to hard code a leap year algorithm yourself.

NSRange days = [calendar rangeOfUnit:NSDayCalendarUnit inUnit:NSMonthCalendarUnit forDate:date];

There are a lot of complexities in date, time and calendrical calculations. Make sure you let the Foundation framework do all the heavy lifting for you and you don’t try and write your own algorithms. I hope you’ve been able to pick up a few tips and tricks and if you’ve anything else to add give me a shout on Twitter.

You might also enjoy

Building an FTP and HTTP Server

When we moved in to our new office last year we wanted to connect our motion camera to hubot so that we could pull images from it into our Campfire chat on demand, because it’s cool. We used the…

Read More