3

I'm using an NSDateFormatter to format a duration of time (see here).

I want to use a format that goes like this: "3 hours, 15 minutes".
Here's the format string I'm using: @"H' hours, 'm' minutes'".

The issue is that, for durations shorter than one hour, the result is something like "0 hours, 35 minutes".
I simply want to show "35 minutes" in cases like that.

Is there a way to tell NSDateFormatter not to show hours at all if there aren't any, or should I just use an if statement and construct the string manually?

Edit:
I'm well aware that this is easy to do manually with a couple extra lines, which is how I've done it in the past. I'm just wondering if the formatter has enough smarts to handle this on its own since it seems like a common problem.

Community
  • 1
  • 1
Nathan
  • 6,772
  • 11
  • 38
  • 55
  • Keep in mind that the NSDateFormatter version will produce nonsense if the time interval ever exceeds 24 hours. – Hot Licks Sep 02 '12 at 19:59
  • Do you also want the output not to include "0 minutes"? For example, instead of "1 hour 0 minutes" just output "1 hour"? – Cameron Spickert Sep 02 '12 at 20:03
  • 1
    [This approach](http://stackoverflow.com/a/8128243/581994) is a better, simpler, and more efficient one than using NSDateFormatter. And, in fact, if one wanted, the omission of "0 hours" could be easily added to that solution with a simple C conditional. – Hot Licks Sep 02 '12 at 20:04
  • @HotLicks I'm not worried about > 24hr durations. The method you linked to is also a great option. – Nathan Sep 02 '12 at 20:09
  • 1
    @CameronSpickert I'm fine with '0 minutes' because it makes it clear that the time isn't just being rounded to the nearest hour (like trailing zeroes in a scientific measurement). – Nathan Sep 02 '12 at 20:09
  • 1
    Nathan, note that that solution was in the thread you referenced, if you had only scrolled down a bit. The "accepted" solution is not always the best. – Hot Licks Sep 02 '12 at 20:12

6 Answers6

5

As of iOS 8.0 and Mac OS X 10.10 (Yosemite), you can use NSDateComponentsFormatter. So lets use hours and minutes as allowedUnits (as in your example) and NSDateComponentsFormatterZeroFormattingBehaviorDropLeading as zeroFormattingBehavior to drop zero hours. Example:

NSTimeInterval intervalWithHours = 11700; // 3:15:00
NSTimeInterval intervalWithZeroHours = 2100; // 0:35:00
NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init];
formatter.allowedUnits = NSCalendarUnitHour | NSCalendarUnitMinute;
formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleFull;
formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropLeading;
NSString *string = [formatter stringFromTimeInterval:intervalWithHours];
NSString *stringWithoutHours = [formatter stringFromTimeInterval:intervalWithZeroHours];
NSLog(@"%@ / %@", string, stringWithoutHours);
// output: 3 hours, 15 minutes / 35 minutes

Swift 5 version:

let intervalWithHours: TimeInterval = 11700
let intervalWithZeroHours: TimeInterval = 2100
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .full
formatter.zeroFormattingBehavior = .dropLeading
if let string = formatter.string(from: intervalWithHours), let stringWithoutHours = formatter.string(from: intervalWithZeroHours) {
    print("\(string) / \(stringWithoutHours)")
    // output: "3 hours, 15 minutes / 35 minutes\n"
}
Dmitry Zhukov
  • 1,809
  • 2
  • 27
  • 35
2

