Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Dates and Times in Cocoa (CocoaConf Columbus 2014)

Dates and Times in Cocoa (CocoaConf Columbus 2014)

Jeff Kelley

August 08, 2014
Tweet

More Decks by Jeff Kelley

Other Decks in Technology

Transcript

  1. 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
  2. NSDate typedef double NSTimeInterval; ! #define NSTimeIntervalSince1970 978307200.0 ! @interface

    NSDate : NSObject <NSCopying, NSSecureCoding> ! - (NSTimeInterval)timeIntervalSinceReferenceDate; ! @end
  3. 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?
  4. 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
  5. 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
  6. 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.
  7. 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
  8. Dumb Calendar Trick strax:~ 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 !
  9. Dumb Calendar Trick strax:~ 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 ! ! !
  10. Dumb Calendar Trick strax:~ 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
  11. 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];
  12. Creating a Date From Components let calendar = NSCalendar(calendarIdentifier: NSGregorianCalendar)

    ! var components = NSDateComponents() ! components.year = 1969 components.month = 7 components.day = 20 ! components.hour = 15 components.minute = 17 ! components.timeZone = NSTimeZone(name: "US/Eastern") ! var date = calendar.dateFromComponents(components)
  13. Creating a Date From Components NSCalendar *gregorianCalendar = [[NSCalendar alloc]

    initWithCalendarIdentifier:NSGregorianCalendar]; ! NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; ! dateComponents.year = 1986; dateComponents.month = 2; dateComponents.day = 9; ! NSDate *lastTime = [gregorianCalendar dateFromComponents:dateComponents]; ! NSDateComponents *nextComponents = [[NSDateComponents alloc] init]; ! nextComponents.year = 2061; nextComponents.month = 7; nextComponents.day = 28; ! NSDate *nextTime = [gregorianCalendar dateFromComponents:dateComponents];
  14. Creating a Date From Components let calendar = NSCalendar(calendarIdentifier: NSGregorianCalendar)

    ! var components = NSDateComponents() ! components.year = 1977 components.month = 5 components.day = 25 ! var date = calendar.dateFromComponents(components)
  15. Extracting Components From a Date NSDateComponents *components = [gregorianCalendar components:NSWeekdayCalendarUnit

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

    NSCalendarUnitHour, NSCalendarUnitMinute, NSCalendarUnitSecond, NSCalendarUnitWeekday, NSCalendarUnitWeekdayOrdinal, NSCalendarUnitQuarter, NSCalendarUnitWeekOfMonth, NSCalendarUnitWeekOfYear, NSCalendarUnitYearForWeekOfYear, NSCalendarUnitNanosecond, NSCalendarUnitCalendar, NSCalendarUnitTimeZone };
  17. Calendar Units struct NSCalendarUnit : RawOptionSet { init(_ value: UInt)

    var value: UInt static var CalendarUnitEra: NSCalendarUnit { get } static var CalendarUnitYear: NSCalendarUnit { get } static var CalendarUnitMonth: NSCalendarUnit { get } static var CalendarUnitDay: NSCalendarUnit { get } static var CalendarUnitHour: NSCalendarUnit { get } static var CalendarUnitMinute: NSCalendarUnit { get } static var CalendarUnitSecond: NSCalendarUnit { get } static var CalendarUnitWeekday: NSCalendarUnit { get } static var CalendarUnitWeekdayOrdinal: NSCalendarUnit { get } static var CalendarUnitQuarter: NSCalendarUnit { get } static var CalendarUnitWeekOfMonth: NSCalendarUnit { get } static var CalendarUnitWeekOfYear: NSCalendarUnit { get } static var CalendarUnitYearForWeekOfYear: NSCalendarUnit { get } static var CalendarUnitNanosecond: NSCalendarUnit { get } static var CalendarUnitCalendar: NSCalendarUnit { get } static var CalendarUnitTimeZone: NSCalendarUnit { get } }
  18. 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
  19. Comparing Dates • Use NSDate to see which of two

    dates occurred first:
 
 NSDate *date1, *date2;
 
 if ([date1 compare:date2] == NSOrderedAscending) {
 // date1 happened first
 }
  20. 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];
  21. Never Do This NSDate *now = [NSDate date]; ! NSTimeInterval

    secondsInADay = 24.0 * 60.0 * 60.0; ! NSDate *aWeekFromNow = [now dateByAddingTimeInterval:secondsInADay * 7.0];
  22. 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]; } }
  23. 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
  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) fromDate:romeFoundedDate toDate:now options:kNilOptions]; ! NSLog(@"It has been %d days since the Roman empire was founded.", difference.day); ! ! Output: It has been 1010369 days since the Roman empire was founded.
  25. 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 2766 years, 109 days since the Roman empire was founded.
  26. 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, 2766 years, 109 days since the Roman empire was founded.
  27. 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
  28. 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
  29. Indiana Time Zones • America/Indiana/Indianapolis • America/Indiana/Vincennes • America/Indiana/Winamac •

    America/Indiana/Marengo • America/Indiana/Petersburg • America/Indiana/Vevay • America/Indiana/Tell_City • America/Indiana/Knox
  30. 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];
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. Never Do This int month, day, year; ! NSString *poorlyFormattedDate

    = [NSString stringWithFormat:@"%d/%d/%d", month, day, year];
  37. 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]; }
  38. More NSDateFormatter • Expensive to create • Create one, then

    re-use as needed. • You can specify your own format • Easy, cheap localization + internationalization
  39. 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
  40. 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
  41. 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
  42. New Formatters in iOS 8 • NSDateComponentsFormatter
 var componentsFormatter =

    NSDateComponentsFormatter() ! var components = NSDateComponents() components.hour = 1 ! var formattedComponents = componentsFormatter.stringFromDateComponents(components)
  43. New Formatters in iOS 8 • NSDateIntervalFormatter
 var intervalFormatter =

    NSDateIntervalFormatter() ! var currentDate = NSDate() var future = currentDate.dateByAddingTimeInterval(10_000) ! var formattedInterval = intervalFormatter.stringFromDate(currentDate, toDate: future)
  44. 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?
  45. 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; }
  46. 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
  47. Swift-Computus func dateComponentsForEaster(year: Int) -> NSDateComponents { var components =

    NSDateComponents(); components.year = year let a = year % 19 let b = year / 100 let c = year % 100 let d = b / 4 let e = b % 4 let f = (b + 8) / 25 let g = (b - f + 1) / 3 let h = ((19 * a) + b - d - g + 15) % 30 let i = c / 4 let k = c % 4 let L = (32 + (2 * e) + (2 * i) - h - k) % 7 let 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 } ! dateComponentsForEaster(2015).month // 4 dateComponentsForEaster(2015).day // 5
  48. Further Reading • Date and Time Programming Guide • NSHipster:

    NSDateComponents • NSHipster: NSFormatter • NSHipster: NSDataDetector