Pro Yearly is on sale from $80 to $50! »

Dates and Times in Cocoa

8d92e9730c561c120200f34e7e50ed46?s=47 Jeff Kelley
February 13, 2014

Dates and Times in Cocoa

A presentation on date and time APIs in Cocoa, why they’re hard, and how to use them. Presented at CocoaHeads Ann Arbor on February 13, 2014.

8d92e9730c561c120200f34e7e50ed46?s=128

Jeff Kelley

February 13, 2014
Tweet

Transcript

  1. Dates and Times in Cocoa Jeff Kelley (@SlaunchaMan) Ann Arbor

    CocoaHeads, February 13th, 2014
  2. Why are dates and times so hard?

  3. Why are dates and times so hard? • NSDate does

    not represent what you think it does • Web services using other date libraries • Time zones exist • Daylight Savings Time exists
  4. NSDate typedef double NSTimeInterval; ! #define NSTimeIntervalSince1970 978307200.0 ! @interface

    NSDate : NSObject <NSCopying, NSSecureCoding> ! - (NSTimeInterval)timeIntervalSinceReferenceDate; ! @end
  5. Let’s Explore NSDate unsigned int ivarCount; Ivar *ivars = class_copyIvarList([NSDate

    class], &ivarCount); ! for (unsigned int i = 0; i < ivarCount; i++) { NSLog(@"Ivar: %s Type: %s", ivar_getName(ivars[i]), ivar_getTypeEncoding(ivars[i])); } ! free(ivars); ! ! Output: Program ended with exit code: 0 ! …what?
  6. What is an NSDate? • Basically just a double •

    A single moment in time, representing the number of seconds since a reference date • By default, the first moment of 2001
  7. Where’s All That Date Stuff? • NSCalendar takes care of

    the time and date APIs that are more than just a number: • Splitting a date into month, day, year, etc. • Creating an NSDate from those components
  8. NSCalendar Calendars encapsulate information about systems of reckoning time in

    which the beginning, length, and divisions of a year are defined. They provide information about the calendar and support for calendrical computations such as determining the range of a given calendrical unit and adding units to a given absolute time.
  9. NSCalendar • We use the Gregorian calendar, as do most

    • Japanese, Buddhist calendars supported on iOS • Split dates into calendrical components, can perform arithmetic operations on them
  10. Dumb Calendar Trick mercutio:~ jeff$ cal February 2014 Su Mo

    Tu We Th Fr Sa 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 !
  11. Dumb Calendar Trick mercutio:~ jeff$ cal 9 1752 September 1752

    Su Mo Tu We Th Fr Sa 1 2 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ! ! !
  12. Dumb Calendar Trick mercutio:~ jeff$ cal 1752 1752 ! January

    February March Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 3 4 1 1 2 3 4 5 6 7 5 6 7 8 9 10 11 2 3 4 5 6 7 8 8 9 10 11 12 13 14 12 13 14 15 16 17 18 9 10 11 12 13 14 15 15 16 17 18 19 20 21 19 20 21 22 23 24 25 16 17 18 19 20 21 22 22 23 24 25 26 27 28 26 27 28 29 30 31 23 24 25 26 27 28 29 29 30 31 April May June Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 3 4 1 2 1 2 3 4 5 6 5 6 7 8 9 10 11 3 4 5 6 7 8 9 7 8 9 10 11 12 13 12 13 14 15 16 17 18 10 11 12 13 14 15 16 14 15 16 17 18 19 20 19 20 21 22 23 24 25 17 18 19 20 21 22 23 21 22 23 24 25 26 27 26 27 28 29 30 24 25 26 27 28 29 30 28 29 30 31 July August September Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 3 4 1 1 2 14 15 16 5 6 7 8 9 10 11 2 3 4 5 6 7 8 17 18 19 20 21 22 23 12 13 14 15 16 17 18 9 10 11 12 13 14 15 24 25 26 27 28 29 30 19 20 21 22 23 24 25 16 17 18 19 20 21 22 26 27 28 29 30 31 23 24 25 26 27 28 29 30 31 October November December Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 3 4 5 6 7 1 2 3 4 1 2 8 9 10 11 12 13 14 5 6 7 8 9 10 11 3 4 5 6 7 8 9 15 16 17 18 19 20 21 12 13 14 15 16 17 18 10 11 12 13 14 15 16 22 23 24 25 26 27 28 19 20 21 22 23 24 25 17 18 19 20 21 22 23 29 30 31 26 27 28 29 30 24 25 26 27 28 29 30 31
  13. Creating a Date From Components NSCalendar *gregorianCalendar = [[NSCalendar alloc]

    initWithCalendarIdentifier:NSGregorianCalendar]; ! NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; ! dateComponents.year = 2007; dateComponents.month = 1; dateComponents.day = 9; ! dateComponents.hour = 9; dateComponents.minute = 41; ! dateComponents.timeZone = [NSTimeZone timeZoneWithName:@"America/Los_Angeles"]; ! NSDate *introductionDate = [gregorianCalendar dateFromComponents:dateComponents];
  14. Extracting Components From a Date NSDateComponents *components = [gregorianCalendar components:NSWeekdayCalendarUnit

    fromDate:introductionDate]; ! NSLog(@"%d", components.weekday); ! ! Output: 3
  15. Calendar Units typedef NS_OPTIONS(NSUInteger, NSCalendarUnit) { NSCalendarUnitEra, NSCalendarUnitYear, NSCalendarUnitMonth, NSCalendarUnitDay,

    NSCalendarUnitHour, NSCalendarUnitMinute, NSCalendarUnitSecond, NSCalendarUnitWeekday, NSCalendarUnitWeekdayOrdinal, NSCalendarUnitQuarter, NSCalendarUnitWeekOfMonth, NSCalendarUnitWeekOfYear, NSCalendarUnitYearForWeekOfYear, NSCalendarUnitNanosecond, NSCalendarUnitCalendar, NSCalendarUnitTimeZone };
  16. Dates vs. Date Components • A date knows when it

    is relative to another time, but not where it is on a calendar • Date components know where they are on a calendar, but not relative to a moment in time
  17. Comparing Dates • Use NSDate to see which of two

    dates occurred first:
 
 NSDate *date1, *date2;
 
 if ([date1 compare:date2] == NSOrderedAscending) {
 // date1 happened first
 }
  18. Comparing Dates • Use NSCalendar and NSDateComponents to perform relative

    arithmetic on dates:
 
 NSDate *now = [NSDate date];
 
 NSCalendar *gregorianCalendar =
 [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
 
 NSDateComponents *diff = [[NSDateComponents alloc] init];
 diff.day = 7;
 
 NSDate *aWeekFromNow =
 [gregorianCalendar dateByAddingComponents:diff
 toDate:now
 options:kNilOptions];
  19. Never Do This NSDate *now = [NSDate date]; ! NSTimeInterval

    secondsInADay = 24.0 * 60.0 * 60.0; ! NSDate *aWeekFromNow = [now dateByAddingTimeInterval:secondsInADay * 7.0];
  20. Because You’ll End Up Doing This NSDate *now = [NSDate

    date]; ! NSTimeInterval secondsInADay = 24.0 * 60.0 * 60.0; ! NSDate *aWeekFromNow = [now dateByAddingTimeInterval:secondsInADay * 7.0]; ! NSDate *dstTransition = [[NSTimeZone defaultTimeZone] nextDaylightSavingTimeTransitionAfterDate:now]; ! if ([dstTransition compare:aWeekFromNow] == NSOrderedAscending) { if ([[NSTimeZone defaultTimeZone] isDaylightSavingTimeForDate:now]) { aWeekFromNow = [aWeekFromNow dateByAddingTimeInterval:60.0 * 60.0]; } else { aWeekFromNow = [aWeekFromNow dateByAddingTimeInterval:-60.0 * 60.0]; } }
  21. Calendrical Calculations • Use NSCalendar to get the NSDateComponents that

    represent the difference between two dates • The resulting components are different based on what you ask for
  22. Calendrical Calculations NSDate *now = [NSDate date]; ! NSDateComponents *romeFoundedComponents

    = [[NSDateComponents alloc] init]; romeFoundedComponents.era = 0; romeFoundedComponents.year = 753; romeFoundedComponents.month = 4; romeFoundedComponents.day = 21; ! NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; ! NSDate *romeFoundedDate = [calendar dateFromComponents:romeFoundedComponents]; ! NSDateComponents *difference = [calendar components:(NSDayCalendarUnit) fromDate:romeFoundedDate toDate:now options:kNilOptions]; ! NSLog(@"It has been %d days since the Roman empire was founded.", difference.day); ! ! Output: It has been 1010193 days since the Roman empire was founded.
  23. Calendrical Calculations NSDate *now = [NSDate date]; ! NSDateComponents *romeFoundedComponents

    = [[NSDateComponents alloc] init]; romeFoundedComponents.era = 0; romeFoundedComponents.year = 753; romeFoundedComponents.month = 4; romeFoundedComponents.day = 21; ! NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; ! NSDate *romeFoundedDate = [calendar dateFromComponents:romeFoundedComponents]; ! NSDateComponents *difference = [calendar components:(NSDayCalendarUnit|NSYearCalendarUnit) fromDate:romeFoundedDate toDate:now options:kNilOptions]; ! NSLog(@"It has been %d years, %d days since the Roman empire was founded.", difference.year, difference.day); ! ! Output: It has been 2765 years, 298 days since the Roman empire was founded.
  24. Calendrical Calculations NSDate *now = [NSDate date]; ! NSDateComponents *romeFoundedComponents

    = [[NSDateComponents alloc] init]; romeFoundedComponents.era = 0; romeFoundedComponents.year = 753; romeFoundedComponents.month = 4; romeFoundedComponents.day = 21; ! NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; ! NSDate *romeFoundedDate = [calendar dateFromComponents:romeFoundedComponents]; ! NSDateComponents *difference = [calendar components:(NSDayCalendarUnit|NSYearCalendarUnit|NSEraCalendarUnit) fromDate:romeFoundedDate toDate:now options:kNilOptions]; ! NSLog(@"It has been %d era, %d years, %d days since the Roman empire was founded.", difference.era, difference.year, difference.day); ! ! Output: It has been 1 era, 2765 years, 298 days since the Roman empire was founded.
  25. Where’s All That Date Stuff? • NSTimeZone handles time zones

    (obviously) • You can get the user’s current time zone or use an ID like “America/Detroit” • Need to get a time zone for a geolocation? https://developers.google.com/maps/ documentation/timezone/ • Time zones are a pain in the ass always
  26. NSTimeZone • Represent “geopolitical regions” • America/Detroit • America/Indiana/Petersburg •

    Indiana is weird about Daylight Savings Time • Represent offsets from GMT • EST is -5 hours, EDT is -4
  27. NSTimeZone • NSTimeZone objects created with an ID know about

    Daylight Savings Time:
 
 NSTimeZone *tz = [NSTimeZone timeZoneWithName:@"America/Detroit"];
 BOOL isCurrentlyDST = [tz isDaylightSavingTimeForDate:[NSDate date]]; • This may change during the time your application is open!
 
 NSDate *timeZoneChange = [tz nextDaylightSavingTimeTransition];
  28. Where’s All That Date Stuff? • NSDateFormatter takes a date

    and turns it into a string • Since a date also includes time, this is also how you print a time of day • Takes advantage of user’s locale to print it properly • en_US: Monday, January 1, 2001 at 12:00:00 AM GMT • en_GB: Monday, 1 January 2001 00:00:00 GMT • fr_FR: lundi 1 janvier 2001 00:00:00 UTC
  29. More NSDateFormatter NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; ! NSDateFormatter *formatter

    = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterFullStyle; formatter.timeStyle = NSDateFormatterFullStyle; formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; ! NSLog(@"%@: %@", [formatter.locale localeIdentifier], [formatter stringFromDate:date]); ! ! Output: en_US: Monday, January 1, 2001 at 12:00:00 AM GMT en_GB: Monday, 1 January 2001 00:00:00 GMT fr_FR: lundi 1 janvier 2001 00:00:00 UTC
  30. More NSDateFormatter NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; ! NSDateFormatter *formatter

    = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterLongStyle; formatter.timeStyle = NSDateFormatterLongStyle; formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; ! NSLog(@"%@: %@", [formatter.locale localeIdentifier], [formatter stringFromDate:date]); ! ! Output: en_US: January 1, 2001 at 12:00:00 AM GMT en_GB: 1 January 2001 00:00:00 GMT fr_FR: 1 janvier 2001 00:00:00 UTC
  31. More NSDateFormatter NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; ! NSDateFormatter *formatter

    = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterMediumStyle; formatter.timeStyle = NSDateFormatterMediumStyle; formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; ! NSLog(@"%@: %@", [formatter.locale localeIdentifier], [formatter stringFromDate:date]); ! ! Output: en_US: Jan 1, 2001, 12:00:00 AM en_GB: 1 Jan 2001 00:00:00 fr_FR: 1 janv. 2001 00:00:00
  32. More NSDateFormatter NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; ! NSDateFormatter *formatter

    = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterShortStyle; formatter.timeStyle = NSDateFormatterShortStyle; formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; ! NSLog(@"%@: %@", [formatter.locale localeIdentifier], [formatter stringFromDate:date]); ! ! Output: en_US: 1/1/01, 12:00 AM en_GB: 01/01/2001 00:00 fr_FR: 01/01/2001 00:00
  33. Never Do This int month, day, year; ! NSString *poorlyFormattedDate

    = [NSString stringWithFormat:@"%d/%d/%d", month, day, year];
  34. Because You’ll End Up With This int month, day, year;

    ! NSString *poorlyFormattedDate; ! if ([[[NSLocale systemLocale] identifier] isEqualToString:@"en_US"]) { poorlyFormattedDate = [NSString stringWithFormat:@"%d/%d/%d", month, day, year]; } else { poorlyFormattedDate = [NSString stringWithFormat:@"%d/%d/%d", day, month, year]; }
  35. More NSDateFormatter • Expensive to create • Create one, then

    re-use as needed. • You can specify your own format • Easy, cheap localization + internationalization
  36. Custom NSDateFormatter Formats NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; ! NSDateFormatter

    *rfc3339DateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; ! [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; ! NSLog(@"Date: %@", [rfc3339DateFormatter stringFromDate:date]); ! ! Output: Date: 2001-01-01T00:00:00Z
  37. API Shortcomings • Limited to the planet Earth • Space

    travelers will need to account for relativity, position relative to Earth, etc. Talk to NASA; I bet they have a library. • Time Zones • Always store your timestamps relative to GMT, then convert to your user’s time zone
  38. Relative Dates • You can use date components to populate

    labels with text like “2 hours ago,” “30 seconds ago,” etc. like Mail.app • Or just use @Mattt’s FormatterKit project
  39. Easter • How hard can all this stuff really be,

    anyway? • Let’s write a method to find the date components for Easter given a year. • Easter falls on the first Sunday after the full moon following the March equinox. • Easy, right?
  40. Computus Source: Wikipedia

  41. Objective-Computus - (NSDateComponents *)dateComponentsForEasterInYear:(NSInteger)year { NSDateComponents *components = [[NSDateComponents alloc]

    init]; components.year = year; // Source: http://en.wikipedia.org/wiki/Computus#Anonymous_Gregorian_algorithm NSInteger a = year % 19; NSInteger b = year / 100; NSInteger c = year % 100; NSInteger d = b / 4; NSInteger e = b % 4; NSInteger f = (b + 8) / 25; NSInteger g = (b - f + 1) / 3; NSInteger h = ((19 * a) + b - d - g + 15) % 30; NSInteger i = c / 4; NSInteger k = c % 4; NSInteger L = (32 + (2 * e) + (2 * i) - h - k) % 7; NSInteger m = (a + (11 * h) + (22 * L)) / 451; components.month = (h + L - (7 * m) + 114) / 31; components.day = ((h + L - (7 * m) + 114) % 31) + 1; return components; }
  42. Objective-Computus NSDateComponents *easterThisYear = [self dateComponentsForEasterInYear:2014]; ! NSLog(@"Easter this year

    is on day %d of month %d", easterThisYear.day, easterThisYear.month); ! ! Output: Easter this year is on day 20 of month 4
  43. Further Reading • Date and Time Programming Guide • NSHipster:

    NSDateComponents • NSHipster: NSFormatter • NSHipster: NSDataDetector