This repository was archived by the owner on Aug 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Distance formatter fixes #3331 #7585
Closed
+444
−0
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[ios, macos] added a distance formatter
commit c69d24d1b776df0f7381cc8144f9f223b65202e5
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
#import <Foundation/Foundation.h> | ||
#import <CoreLocation/CoreLocation.h> | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
/** | ||
`MGLDistanceFormatter` implements a formatter object meant to be used for | ||
geographic distances. By default, the resulting string will be based on the user’s | ||
current locale but can be overriden in order to force a measurement system of your choice. | ||
*/ | ||
@interface MGLDistanceFormatter : NSFormatter | ||
|
||
/** | ||
Determines what system of measurement to use. | ||
*/ | ||
typedef NS_ENUM(NSUInteger, MGLDistanceFormatterUnits) { | ||
// Measurement system will be based on the current locale. | ||
MGLDistanceFormatterUnitsDefault, | ||
// Use the metric system. | ||
MGLDistanceFormatterUnitsMetric, | ||
// Imperial units using feets. | ||
MGLDistanceFormatterUnitsImperial, | ||
// Imperial units using yards. | ||
MGLDistanceFormatterUnitsImperialWithYards | ||
}; | ||
|
||
/** | ||
Determines if the localized unit string should be abbreviated or spelled out. | ||
*/ | ||
typedef NS_ENUM(NSUInteger, MGLDistanceFormatterUnitStyle) { | ||
MGLDistanceFormatterUnitStyleDefault, | ||
// Abbreviate units (i.e. 1 km). | ||
MGLDistanceFormatterUnitStyleAbbreviated, | ||
// Full style (i.e. 2 kilometers). | ||
MGLDistanceFormatterUnitStyleFull | ||
}; | ||
|
||
/** | ||
Determines what system of measurement to use. | ||
|
||
Based on the user’s locale by default. | ||
*/ | ||
@property (nonatomic, assign) MGLDistanceFormatterUnits units; | ||
|
||
/** | ||
Determines how to output the localized unit string. | ||
*/ | ||
@property (nonatomic, assign) MGLDistanceFormatterUnitStyle unitStyle; | ||
|
||
/** | ||
Returns a localized formatted string for the provided distance. | ||
|
||
@param distance The distance, measured in meters. Negative distance will return nil. | ||
@return A localized formatted distance string including units. | ||
*/ | ||
- (NSString *)stringFromDistance:(CLLocationDistance)distance; | ||
|
||
/** | ||
This method is not supported for the `MGLDistanceFormatter` class. | ||
*/ | ||
- (BOOL)getObjectValue:(out id __nullable * __nullable)obj forString:(NSString *)string errorDescription:(out NSString * __nullable * __nullable)error; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
#import "MGLDistanceFormatter.h" | ||
|
||
#import "NSBundle+MGLAdditions.h" | ||
|
||
|
||
@interface MGLDistanceFormatter() | ||
@property (nonatomic) NSNumberFormatter *numberFormatter; | ||
@end | ||
|
||
@implementation MGLDistanceFormatter | ||
|
||
static const CLLocationDistance METERS_PER_KM = 1000; | ||
static const CLLocationDistance METERS_PER_MILE = 1609.34; | ||
static const CLLocationDistance METERS_PER_YARD = 0.9144; | ||
static const CLLocationDistance METERS_PER_FOOT = 0.3048; | ||
|
||
typedef NS_ENUM(NSUInteger, MGLDistanceFormatterUnit) { | ||
MGLDistanceFormatterUnitMeter, | ||
MGLDistanceFormatterUnitKilometer, | ||
MGLDistanceFormatterUnitFoot, | ||
MGLDistanceFormatterUnitMile, | ||
MGLDistanceFormatterUnitYard | ||
}; | ||
|
||
- (instancetype)init { | ||
if (self = [super init]) { | ||
_unitStyle = MGLDistanceFormatterUnitStyleDefault; | ||
|
||
_numberFormatter = [[NSNumberFormatter alloc] init]; | ||
_numberFormatter.minimumFractionDigits = 0; | ||
_numberFormatter.roundingMode = NSNumberFormatterRoundHalfUp; | ||
} | ||
return self; | ||
} | ||
|
||
- (NSString *)stringFromDistance:(CLLocationDistance)distance { | ||
if (distance < 0) | ||
return nil; | ||
|
||
NSString *format = [self formatForDistance:distance]; | ||
NSNumber *convertedDistance = @([self convertedDistance:distance]); | ||
|
||
MGLDistanceFormatterUnit unit = [self unitForDistance:distance]; | ||
|
||
switch (self.preferredUnits) { | ||
case MGLDistanceFormatterUnitsDefault: | ||
case MGLDistanceFormatterUnitsMetric: | ||
_numberFormatter.maximumFractionDigits = unit == MGLDistanceFormatterUnitMeter ? 0 : 1; | ||
return [NSString stringWithFormat:format, [self.numberFormatter numberFromString:[self.numberFormatter stringFromNumber:convertedDistance]]]; | ||
case MGLDistanceFormatterUnitsImperial: | ||
_numberFormatter.maximumFractionDigits = unit == MGLDistanceFormatterUnitFoot ? 0 : 1; | ||
return [NSString stringWithFormat:format, [self.numberFormatter numberFromString:[self.numberFormatter stringFromNumber:convertedDistance]]]; | ||
case MGLDistanceFormatterUnitsImperialWithYards: | ||
_numberFormatter.maximumFractionDigits = unit == MGLDistanceFormatterUnitYard ? 0 : 1; | ||
return [NSString stringWithFormat:format, [self.numberFormatter numberFromString:[self.numberFormatter stringFromNumber:convertedDistance]]]; | ||
} | ||
} | ||
|
||
- (CLLocationDistance)convertedDistance:(CLLocationDistance)distance { | ||
MGLDistanceFormatterUnit unit = [self unitForDistance:distance]; | ||
switch (unit) { | ||
case MGLDistanceFormatterUnitMeter: | ||
return distance; | ||
case MGLDistanceFormatterUnitKilometer: | ||
return distance / METERS_PER_KM; | ||
case MGLDistanceFormatterUnitFoot: | ||
return distance / METERS_PER_FOOT; | ||
case MGLDistanceFormatterUnitMile: | ||
return distance / METERS_PER_MILE; | ||
case MGLDistanceFormatterUnitYard: | ||
return distance / METERS_PER_YARD; | ||
} | ||
} | ||
|
||
- (NSString *)formatForDistance:(CLLocationDistance)distance { | ||
MGLDistanceFormatterUnit unit = [self unitForDistance:distance]; | ||
|
||
switch (self.unitStyle) { | ||
case MGLDistanceFormatterUnitStyleDefault: | ||
case MGLDistanceFormatterUnitStyleAbbreviated: | ||
switch (unit) { | ||
case MGLDistanceFormatterUnitMeter: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_METER_SHORT", @"Foundation", nil, @"%@ m", @"Distance format, short: {1 m}"); | ||
case MGLDistanceFormatterUnitKilometer: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_KILOMETER_SHORT", @"Foundation", nil, @"%@ km", @"Distance format, short: {1 km}"); | ||
case MGLDistanceFormatterUnitFoot: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_FOOT_SHORT", @"Foundation", nil, @"%@ ft", @"Distance format, short: {1 ft}"); | ||
case MGLDistanceFormatterUnitMile: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_MILE_SHORT", @"Foundation", nil, @"%@ mi", @"Distance format, short: {1 mi}"); | ||
case MGLDistanceFormatterUnitYard: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_YARD_SHORT", @"Foundation", nil, @"%@ yd", @"Distance format, short: {1 yd}"); | ||
} | ||
case MGLDistanceFormatterUnitStyleFull: | ||
switch (unit) { | ||
case MGLDistanceFormatterUnitMeter: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_METER_LONG", @"Foundation", nil, @"%@ meter(s)", @"Distance format, long: {1 meter}"); | ||
case MGLDistanceFormatterUnitKilometer: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_KILOMETER_LONG", @"Foundation", nil, @"%@ kilometer(s)", @"Distance format, long: {1 kilometer}"); | ||
case MGLDistanceFormatterUnitFoot: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_FOOT_LONG", @"Foundation", nil, @"%@ foot/feet", @"Distance format, long: {1 foot}"); | ||
case MGLDistanceFormatterUnitMile: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_MILE_LONG", @"Foundation", nil, @"%@ mile(s)", @"Distance format, long: {1 mile}"); | ||
case MGLDistanceFormatterUnitYard: | ||
return NSLocalizedStringWithDefaultValue(@"DISTANCE_YARD_LONG", @"Foundation", nil, @"%@ yard(s)", @"Distance format, long: {1 yard}"); | ||
} | ||
} | ||
} | ||
|
||
- (MGLDistanceFormatterUnit)unitForDistance:(CLLocationDistance)distance { | ||
switch (self.preferredUnits) { | ||
// MGLDistanceFormatterUnitsDefault is only added to make the switch statement | ||
// exhaustive, it is never returned from -[MGLDistanceFormatter preferredUnits] | ||
case MGLDistanceFormatterUnitsDefault: | ||
case MGLDistanceFormatterUnitsMetric: | ||
return distance < METERS_PER_KM ? MGLDistanceFormatterUnitMeter : MGLDistanceFormatterUnitKilometer; | ||
case MGLDistanceFormatterUnitsImperial: { | ||
double ft = distance / METERS_PER_FOOT; | ||
return ft < 1000 ? MGLDistanceFormatterUnitFoot : MGLDistanceFormatterUnitMile; | ||
} | ||
case MGLDistanceFormatterUnitsImperialWithYards: { | ||
double yd = distance / METERS_PER_YARD; | ||
return yd < 1000 ? MGLDistanceFormatterUnitYard : MGLDistanceFormatterUnitMile; | ||
} | ||
} | ||
} | ||
|
||
- (MGLDistanceFormatterUnits)preferredUnits { | ||
if (self.units == MGLDistanceFormatterUnitsDefault) { | ||
BOOL usesMetric = [[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue]; | ||
return usesMetric ? MGLDistanceFormatterUnitsMetric : MGLDistanceFormatterUnitsImperial; | ||
} | ||
return self.units; | ||
} | ||
|
||
- (BOOL)getObjectValue:(out id __nullable * __nullable)obj forString:(NSString *)string errorDescription:(out NSString * __nullable * __nullable)error { | ||
return NO; | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
#import <XCTest/XCTest.h> | ||
#import <Mapbox/Mapbox.h> | ||
|
||
@interface MGLDistanceFormatter(Private) | ||
@property (nonatomic) NSNumberFormatter *numberFormatter; | ||
@end | ||
|
||
@interface MGLDistanceFormatterTests : XCTestCase | ||
@end | ||
|
||
@implementation MGLDistanceFormatterTests | ||
|
||
- (void)testMetricAbbreviated { | ||
MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init]; | ||
formatter.units = MGLDistanceFormatterUnitsMetric; | ||
formatter.unitStyle = MGLDistanceFormatterUnitStyleAbbreviated; | ||
|
||
XCTAssertEqualObjects([formatter stringFromDistance:0], [self testStringWithDistance:0 formatter:formatter unit:@"m"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:10.5], [self testStringWithDistance:11 formatter:formatter unit:@"m"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:50], [self testStringWithDistance:50 formatter:formatter unit:@"m"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:500], [self testStringWithDistance:500 formatter:formatter unit:@"m"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:999], [self testStringWithDistance:999 formatter:formatter unit:@"m"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1000], [self testStringWithDistance:1 formatter:formatter unit:@"km"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1100], [self testStringWithDistance:1.1 formatter:formatter unit:@"km"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1100], [self testStringWithDistance:1.1 formatter:formatter unit:@"km"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:32450], [self testStringWithDistance:32.5 formatter:formatter unit:@"km"]); | ||
} | ||
|
||
- (void)testMetric { | ||
MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init]; | ||
formatter.units = MGLDistanceFormatterUnitsMetric; | ||
formatter.unitStyle = MGLDistanceFormatterUnitStyleFull; | ||
|
||
XCTAssertEqualObjects([formatter stringFromDistance:0], [self testStringWithDistance:0 formatter:formatter unit:@"meters"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1], [self testStringWithDistance:1 formatter:formatter unit:@"meter"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:2], [self testStringWithDistance:2 formatter:formatter unit:@"meters"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:500], [self testStringWithDistance:500 formatter:formatter unit:@"meters"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:999], [self testStringWithDistance:999 formatter:formatter unit:@"meters"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1000], [self testStringWithDistance:1 formatter:formatter unit:@"kilometer"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1500], [self testStringWithDistance:1.5 formatter:formatter unit:@"kilometers"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:2000], [self testStringWithDistance:2 formatter:formatter unit:@"kilometers"]); | ||
} | ||
|
||
- (void)testImperialAbbreviated { | ||
MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init]; | ||
formatter.units = MGLDistanceFormatterUnitsImperial; | ||
formatter.unitStyle = MGLDistanceFormatterUnitStyleAbbreviated; | ||
|
||
XCTAssertEqualObjects([formatter stringFromDistance:0], [self testStringWithDistance:0 formatter:formatter unit:@"ft"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:10], [self testStringWithDistance:33 formatter:formatter unit:@"ft"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:30.48], [self testStringWithDistance:100 formatter:formatter unit:@"ft"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:152.4], [self testStringWithDistance:500 formatter:formatter unit:@"ft"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1609.34], [self testStringWithDistance:1 formatter:formatter unit:@"mi"]); | ||
} | ||
|
||
- (void)testImperial { | ||
MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init]; | ||
formatter.units = MGLDistanceFormatterUnitsImperial; | ||
formatter.unitStyle = MGLDistanceFormatterUnitStyleFull; | ||
|
||
XCTAssertEqualObjects([formatter stringFromDistance:0], [self testStringWithDistance:0 formatter:formatter unit:@"feet"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:0.3048], [self testStringWithDistance:1 formatter:formatter unit:@"foot"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:0.6096], [self testStringWithDistance:2 formatter:formatter unit:@"feet"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:304.495], [self testStringWithDistance:999 formatter:formatter unit:@"feet"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1609.34], [self testStringWithDistance:1 formatter:formatter unit:@"mile"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1609.34], [self testStringWithDistance:1 formatter:formatter unit:@"mile"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:2414.01], [self testStringWithDistance:1.5 formatter:formatter unit:@"miles"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:3218.68], [self testStringWithDistance:2 formatter:formatter unit:@"miles"]); | ||
} | ||
|
||
- (void)testImperialWithYardsAbbreviated { | ||
MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init]; | ||
formatter.units = MGLDistanceFormatterUnitsImperialWithYards; | ||
formatter.unitStyle = MGLDistanceFormatterUnitStyleAbbreviated; | ||
|
||
XCTAssertEqualObjects([formatter stringFromDistance:0], [self testStringWithDistance:0 formatter:formatter unit:@"yd"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:457.2], [self testStringWithDistance:500 formatter:formatter unit:@"yd"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:913.486], [self testStringWithDistance:999 formatter:formatter unit:@"yd"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1609.34], [self testStringWithDistance:1 formatter:formatter unit:@"mi"]); | ||
} | ||
|
||
- (void)testImperialWithYards { | ||
MGLDistanceFormatter *formatter = [[MGLDistanceFormatter alloc] init]; | ||
formatter.units = MGLDistanceFormatterUnitsImperialWithYards; | ||
formatter.unitStyle = MGLDistanceFormatterUnitStyleFull; | ||
|
||
XCTAssertEqualObjects([formatter stringFromDistance:0], [self testStringWithDistance:0 formatter:formatter unit:@"yards"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:0.9144], [self testStringWithDistance:1 formatter:formatter unit:@"yard"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:457.2], [self testStringWithDistance:500 formatter:formatter unit:@"yards"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:913.486], [self testStringWithDistance:999 formatter:formatter unit:@"yards"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:1609.34], [self testStringWithDistance:1 formatter:formatter unit:@"mile"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:2414.02], [self testStringWithDistance:1.5 formatter:formatter unit:@"miles"]); | ||
XCTAssertEqualObjects([formatter stringFromDistance:3218.68], [self testStringWithDistance:2 formatter:formatter unit:@"miles"]); | ||
} | ||
|
||
- (NSString *)testStringWithDistance:(CLLocationDistance)distance formatter:(MGLDistanceFormatter *)formatter unit:(NSString *)unit { | ||
return [NSString stringWithFormat:@"%@ %@", [formatter.numberFormatter numberFromString:[formatter.numberFormatter stringFromNumber:@(distance)]], unit]; | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In XCTest, I think we should avoid writing utility methods that start with
test
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It only recognizes methods prefixed with
test
if the return type is void but I agree.