2
\$\begingroup\$

I have this method that returns an array of all the hours for the past 24 hours, ex: if the current time is 3:24pm it looks like @[3pm,4pm...12am,1am...2pm,3pm];

Everything is working as it should, I was wondering if there was a more efficient (or pretty) way to do it than how I have it:

NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *today = [NSDate date];
NSDateComponents *comps = [calendar components:NSCalendarUnitHour|NSCalendarUnitDay fromDate:today];

NSMutableArray *units = [[NSMutableArray alloc] init];

int currentHour = (int)[comps hour];

for (int i = 0; i < 25; i++) {

    int hourToInsert = currentHour + i;

    NSString *insertString = @"";

    if (hourToInsert == 12 || hourToInsert == 36) {

        //special case for noon
        insertString = @"12PM";

        [units addObject:insertString];

    } else if (hourToInsert == 24) {

        //special case for midnight
        insertString = @"12AM";

        [units addObject:insertString];

    } else if (hourToInsert > 36) {

        //next day PM
        hourToInsert = hourToInsert - 36;

        insertString = [NSString stringWithFormat:@"%iPM", hourToInsert];

        [units addObject:insertString];

    } else if (hourToInsert > 24) {

        //next day
        hourToInsert = hourToInsert - 24;

        insertString = [NSString stringWithFormat:@"%iAM", hourToInsert];

        [units addObject:insertString];

    } else if (hourToInsert > 12) {

        hourToInsert = hourToInsert - 12;

        insertString = [NSString stringWithFormat:@"%iPM", hourToInsert];

        [units addObject:insertString];

    } else {

        insertString = [NSString stringWithFormat:@"%iAM", hourToInsert];

        [units addObject:insertString];

    }

}
\$\endgroup\$
2
\$\begingroup\$

You can simplify the logic with fewer conditional branches:

  • hour to insert = current hour + i, modulo 24
  • if hour to insert is 12 : special case 12AM
  • else if hour to insert is 0 : special case 12PM
  • else if hour to insert < 12 : use AM
  • otherwise use PM
  • lastly, you can do the [units addObject:insertString] step at the end, since it's the same for all hours
  • no need to initialize insertString, as you will set it in one of the conditional branches anyway
  • insertString is a poor name. hourString would be better, hourWithAmPm might be even better

Something like this:

for (int i = 0; i < 25; i++) {

    int hourToInsert = (currentHour + i) % 24;

    NSString *insertString;

    if (hourToInsert == 12) {

        //special case for noon
        insertString = @"12PM";

    } else if (hourToInsert == 0) {

        //special case for midnight
        insertString = @"12AM";

    } else if (hourToInsert < 12) {

        insertString = [NSString stringWithFormat:@"%iAM", hourToInsert];

    } else {

        insertString = [NSString stringWithFormat:@"%iPM", hourToInsert - 12];

    }

    [units addObject:insertString];
}
\$\endgroup\$
  • \$\begingroup\$ What exactly does the int hourToInsert = currentHour + i % 24; do? \$\endgroup\$ – Brandon Shega Jun 3 '15 at 20:22
  • \$\begingroup\$ I corrected to (currentHour + i) % 24. It returns the remainder after dividing by 24. For example, 39 % 24 is 15. \$\endgroup\$ – janos Jun 3 '15 at 20:24
  • \$\begingroup\$ Oh I see so it removes the need to check for anything higher than 24? \$\endgroup\$ – Brandon Shega Jun 3 '15 at 20:25
  • \$\begingroup\$ Excellent, works as intended and is much more readable. Thank you! \$\endgroup\$ – Brandon Shega Jun 3 '15 at 20:27
4
\$\begingroup\$

Instead of handling all the different cases (am, pm, special rules for midnight and noon) you can use a date formatter which does all these things for you:

NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"ha"; // h = 0..12, a = am/pm

NSString *string = [fmt stringFromDate:someDate];

Then note that not all people use the 12hour AM/PM system. You might consider to display the hours according to the locale/region that a user has configured on his computer. That is achieved simply by using the date format

fmt.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"h" options:0 locale:[NSLocale currentLocale]];

Another problem occurs when the clock is adjusted for the daylight saving time. For example, when DST starts in Germany, the hour is put forward at 2am by one hour, so that the time "2am" does not exist at all. Similarly, when DST ends and the hour is put back at 3am by one hour, then time "2am" occurs twice.

Therefore, instead of simply incrementing an "hour" variable, I would recommend to use calendrical calculations which handle all these situations correctly:

// A date formatter that prints hours according to the user's locale/region settings:
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"h" options:0 locale:[NSLocale currentLocale]];

NSCalendar *calendar = [NSCalendar currentCalendar];

// The last hour to print is the full hour of the current point in time:
NSDate *lastHour;
[calendar rangeOfUnit:NSCalendarUnitHour startDate:&lastHour interval:nil forDate:[NSDate date]];

// The first hour to print is one day earlier:
NSDate *firstHour = [calendar dateByAddingUnit:NSCalendarUnitDay value:-1 toDate:lastHour options:0];

NSMutableArray *units = [[NSMutableArray alloc] init];

// hour = firstHour ... lastHour:
NSDate *hour = firstHour;
do {
    [units addObject:[fmt stringFromDate:hour]];

    // Add one hour:
    hour = [calendar dateByAddingUnit:NSCalendarUnitHour value:1 toDate:hour options:0];
} while ([hour compare:lastHour] != NSOrderedDescending); // while hour <= lastHour
\$\endgroup\$
  • \$\begingroup\$ Oh my gosh, this is awesome. I asked a few weeks back on StackOverflow for help on how to do it this way and people just kept marking my post as duplicate and saying I should do more research. They were all showing me how to get HOW MANY hours are between two dates when I really needed the actual hours. Thank you for this! I do have one question, I am also doing this for the days during the week and the days in the last month, are there similar methods for doing this? I feel those 2 are pretty straightforward so I just used for loops but if you have any idea, I'm all ears :). Thanks again \$\endgroup\$ – Brandon Shega Jun 4 '15 at 13:07
  • \$\begingroup\$ @BrandonShega: That should be just a matter of choosing the required NSCalendarUnit..., e.g. NSCalendarUnitWeekOfYear instead of NSCalendarUnitDay to go back one week, and NSCalendarUnitDay instead of NSCalendarUnitHour when iterating forward, etc ... \$\endgroup\$ – Martin R Jun 4 '15 at 13:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.