Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

    View Slide

  2. Why are dates and times so
    hard?

    View Slide

  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

    View Slide

  4. NSDate
    typedef double NSTimeInterval;
    !
    #define NSTimeIntervalSince1970 978307200.0
    !
    @interface NSDate : NSObject
    !
    - (NSTimeInterval)timeIntervalSinceReferenceDate;
    !
    @end

    View Slide

  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?

    View Slide

  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

    View Slide

  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

    View Slide

  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.

    View Slide

  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

    View Slide

  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
    !

    View Slide

  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
    !
    !
    !

    View Slide

  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

    View Slide

  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];

    View Slide

  14. Extracting Components From a
    Date
    NSDateComponents *components =
    [gregorianCalendar components:NSWeekdayCalendarUnit
    fromDate:introductionDate];
    !
    NSLog(@"%d", components.weekday);
    !
    !
    Output: 3

    View Slide

  15. Calendar Units
    typedef NS_OPTIONS(NSUInteger, NSCalendarUnit) {
    NSCalendarUnitEra,
    NSCalendarUnitYear,
    NSCalendarUnitMonth,
    NSCalendarUnitDay,
    NSCalendarUnitHour,
    NSCalendarUnitMinute,
    NSCalendarUnitSecond,
    NSCalendarUnitWeekday,
    NSCalendarUnitWeekdayOrdinal,
    NSCalendarUnitQuarter,
    NSCalendarUnitWeekOfMonth,
    NSCalendarUnitWeekOfYear,
    NSCalendarUnitYearForWeekOfYear,
    NSCalendarUnitNanosecond,
    NSCalendarUnitCalendar,
    NSCalendarUnitTimeZone
    };

    View Slide

  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

    View Slide

  17. Comparing Dates
    • Use NSDate to see which of two dates occurred
    first:


    NSDate *date1, *date2;


    if ([date1 compare:date2] == NSOrderedAscending) {

    // date1 happened first

    }

    View Slide

  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];

    View Slide

  19. Never Do This
    NSDate *now = [NSDate date];
    !
    NSTimeInterval secondsInADay = 24.0 * 60.0 * 60.0;
    !
    NSDate *aWeekFromNow = [now dateByAddingTimeInterval:secondsInADay * 7.0];

    View Slide

  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];
    }
    }

    View Slide

  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

    View Slide

  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.

    View Slide

  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.

    View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

  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];

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  33. Never Do This
    int month, day, year;
    !
    NSString *poorlyFormattedDate =
    [NSString stringWithFormat:@"%d/%d/%d",
    month, day, year];

    View Slide

  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];
    }

    View Slide

  35. More NSDateFormatter
    • Expensive to create
    • Create one, then re-use as needed.
    • You can specify your own format
    • Easy, cheap localization + internationalization

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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?

    View Slide

  40. Computus
    Source: Wikipedia

    View Slide

  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;
    }

    View Slide

  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

    View Slide

  43. Further Reading
    • Date and Time Programming Guide
    • NSHipster: NSDateComponents
    • NSHipster: NSFormatter
    • NSHipster: NSDataDetector

    View Slide