-
Notifications
You must be signed in to change notification settings - Fork 325
Use fractions for voice only #383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8595e23
6b74e31
81c1b6f
22b8c89
9a33f1d
5bcea5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import CoreLocation | ||
|
|
||
| /// Provides appropriately formatted, localized descriptions of linear distances for voice use. | ||
| @objc(MBSpokenDistanceFormatter) | ||
| public class SpokenDistanceFormatter: DistanceFormatter { | ||
|
|
||
|
|
||
| /** | ||
| Returns a more human readable `String` from a given `CLLocationDistance`. | ||
|
|
||
| The user’s `Locale` is used here to set the units. | ||
| */ | ||
| public override func string(from distance: CLLocationDistance) -> String { | ||
| // British roads are measured in miles, yards, and feet. Simulate this idiosyncrasy using the U.S. locale. | ||
| let localeIdentifier = numberFormatter.locale.identifier | ||
| if localeIdentifier == "en-GB" || localeIdentifier == "en_GB" { | ||
| numberFormatter.locale = Locale(identifier: "en-US") | ||
| } | ||
|
|
||
| var unit: LengthFormatter.Unit = .millimeter | ||
| unitString(fromMeters: distance, usedUnit: &unit) | ||
| let replacesYardsWithMiles = unit == .yard && distance.miles > 0.2 | ||
| let showsMixedFraction = (unit == .mile && distance.miles < 10) || replacesYardsWithMiles | ||
|
|
||
| // An elaborate hack to use vulgar fractions with miles regardless of | ||
| // language. | ||
| if showsMixedFraction { | ||
| numberFormatter.positivePrefix = "|" | ||
| numberFormatter.positiveSuffix = "|" | ||
| numberFormatter.decimalSeparator = "!" | ||
| numberFormatter.alwaysShowsDecimalSeparator = true | ||
| } else { | ||
| numberFormatter.positivePrefix = "" | ||
| numberFormatter.positiveSuffix = "" | ||
| numberFormatter.decimalSeparator = nonFractionalLengthFormatter.numberFormatter.decimalSeparator | ||
| numberFormatter.alwaysShowsDecimalSeparator = nonFractionalLengthFormatter.numberFormatter.alwaysShowsDecimalSeparator | ||
| } | ||
|
|
||
| numberFormatter.usesSignificantDigits = false | ||
| numberFormatter.maximumFractionDigits = maximumFractionDigits(for: distance) | ||
| numberFormatter.roundingIncrement = roundingIncrement(for: distance, unit: unit) as NSNumber | ||
|
|
||
| var distanceString = formattedDistance(distance, modify: &unit) | ||
|
|
||
| // Elaborate hack continued. | ||
| if showsMixedFraction { | ||
| var parts = distanceString.components(separatedBy: "|") | ||
| assert(parts.count == 3, "Positive format should’ve inserted two pipe characters.") | ||
| var numberParts = parts[1].components(separatedBy: "!") | ||
| assert(numberParts.count == 2, "Decimal separator should be present.") | ||
| let decimal = Int(numberParts[0]) | ||
| if let fraction = Double(".\(numberParts[1])0") { | ||
| let fourths = Int(round(fraction * 4)) | ||
| if fourths == fourths % 4 { | ||
| if decimal == 0 && fourths != 0 { | ||
| numberParts[0] = "" | ||
| } | ||
| var vulgarFractions = ["", "¼", "½", "¾"] | ||
| if Locale.current.languageCode == "en" { | ||
| vulgarFractions = ["", "a quarter", "a half", "3 quarters"] | ||
| if numberParts[0].isEmpty { | ||
| parts[2] = " \(unitString(fromValue: 1, unit: unit)) " | ||
| if fourths == 3 { | ||
| parts[2] = " of a\(parts[2])" | ||
| } | ||
| } | ||
| } | ||
| numberParts[1] = vulgarFractions[fourths] | ||
| if !numberParts[0].isEmpty && !numberParts[1].isEmpty { | ||
| numberParts[0] += " & " | ||
| } | ||
| parts[1] = numberParts.joined(separator: "") | ||
| } else { | ||
| parts[1] = numberParts.joined(separator: nonFractionalLengthFormatter.numberFormatter.decimalSeparator) | ||
| } | ||
| } else { | ||
| parts[1] = numberParts.joined(separator: nonFractionalLengthFormatter.numberFormatter.decimalSeparator) | ||
| } | ||
| distanceString = parts.joined(separator: "") | ||
| } | ||
|
|
||
| return distanceString | ||
| } | ||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import XCTest | ||
| import CoreLocation | ||
| @testable import MapboxCoreNavigation | ||
|
|
||
| let oneMile: CLLocationDistance = metersPerMile | ||
| let oneFeet: CLLocationDistance = 0.3048 | ||
|
|
||
| class DistanceFormatterTests: XCTestCase { | ||
|
|
||
| var spokenDistanceFormatter = SpokenDistanceFormatter(approximate: true) | ||
| var distanceFormatter = DistanceFormatter(approximate: true) | ||
|
|
||
| override func setUp() { | ||
| super.setUp() | ||
| spokenDistanceFormatter.unitStyle = .long | ||
| } | ||
|
|
||
| func assertDistance(_ distance: CLLocationDistance, spoken: String, displayed: String) { | ||
| let spokenString = spokenDistanceFormatter.string(from: distance) | ||
| let displayedString = distanceFormatter.string(from: distance) | ||
| XCTAssert(spokenString.contains(spoken), "Spoken '\(spokenString)' should be equal to '\(spoken)'") | ||
| XCTAssert(displayedString.contains(displayed), "Displayed: '\(displayedString)' should be equal to \(displayed)") | ||
| } | ||
|
|
||
| func testDistanceFormatters_US() { | ||
| spokenDistanceFormatter.numberFormatter.locale = Locale(identifier: "en-US") | ||
| distanceFormatter.numberFormatter.locale = Locale(identifier: "en-US") | ||
|
|
||
| assertDistance(oneFeet*50, spoken: "50 feet", displayed: "50 ft") | ||
| assertDistance(oneFeet*100, spoken: "100 feet", displayed: "100 ft") | ||
| assertDistance(oneFeet*249, spoken: "250 feet", displayed: "250 ft") | ||
| assertDistance(oneFeet*305, spoken: "300 feet", displayed: "300 ft") | ||
| assertDistance(oneFeet*997, spoken: "1,000 feet", displayed: "1,000 ft") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use this grouping separator here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The grouping separator is very important when there are more than two significant figures. But if we’re rounding up to the hundreds and lack the space to display it, I suppose we could get away with omitting it. In any case, I think it’s really unlikely that anyone would misread “1,000 feet” as “1.000 feet” (that is, one and zero thousandths). Does the grouping separator ever occur when the formatter uses the metric system?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It did show "1,000 m" before I forced "1,000 m" to display "1 km" (5bd709d#diff-cbb2b216ecdb63112349c1a2e9642224R118) |
||
| assertDistance(oneMile*0.25, spoken: "a quarter mile", displayed: "0.2 mi") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this quarter mile displayed correctly?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we’re using the formatter to count down to a maneuver, I suppose it’s better to undercount than to overcount. |
||
| assertDistance(oneMile/2, spoken: "a half mile", displayed: "0.5 mi") | ||
| assertDistance(oneMile*0.75, spoken: "3 quarters of a mile", displayed: "0.8 mi") | ||
| assertDistance(oneMile, spoken: "1 mile", displayed: "1 mi") | ||
| assertDistance(oneMile*2.5, spoken: "2 & a half miles", displayed: "2.5 mi") | ||
| assertDistance(oneMile*2.9, spoken: "3 miles", displayed: "3 mi") | ||
| assertDistance(oneMile*3, spoken: "3 miles", displayed: "3 mi") | ||
| assertDistance(oneMile*3.5, spoken: "4 miles", displayed: "4 mi") | ||
| } | ||
|
|
||
| func testDistanceFormatters_DE() { | ||
| spokenDistanceFormatter.numberFormatter.locale = Locale(identifier: "de-DE") | ||
| distanceFormatter.numberFormatter.locale = Locale(identifier: "de-DE") | ||
|
|
||
| assertDistance(10, spoken: "0 Meter", displayed: "0 m") | ||
| assertDistance(15, spoken: "25 Meter", displayed: "25 m") | ||
| assertDistance(24, spoken: "25 Meter", displayed: "25 m") | ||
| assertDistance(500, spoken: "500 Meter", displayed: "500 m") | ||
| assertDistance(949, spoken: "950 Meter", displayed: "950 m") | ||
| assertDistance(951, spoken: "950 Meter", displayed: "950 m") | ||
| assertDistance(1000, spoken: "1 Kilometer", displayed: "1 km") | ||
| assertDistance(1001, spoken: "1 Kilometer", displayed: "1 km") | ||
| assertDistance(2_500, spoken: "2.5 Kilometer", displayed: "2.5 km") | ||
| assertDistance(2_900, spoken: "2.9 Kilometer", displayed: "2.9 km") | ||
| assertDistance(3_000, spoken: "3 Kilometer", displayed: "3 km") | ||
| assertDistance(3_500, spoken: "4 Kilometer", displayed: "4 km") | ||
| } | ||
|
|
||
| func testDistanceFormatters_GB() { | ||
| spokenDistanceFormatter.numberFormatter.locale = Locale(identifier: "en-GB") | ||
| distanceFormatter.numberFormatter.locale = Locale(identifier: "en-GB") | ||
|
|
||
| assertDistance(oneMile/2, spoken: "a half mile", displayed: "0.5 mi") | ||
| assertDistance(oneMile, spoken: "1 mile", displayed: "1 mi") | ||
| assertDistance(oneMile*2.5, spoken: "2 & a half miles", displayed: "2.5 mi") | ||
| assertDistance(oneMile*3, spoken: "3 miles", displayed: "3 mi") | ||
| assertDistance(oneMile*3.5, spoken: "4 miles", displayed: "4 mi") | ||
| } | ||
| } | ||
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.
If we set the cutoff at exactly 3 miles with a rounding increment of 0.25, I think that means 3.0 miles becomes “3” while 2.9 miles becomes “3.0”. That would be odd.
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.
Added a test case to verify that this is not the case https://github.com/mapbox/mapbox-navigation-ios/pull/383/files#diff-e0d6099c4f87ad73208824318f1f72a0R39