This repository has been 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
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented an NSFormatter for formatting CLLocationDirection values as localizable display strings. The formatter supports both absolute compass directions, such as “south”, and relative “clock” directions, such as “6 o’clock”.
- Loading branch information
Showing
9 changed files
with
384 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
#import <Foundation/Foundation.h> | ||
#import <CoreLocation/CoreLocation.h> | ||
|
||
#import "MGLTypes.h" | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
/** | ||
The reference point for an `MGLDirectionFormatter`. | ||
*/ | ||
typedef NS_ENUM(NSUInteger, MGLDirectionFormatterOrigin) { | ||
/** | ||
Directions are assumed to be relative to true north and are given as | ||
compass directions, such as “south” for a value of `90`. | ||
*/ | ||
MGLDirectionFormatterOriginNorth = 0, | ||
/** | ||
Directions are assumed to be relative to the direction in which the user is | ||
facing and are given as “clock directions”, such as “6 o’clock” for a value | ||
of `90`. | ||
*/ | ||
MGLDirectionFormatterOriginStraightAhead, | ||
}; | ||
|
||
/** | ||
The `MGLDirectionFormatter` class provides properly formatted descriptions of | ||
absolute or relative headings. Use this class to create localized heading | ||
strings when displaying directional information to users. | ||
*/ | ||
@interface MGLDirectionFormatter : NSFormatter | ||
|
||
/** | ||
The receiver’s reference point. The receiver’s input is assumed to be relative | ||
to this reference point, and its output is given in the conventional form for | ||
directions with this reference point. | ||
This class does not convert between different reference points. To convert an | ||
`CLLocationDirection` with respect to true north into a `CLLocationDirection` | ||
with respect to the direction in which the user is currently facing, use Core | ||
Location to determine the user’s current heading. | ||
The default value of this property is `MGLDirectionFormatterOriginNorth`, which | ||
means a value of `0` is formatted as “north” in the receiver’s locale. | ||
*/ | ||
@property (nonatomic) MGLDirectionFormatterOrigin origin; | ||
|
||
/** | ||
The unit style used by this formatter. | ||
This property defaults to `NSFormattingUnitStyleMedium`. | ||
*/ | ||
@property (nonatomic) NSFormattingUnitStyle unitStyle; | ||
|
||
/** | ||
The locale of the receiver. | ||
The locale determines the output language as well as the numeral system used | ||
when the `relativeToUser` property is set to `YES`. | ||
*/ | ||
@property (copy) NSLocale *locale; | ||
|
||
/** | ||
Returns a heading string for the provided value. | ||
@param direction The heading, measured in degrees. | ||
@return The heading string appropriately formatted for the formatter’s locale. | ||
*/ | ||
- (NSString *)stringFromDirection:(CLLocationDirection)direction; | ||
|
||
/** | ||
This method is not supported for the `MGLDirectionFormatter` 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,167 @@ | ||
#import "MGLDirectionFormatter.h" | ||
|
||
#define wrap(value, min, max) \ | ||
(fmod((fmod((value - min), (max - min)) + (max - min)), (max - min)) + min) | ||
|
||
@implementation MGLDirectionFormatter { | ||
NSNumberFormatter *_numberFormatter; | ||
} | ||
|
||
- (instancetype)init { | ||
if (self = [super init]) { | ||
_unitStyle = NSFormattingUnitStyleMedium; | ||
_numberFormatter = [[NSNumberFormatter alloc] init]; | ||
} | ||
return self; | ||
} | ||
|
||
- (NSLocale *)locale { | ||
return _numberFormatter.locale; | ||
} | ||
|
||
- (void)setLocale:(NSLocale *)locale { | ||
_numberFormatter.locale = locale; | ||
} | ||
|
||
- (NSString *)stringFromDirection:(CLLocationDirection)direction { | ||
if (self.origin == MGLDirectionFormatterOriginNorth) { | ||
return [self stringFromAbsoluteDirection:direction]; | ||
} else { | ||
return [self stringFromRelativeDirection:direction]; | ||
} | ||
} | ||
|
||
- (NSString *)stringFromRelativeDirection:(CLLocationDirection)direction { | ||
NSInteger hour = round(-wrap(-direction, -360, 0) / 360 * 12); | ||
NSString *format; | ||
NSNumberFormatterStyle style = NSNumberFormatterDecimalStyle; | ||
switch (self.unitStyle) { | ||
case NSFormattingUnitStyleShort: | ||
format = NSLocalizedString(@"%@:00", @"Relative heading format, short style"); | ||
break; | ||
|
||
case NSFormattingUnitStyleMedium: | ||
format = NSLocalizedString(@"%@ o’clock", @"Relative heading format, medium style"); | ||
|
||
break; | ||
|
||
case NSFormattingUnitStyleLong: | ||
format = NSLocalizedString(@"%@ o’clock", @"Relative heading format, long style"); | ||
style = NSNumberFormatterSpellOutStyle; | ||
break; | ||
|
||
default: | ||
break; | ||
} | ||
_numberFormatter.numberStyle = style; | ||
return [NSString stringWithFormat:format, [_numberFormatter stringFromNumber:@(hour)]]; | ||
} | ||
|
||
- (NSString *)stringFromAbsoluteDirection:(CLLocationDirection)direction { | ||
static NS_ARRAY_OF(NSString *) *shortStrings; | ||
static NS_ARRAY_OF(NSString *) *longStrings; | ||
static dispatch_once_t onceToken; | ||
dispatch_once(&onceToken, ^{ | ||
shortStrings = @[ | ||
NSLocalizedString(@"N", @"North, short"), | ||
NSLocalizedString(@"N×E", @"North by east, short"), | ||
NSLocalizedString(@"NNE", @"North-northeast, short"), | ||
NSLocalizedString(@"NE×N", @"Northeast by north, short"), | ||
NSLocalizedString(@"NE", @"Northeast, short"), | ||
NSLocalizedString(@"NE×E", @"Northeast by east, short"), | ||
NSLocalizedString(@"ENE", @"East-northeast, short"), | ||
NSLocalizedString(@"E×N", @"East by north, short"), | ||
|
||
NSLocalizedString(@"E", @"East, short"), | ||
NSLocalizedString(@"E×S", @"East by south, short"), | ||
NSLocalizedString(@"ESE", @"East-southeast, short"), | ||
NSLocalizedString(@"SE×E", @"Southeast by east, short"), | ||
NSLocalizedString(@"SE", @"Southeast, short"), | ||
NSLocalizedString(@"SE×S", @"Southeast by south, short"), | ||
NSLocalizedString(@"SSE", @"South-southeast, short"), | ||
NSLocalizedString(@"S×E", @"South by east, short"), | ||
|
||
NSLocalizedString(@"S", @"South, short"), | ||
NSLocalizedString(@"S×W", @"South by west, short"), | ||
NSLocalizedString(@"SSW", @"South-southwest, short"), | ||
NSLocalizedString(@"SW×S", @"Southwest by south, short"), | ||
NSLocalizedString(@"SW", @"Southwest, short"), | ||
NSLocalizedString(@"SW×W", @"Southwest by west, short"), | ||
NSLocalizedString(@"WSW", @"West-southwest, short"), | ||
NSLocalizedString(@"W×S", @"West by south, short"), | ||
|
||
NSLocalizedString(@"W", @"West, short"), | ||
NSLocalizedString(@"W×N", @"West by north, short"), | ||
NSLocalizedString(@"WNW", @"West-northwest, short"), | ||
NSLocalizedString(@"NW×W", @"Northwest by west, short"), | ||
NSLocalizedString(@"NW", @"Northwest, short"), | ||
NSLocalizedString(@"NW×N", @"Northwest by north, short"), | ||
NSLocalizedString(@"NNW", @"North-northwest, short"), | ||
NSLocalizedString(@"N×W", @"North by west, short"), | ||
]; | ||
|
||
longStrings = @[ | ||
NSLocalizedString(@"north", @"North, long"), | ||
NSLocalizedString(@"north by east", @"North by east, long"), | ||
NSLocalizedString(@"north-northeast", @"North-northeast, long"), | ||
NSLocalizedString(@"northeast by north", @"Northeast by north, long"), | ||
NSLocalizedString(@"northeast", @"Northeast, long"), | ||
NSLocalizedString(@"northeast by east", @"Northeast by east, long"), | ||
NSLocalizedString(@"east-northeast", @"East-northeast, long"), | ||
NSLocalizedString(@"east by north", @"East by north, long"), | ||
|
||
NSLocalizedString(@"east", @"East, long"), | ||
NSLocalizedString(@"east by south", @"East by south, long"), | ||
NSLocalizedString(@"east-southeast", @"East-southeast, long"), | ||
NSLocalizedString(@"southeast by east", @"Southeast by east, long"), | ||
NSLocalizedString(@"southeast", @"Southeast, long"), | ||
NSLocalizedString(@"southeast by south", @"Southeast by south, long"), | ||
NSLocalizedString(@"south-southeast", @"South-southeast, long"), | ||
NSLocalizedString(@"south by east", @"South by east, long"), | ||
|
||
NSLocalizedString(@"south", @"South, long"), | ||
NSLocalizedString(@"south by west", @"South by west, long"), | ||
NSLocalizedString(@"south-southwest", @"South-southwest, long"), | ||
NSLocalizedString(@"southwest by south", @"Southwest by south, long"), | ||
NSLocalizedString(@"southwest", @"Southwest, long"), | ||
NSLocalizedString(@"southwest by west", @"Southwest by west, long"), | ||
NSLocalizedString(@"west-southwest", @"West-southwest, long"), | ||
NSLocalizedString(@"west by south", @"West by south, long"), | ||
|
||
NSLocalizedString(@"west", @"West, long"), | ||
NSLocalizedString(@"west by north", @"West by north, long"), | ||
NSLocalizedString(@"west-northwest", @"West-northwest, long"), | ||
NSLocalizedString(@"northwest by west", @"Northwest by west, long"), | ||
NSLocalizedString(@"northwest", @"Northwest, long"), | ||
NSLocalizedString(@"northwest by north", @"Northwest by north, long"), | ||
NSLocalizedString(@"north-northwest", @"North-northwest, long"), | ||
NSLocalizedString(@"north by west", @"North by west, long"), | ||
]; | ||
|
||
NSAssert(shortStrings.count == longStrings.count, @"Long and short direction string arrays must have the same size."); | ||
}); | ||
|
||
NSInteger cardinalPoint = round(wrap(direction, 0, 360) / 360 * shortStrings.count); | ||
switch (self.unitStyle) { | ||
case NSFormattingUnitStyleShort: | ||
return shortStrings[cardinalPoint]; | ||
|
||
case NSFormattingUnitStyleMedium: | ||
case NSFormattingUnitStyleLong: | ||
return longStrings[cardinalPoint]; | ||
} | ||
} | ||
|
||
- (nullable NSString *)stringForObjectValue:(id)obj { | ||
if (![obj isKindOfClass:[NSValue class]]) { | ||
return nil; | ||
} | ||
return [self stringFromDirection:[obj doubleValue]]; | ||
} | ||
|
||
- (BOOL)getObjectValue:(out id __nullable * __nullable)obj forString:(NSString *)string errorDescription:(out NSString * __nullable * __nullable)error { | ||
NSAssert(NO, @"-getObjectValue:forString:errorDescription: has not been implemented"); | ||
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,111 @@ | ||
#import <Mapbox/Mapbox.h> | ||
#import <XCTest/XCTest.h> | ||
|
||
static NSString * const MGLTestLocaleIdentifier = @"en-US"; | ||
|
||
@interface MGLDirectionFormatterTests : XCTestCase | ||
|
||
@end | ||
|
||
@implementation MGLDirectionFormatterTests | ||
|
||
- (void)testAbsoluteDirections { | ||
MGLDirectionFormatter *shortFormatter = [[MGLDirectionFormatter alloc] init]; | ||
XCTAssertEqual(shortFormatter.origin, MGLDirectionFormatterOriginNorth, @"Reference point should be north by default."); | ||
shortFormatter.unitStyle = NSFormattingUnitStyleShort; | ||
shortFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier]; | ||
|
||
MGLDirectionFormatter *mediumFormatter = [[MGLDirectionFormatter alloc] init]; | ||
XCTAssertEqual(mediumFormatter.unitStyle, NSFormattingUnitStyleMedium, @"Unit style should be medium by default."); | ||
mediumFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier]; | ||
|
||
MGLDirectionFormatter *longFormatter = [[MGLDirectionFormatter alloc] init]; | ||
longFormatter.unitStyle = NSFormattingUnitStyleLong; | ||
longFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier]; | ||
|
||
XCTAssertEqualObjects(@"NW", [shortFormatter stringFromDirection:-45]); | ||
XCTAssertEqualObjects(@"northwest", [mediumFormatter stringFromDirection:-45]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:-45], [longFormatter stringFromDirection:-45]); | ||
|
||
XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:0]); | ||
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:0]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:0], [longFormatter stringFromDirection:0]); | ||
|
||
XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:1]); | ||
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:1]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:1], [longFormatter stringFromDirection:1]); | ||
|
||
XCTAssertEqualObjects(@"N×E", [shortFormatter stringFromDirection:10]); | ||
XCTAssertEqualObjects(@"north by east", [mediumFormatter stringFromDirection:10]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:10], [longFormatter stringFromDirection:10]); | ||
|
||
XCTAssertEqualObjects(@"NNE", [shortFormatter stringFromDirection:20]); | ||
XCTAssertEqualObjects(@"north-northeast", [mediumFormatter stringFromDirection:20]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:20], [longFormatter stringFromDirection:20]); | ||
|
||
XCTAssertEqualObjects(@"NE", [shortFormatter stringFromDirection:45]); | ||
XCTAssertEqualObjects(@"northeast", [mediumFormatter stringFromDirection:45]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:45], [longFormatter stringFromDirection:45]); | ||
|
||
XCTAssertEqualObjects(@"E", [shortFormatter stringFromDirection:90]); | ||
XCTAssertEqualObjects(@"east", [mediumFormatter stringFromDirection:90]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:90], [longFormatter stringFromDirection:90]); | ||
|
||
XCTAssertEqualObjects(@"S", [shortFormatter stringFromDirection:180]); | ||
XCTAssertEqualObjects(@"south", [mediumFormatter stringFromDirection:180]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:180], [longFormatter stringFromDirection:180]); | ||
|
||
XCTAssertEqualObjects(@"W", [shortFormatter stringFromDirection:270]); | ||
XCTAssertEqualObjects(@"west", [mediumFormatter stringFromDirection:270]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:270], [longFormatter stringFromDirection:270]); | ||
|
||
XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:360]); | ||
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:360]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:360], [longFormatter stringFromDirection:360]); | ||
|
||
XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:720]); | ||
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:720]); | ||
XCTAssertEqualObjects([mediumFormatter stringFromDirection:720], [longFormatter stringFromDirection:720]); | ||
} | ||
|
||
- (void)testRelativeDirections { | ||
MGLDirectionFormatter *shortFormatter = [[MGLDirectionFormatter alloc] init]; | ||
shortFormatter.origin = MGLDirectionFormatterOriginStraightAhead; | ||
shortFormatter.unitStyle = NSFormattingUnitStyleShort; | ||
shortFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier]; | ||
|
||
MGLDirectionFormatter *mediumFormatter = [[MGLDirectionFormatter alloc] init]; | ||
mediumFormatter.origin = MGLDirectionFormatterOriginStraightAhead; | ||
mediumFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier]; | ||
|
||
MGLDirectionFormatter *longFormatter = [[MGLDirectionFormatter alloc] init]; | ||
longFormatter.origin = MGLDirectionFormatterOriginStraightAhead; | ||
longFormatter.unitStyle = NSFormattingUnitStyleLong; | ||
longFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier]; | ||
|
||
XCTAssertEqualObjects(@"9:00", [shortFormatter stringFromDirection:-90]); | ||
XCTAssertEqualObjects(@"9 o’clock", [mediumFormatter stringFromDirection:-90]); | ||
XCTAssertEqualObjects(@"nine o’clock", [longFormatter stringFromDirection:-90]); | ||
|
||
XCTAssertEqualObjects(@"12:00", [shortFormatter stringFromDirection:0]); | ||
XCTAssertEqualObjects(@"12 o’clock", [mediumFormatter stringFromDirection:0]); | ||
XCTAssertEqualObjects(@"twelve o’clock", [longFormatter stringFromDirection:0]); | ||
|
||
XCTAssertEqualObjects(@"2:00", [shortFormatter stringFromDirection:45]); | ||
XCTAssertEqualObjects(@"2 o’clock", [mediumFormatter stringFromDirection:45]); | ||
XCTAssertEqualObjects(@"two o’clock", [longFormatter stringFromDirection:45]); | ||
|
||
XCTAssertEqualObjects(@"3:00", [shortFormatter stringFromDirection:90]); | ||
XCTAssertEqualObjects(@"3 o’clock", [mediumFormatter stringFromDirection:90]); | ||
XCTAssertEqualObjects(@"three o’clock", [longFormatter stringFromDirection:90]); | ||
|
||
XCTAssertEqualObjects(@"6:00", [shortFormatter stringFromDirection:180]); | ||
XCTAssertEqualObjects(@"6 o’clock", [mediumFormatter stringFromDirection:180]); | ||
XCTAssertEqualObjects(@"six o’clock", [longFormatter stringFromDirection:180]); | ||
|
||
XCTAssertEqualObjects(@"9:00", [shortFormatter stringFromDirection:270]); | ||
XCTAssertEqualObjects(@"9 o’clock", [mediumFormatter stringFromDirection:270]); | ||
XCTAssertEqualObjects(@"nine o’clock", [longFormatter stringFromDirection:270]); | ||
} | ||
|
||
@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
Oops, something went wrong.