Objective c string formatter for distances
Asked Answered
S

7

10

I have a distance as a float and I'm looking for a way to format it nicely for human readers. Ideally, I'd like it to change from m to km as it gets bigger, and to round the number nicely. Converting to miles would be a bonus. I'm sure many people have had a need for one of these and I'm hoping that there's some code floating around somewhere.

Here's how I'd like the formats:

  • 0-100m: 47m (as a whole number)
  • 100-1000m: 325m or 320m (round to the nearest 5 or 10 meters)
  • 1000-10000m: 1.2km (round to nearest with one decimal place)
  • 10000m +: 21km

If there's no code available, how can I write my own formatter?

Thanks

Shinny answered 24/2, 2010 at 6:32 Comment(0)
V
25

None of these solutions really met what I was looking for, so I built on them:

#define METERS_TO_FEET  3.2808399
#define METERS_TO_MILES 0.000621371192
#define METERS_CUTOFF   1000
#define FEET_CUTOFF     3281
#define FEET_IN_MILES   5280

- (NSString *)stringWithDistance:(double)distance {
    BOOL isMetric = [[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue];

    NSString *format;

    if (isMetric) {
        if (distance < METERS_CUTOFF) {
            format = @"%@ metres";
        } else {
            format = @"%@ km";
            distance = distance / 1000;
        }
    } else { // assume Imperial / U.S.
        distance = distance * METERS_TO_FEET;
        if (distance < FEET_CUTOFF) {
            format = @"%@ feet";
        } else {
            format = @"%@ miles";
            distance = distance / FEET_IN_MILES;
        }
    }

    return [NSString stringWithFormat:format, [self stringWithDouble:distance]];
}

// Return a string of the number to one decimal place and with commas & periods based on the locale.
- (NSString *)stringWithDouble:(double)value {
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setLocale:[NSLocale currentLocale]];
    [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
    [numberFormatter setMaximumFractionDigits:1];
    return [numberFormatter stringFromNumber:[NSNumber numberWithDouble:value]];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    double distance = 5434.45;
    NSLog(@"%f meters is %@", distance, [self stringWithDistance:distance]);

    distance = 543.45;
    NSLog(@"%f meters is %@", distance, [self stringWithDistance:distance]);    

    distance = 234234.45;
    NSLog(@"%f meters is %@", distance, [self stringWithDistance:distance]);    
}
Vernation answered 16/4, 2011 at 6:43 Comment(4)
I liked this solution. Created a NSString+Distance-category based on it. Gist: gist.github.com/1397837Oatmeal
Thanks this is just what I need for CLLocationDistance.Janettejaneva
What license/terms are associated with this category. I'm planning on using it in my app. In my licenses VC, would you like arbitration?Inconsiderate
Thanx my colleague jus copy pasted your answer in to our project. I was refactoring some classes and I was curios if he was able to write something like this. Seems not! Thanks stack for doing the job for so many developers. For this +1 for your work because I don't think he up voted your answer.Olgaolguin
U
23

iOS 7 and OS X 10.9 introduced MKDistanceFormatter for formatting distances:

Code Example:

double myDistance = 21837.0f;

MKDistanceFormatter *df = [[MKDistanceFormatter alloc]init];
df.unitStyle = MKDistanceFormatterUnitStyleAbbreviated;

NSLog(@"myDistance is %@", [df stringFromDistance: myDistance]);

Update:

It seems that MKDistanceFormatter is rounding the input value somehow. E.g. when I set myDistance to 111.0 I get "100 m".

Unbalanced answered 1/2, 2014 at 16:55 Comment(0)
B
17

Yes you need to write your own formatter, like

#include <math.h>
NSString* convertDistanceToString(float distance) {
    if (distance < 100)
       return [NSString stringWithFormat:@"%g m", roundf(distance)];
    else if (distance < 1000)
       return [NSString stringWithFormat:@"%g m", roundf(distance/5)*5];
    else if (distance < 10000)
       return [NSString stringWithFormat:@"%g km", roundf(distance/100)/10];
    else
       return [NSString stringWithFormat:@"%g km", roundf(distance/1000)];
}
...
NSLog(@"The distance is %@", convertDistanceToString(1024));
Breuer answered 24/2, 2010 at 6:37 Comment(1)
Thanks very much for the answer, this was perfect for what I needed (and saved me lots of time).Shinny
L
4

NSLengthFormatter which was introduced with iOS 8 and OS X 10.10 is an option people should be aware of.

    NSLengthFormatter *lengthFormatter = [[NSLengthFormatter alloc] init];
    lengthFormatter.unitStyle = NSFormattingUnitStyleShort;

    NSLog(@"distance = %@", [lengthFormatter stringFromMeters:meters]);

However, NSLengthFormatter has doesn't use Imperial units in those locales where they use metric except for distances.

Liturgics answered 2/9, 2015 at 10:35 Comment(1)
This would be nice if it returned feet instead of yards.Stamp
P
2

Here's how I do it. This uses the locale of the user to properly format the string, which you should probably do too.

// Returns a string representing the distance in the correct units.
// If distance is greater than convert max in feet or meters, 
// the distance in miles or km is returned instead
NSString* getDistString(float distance, int convertMax, BOOL includeUnit) {
  NSString * unitName;
  if (METRIC) {
    unitName = @"m";
    if (convertMax != -1 && distance > convertMax) {
      unitName = @"km";
      distance = distance / 1000;
    }
  } else {
    unitName = @"ft";
    if (convertMax != -1 && distance > convertMax) {
      unitName = @"mi";
      distance = distance / 5280;
    }
    distance = metersToFeet(distance);
  }

  if (includeUnit) return [NSString stringWithFormat:@"%@ %@", formatDecimal_1(distance), unitName];

  return formatDecimal_1(distance);

}
// returns a string if the number with one decimal place of precision
// sets the style (commas or periods) based on the locale
NSString * formatDecimal_1(float num) {
  static NSNumberFormatter *numFormatter;
  if (!numFormatter) {
    numFormatter = [[[NSNumberFormatter alloc] init] retain];
    [numFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
    [numFormatter setLocale:[NSLocale currentLocale]];
    [numFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
    [numFormatter setMaximumFractionDigits:1];
    [numFormatter setMinimumFractionDigits:1];
  }

  return [numFormatter stringFromNumber:F(num)];

}
Prickle answered 24/2, 2010 at 6:53 Comment(5)
Thanks very much. I hadn't thought of using localization for the number formats. I'll have to take the time to add this to the code I used.Shinny
How do you know if the user wants METRIC or not?Vernation
If they don't live in the US, we default to metric. And they can switch it. You can check NSLocale to get their location.Prickle
numFormatter = [[[NSNumberFormatter alloc] init] retain]; alloc already does a retain. you are retaining too many timesMansur
A common error is assuming that the UK uses Metric. In practice most UK citizens still use miles and pints, and stones and lbs, although our petrol is now priced in litres.Curettage
H
0

found this today asking the same question....going with :

 NSString *rvalue;
    if (value > 1000) {
        rvalue = [NSString stringWithFormat:@"%.02f km",value];
    }else {
        rvalue = [NSString stringWithFormat:@"%.02f m",value];
    }

could wrap this in a method, if need be

Hagioscope answered 18/4, 2010 at 1:17 Comment(0)
F
-1

For those looking for a swift 2.0 version. Ported from @MattDiPasquale

 extension Double {
  func toDistanceString() -> String {
    let METERS_TO_FEET = 3.2808399
    let METERS_CUTOFF = 1000.0
    let FEET_CUTOFF = 3281.0
    let FEET_IN_MILES =  5280.0

    let format:String
    if (NSLocale.isMetric()) {
      if (self < METERS_CUTOFF) {
        format = "\(self.stringWithDecimals(0)) metres"
      } else {
        format = "\((self / 1000).stringWithDecimals(1)) km";
      }
    } else { // assume Imperial / U.S.
      let feet = self * METERS_TO_FEET;
      if (feet < FEET_CUTOFF) {
        format = "\(feet.stringWithDecimals(0)) feet";
      } else {
        format = "\((self / FEET_IN_MILES).stringWithDecimals(1)) miles";
      }
    }
    return format
  }

  func stringWithDecimals(decimals:Int) -> String {
    return String(format: "%.\(decimals)f", self)
  }
}

extension NSLocale {
  class func isMetric() -> Bool {
    let locale = NSLocale.currentLocale()
    return locale.objectForKey(NSLocaleUsesMetricSystem) as! Bool
  }
}
Fluted answered 18/10, 2015 at 1:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.