Once you have the date string (let's call it foo) just make another string by sending stringByReplacingOccurencesOfString:withString: to it. For example:

NSString* foo = @"0 hours, 35 minutes";
NSString* bar = [foo stringByReplacingOccurrencesOfString:@"0 hours, " withString:@"" options:0 range:NSMakeRange(0,9)];
NSLog(@"%@",bar);

Output is:

35 minutes

I doubt this is very efficient compared to other possible methods, but unless you have to do it to hundreds of strings it probably won't matter much.

EDIT: Also, if you want to be able to get a date without the 0 hours, and assuming you're using stringFromDate: you could add a category to add a method like this:

- (NSString *)stringFromDateWithoutZeroHours:(NSDate *)date{
    result = [self stringFromDate:date];
    return [result stringByReplacingOccurrencesOfString:@"0 hours, " withString:@"" options:0 range:NSMakeRange(0,9)];
}
Metabble
  • 11,773
  • 1
  • 16
  • 29
  • Would this still work for @"10 hours, 35 minutes"? I think you would need to include the range. – bbarnhart Sep 03 '12 at 03:03
  • Excellent point! I didn't think of that. Luckily, we know the range; it's how long the beginning part we want to remove is, including the space. This will now work in all cases, since you're example would shift the match out of range. Updating now. – Metabble Sep 03 '12 at 03:18
1

Why not create your own NSDateFormatter subclass and override stringFromDate (if that's what you're calling) for this one case?

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    Uh, why not just have an `if` statement and select from one of two formats? That's 5 extra lines of code vs about 30 for a subclass. – Hot Licks Sep 02 '12 at 19:39
  • It wouldn't be 30 lines. It would be about 5 lines, and, more important, they'd be in the right place. An "if" where you should be subclassing is a "bad smell". Instead of testing, just make the formatter do the thing you want, from the get-go. – matt Sep 03 '12 at 01:19
  • 1
    "Should be subclassing"??? For logic that doesn't even belong in NSDateFormatter to begin with, but can be more quickly and clearly done differently? You'd create a subclass (two additional files to manage) for that? – Hot Licks Sep 03 '12 at 01:30
  • I had to pass a NSDateFormatter to CorePlot for axis label and I used @matt approach successfully: – Jens Schwarzer Oct 10 '13 at 12:09
1

Here's one way to do it using NSCalendar and NSDateComponents:

static NSString *stringWithTimeInterval (NSTimeInterval interval)
{
    NSDate *referenceDate = [NSDate dateWithTimeIntervalSince1970:0];
    NSDate *intervalDate = [NSDate dateWithTimeIntervalSince1970:interval];

    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components = [calendar components:NSHourCalendarUnit|NSMinuteCalendarUnit fromDate:referenceDate toDate:intervalDate options:0];

    NSMutableArray *stringComponents = [NSMutableArray array];

    NSInteger hours = [components hour];
    NSInteger minutes = [components minute];

    if (hours > 0) {
        [stringComponents addObject:[NSString stringWithFormat:@"%ld hours", hours]];
    }
    [stringComponents addObject:[NSString stringWithFormat:@"%ld minutes", minutes]];

    return [stringComponents componentsJoinedByString:@" "];
}
Cameron Spickert
  • 5,190
  • 1
  • 27
  • 34
0

OK, I'm going to steal some code from Rob Mayoff:

His original (slightly modified):

-(NSString*) doit:(NSTimeInterval)timeInterval {
    NSUInteger seconds = (NSUInteger)round(timeInterval);
    NSString *result = [NSString stringWithFormat:@"%u hours, %u minutes", seconds / 3600, (seconds / 60) % 60];
    return result;
}

Simple-minded modification to return with/without "hours":

-(NSString*) doit:(NSTimeInterval)timeInterval {
    NSString* result = nil;
    NSUInteger seconds = (NSUInteger)round(timeInterval);
    NSUInteger hours = seconds / 3600;
    NSUInteger minutes = (seconds / 60) % 60;
    if (hours > 0) {
        result = [NSString stringWithFormat:@"%u hours, %u minutes", hours, minutes];
    }
    else {
        result = [NSString stringWithFormat:@"%u minutes", minutes];
    }
    return result;
}

Again, but eliminating the if:

-(NSString*) doit:(NSTimeInterval)timeInterval {
    NSUInteger seconds = (NSUInteger)round(timeInterval);
    NSUInteger hours = seconds / 3600;
    NSUInteger minutes = (seconds / 60) % 60;
    NSString* result = hours ? [NSString stringWithFormat:@"%u hours, %u minutes", hours, minutes] : [NSString stringWithFormat:@"%u minutes", minutes];
    return result;
}

Now, I could get it down to an absurd 1-liner using ,, but that would be kind of silly and would not make the code any clearer.

(And note that all of the schemes that use NSDateFormatter are broken due to the iOS 12/24 "fechure".)

Community
  • 1
  • 1
Hot Licks
  • 47,103
  • 17
  • 93
  • 151
0

Based on @matt's answer I have successfully used his approach to make a conditional date formatting. I my case I just wanted the month every 1st day in the month and otherwise just the day. I needed to provide a date formatter to CorePlot for axis formatting and it had to be conditional.

@interface MyNSDateFormatter : NSDateFormatter
@property (nonatomic) NSDateFormatter *dateFormatterFirstDay;
@property (nonatomic) NSDateFormatter *dateFormatterTheRest;
@end

@implementation MyNSDateFormatter
-(id)init
{
    self = [super init];
    if(self != nil)
        {
        _dateFormatterFirstDay = [[NSDateFormatter alloc] init];
        _dateFormatterTheRest = [[NSDateFormatter alloc] init];

        [_dateFormatterFirstDay setDateFormat:@"MMM"];
        [_dateFormatterTheRest setDateFormat:@"d"];
    }
    return self;
}

-(NSString *)stringFromDate:(NSDate *)date
{
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSDayCalendarUnit fromDate:date];
    if ([dateComponents day] == 1) return [_dateFormatterFirstDay stringFromDate:date];
    else return [_dateFormatterTheRest stringFromDate:date];
}
@end
Jens Schwarzer
  • 2,840
  • 1
  • 22
  • 35