$30 off During Our Annual Pro Sale. View Details »

Dates and Times in Cocoa

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.

Jeff Kelley

February 13, 2014
Tweet

More Decks by Jeff Kelley

Other Decks in Programming

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