6
\$\begingroup\$

In my very specific scenario, I need to get an NSDate holding the next time the hour will be 7 (either AM or PM).

On the case that this is the first time to do this, I need to jump extra 12 hours (e.g., if I got Apr 1st 7AM - to jump it to 7PM).

After playing with playgrounds I was able to achieve what I believe solves this, but looking at my code it feels too hard-coded and full of magic numbers. I am wondering if there is a cleaner way to do this.

//constants
NSInteger secInHour = 60 * 60;
NSInteger secInMin = 60;
NSInteger NO_VALUE = NSIntegerMax;

//Break "now" to components
NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
cal.locale = [NSLocale localeWithLocaleIdentifier:@"he_IL"]; //force a 24hr locale - easier to calculate
NSInteger nowHr = [cal component:NSCalendarUnitHour fromDate:date];
NSInteger nowMin = [cal component:NSCalendarUnitMinute fromDate:date];
NSInteger nowSec = [cal component:NSCalendarUnitSecond fromDate:date];
NSTimeInterval nowMS = date.timeIntervalSince1970 - (double)((int)date.timeIntervalSince1970);

//Get closest "7" hour
BOOL isFirstTime = YES;
NSInteger firstTimeDelta = isFirstTime ? 12 : 0;

//find hour distance
NSInteger addHr = NO_VALUE;
BOOL alarmToConcludeDay = true;
if (nowHr >= 7 && nowHr < 19) {
    //now is day
    alarmToConcludeDay = true;
    addHr = 19 - nowHr - 1;
} else {
    //now is night
    alarmToConcludeDay = false;
    if (nowHr < 7) {
        addHr = 7 - nowHr - 1;
    } else {
        addHr = 7 - nowHr - 1 + 24;
    }
}

//sum everything
if (addHr != NO_VALUE) {
    //how much to add to now?
    NSInteger addMin = 60 - nowMin - 1;
    NSInteger addSec = 60 - nowSec - 1;
    NSInteger addMS = 1 - nowMS;

    NSTimeInterval addTotal = (double)((addHr + firstTimeDelta) * secInHour + addMin * secInMin + addSec) + addMS;

    NSDate *alarmDate = [date dateByAddingTimeInterval:addTotal];
}
\$\endgroup\$
6
\$\begingroup\$

There's a much easier approach to this problem.

We can use NSCalendar's nextDateAfterDate:matchingComponents:options: method to find both our next 7am and 7pm. Then it's simply a matter of comparing these values and returning the lesser of the two.

NSDate * nextSevenOClock() {
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components = [[NSDateComponents alloc] init];

    components.hour = 7;
    NSDate *nextSevenAm = [calendar nextDateAfterDate:[NSDate date]
                                   matchingComponents:components
                                              options:NSCalendarMatchNextTimePreservingSmallerUnits];

    components.hour = 19;
    NSDate *nextSevenPm = [calendar nextDateAfterDate:[NSDate date]
                                   matchingComponents:components
                                              options:NSCalendarMatchNextTimePreservingSmallerUnits];

    NSDate *nextSeven = nextSevenAm;
    if ([nextSeven compare:nextSevenPm] == NSOrderedDescending) {
        nextSeven = nextSevenPm;
    }

    return nextSeven;
}

Importantly, this approach with account correctly for things like daylight savings time and leap seconds, which the original solution does not.


This method is very specific however. It would be good if we could get a bit more utility out of it. And rather than a simple C-style function, I've expanded it properly into an NSDate category.

@implementation NSDate (NextTimeStamp)

+ (instancetype)nextTimeStampMatchingHour:(NSUInteger)hour {
    return [self nextTimeStampMatchingHour:hour minute:0 second:0];
}

+ (instancetype)nextTimeStampMatchingHour:(NSUInteger)hour minute:(NSUInteger)minute {
    return [self nextTimeStampMatchingHour:hour minute:minute second:0];
}

+ (instancetype)nextTimeStampMatchingHour:(NSUInteger)hour minute:(NSUInteger)minute second:(NSUInteger)second {
    NSUInteger morningHour = hour % 12;
    NSUInteger eveningHour = morningHour + 12;

    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components = [[NSDateComponents alloc] init];

    components.minute = minute;
    components.second = second;

    components.hour = morningHour;
    NSDate *nextAM = [calendar nextDateAfterDate:[NSDate date]
                              matchingComponents:components
                                         options:NSCalendarMatchNextTimePreservingSmallerUnits];

    components.hour = eveningHour;
    NSDate *nextPM = [calendar nextDateAfterDate:[NSDate date]
                              matchingComponents:components
                                         options:NSCalendarMatchNextTimePreservingSmallerUnits];

    return [nextAM compare:nextPM] == NSOrderedAscending ? nextAM : nextPM;
}

@end

For your original exxample of looking for the next 7 o'Clock, we can just do simply:

NSDate *nextSeven = [NSDate nextTimeStampMatchingHour:7];

But with these extra methods, we can look for more specific times:

NSDate *nextSevenFifteen = [NSDate nextTimeStampMatchingHour:7
                                                      minute:15];
\$\endgroup\$
  • \$\begingroup\$ This looks great I will go check it thanks! One thing though: Assuming now its XX:33 and 21 seconds, I don't want the next time it's 7:33:21, but when its 7:00:00, so shouldn't I use options:NSCalendarMatchNextTime instead options:NSCalendarMatchNextTimePreservingSmallerUnits? \$\endgroup\$ – Aviel Gross Mar 31 '15 at 12:36
  • \$\begingroup\$ It preserves the units from the components, which has 0 for everything less then the hour unit. \$\endgroup\$ – nhgrif Mar 31 '15 at 12:38

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.