Swift - Measurement convert(to:) miles to feet gives wrong result
You can see the definition of UnitLength
here. Every unit of length has a name and a coefficient.
The mile unit has a coefficient of 1609.34
, and the foot unit has a coefficient of 0.3048
. When represented as a Double
(IEEE 754 Double precision floating point number), the closest representations are 1609.3399999999999
and 0.30480000000000002
, respectively.
When you do the conversion 1 * 1609.34 / 0.3048
, you get 5279.9868766404197
rather than the expected 5280
. That's just a consequence of the imprecision of fixed-precision floating point math.
This could be mitigated, if the base unit of length was a mile. This would be incredibly undesirable of course, because most of the world doesn't use this crazy system, but it could be done. Foot could be defined with a coefficient of 5280
, which can be represented precisely by Double
. But now, instead of mile->foot being imprecise, meter->kilometer will be imprecise. You can't win, I'm afraid.
Swift Measurement class · angle conversion accuracy
import Foundation
print(UnitAngle.radians.converter.baseUnitValue(fromValue: 1.0)) // 57.2958
print(180.0 / .pi) // 57.2957795130823
print(UnitAngle.radians.converter.baseUnitValue(fromValue: .pi)) // 180.00006436155007
Is this a bug? It must be a bug. It's far too inaccurate.
I would say yes. All Measurement
APIs take Double
arguments and returnDouble
values. This large deviation cannot simply be explained with
floating point inaccuracies, and makes the result useless for serious work
with trigonometric functions.
Interestingly, the result is correct on Ubuntu Linux (where the open source
implementation from Units.swift is used):
Welcome to Swift version 4.2-dev (LLVM ec4d8ae334, Clang 5a454fa3d6, Swift 0ca361f487). Type :help for assistance.
1> import Foundation
2> print(UnitAngle.radians.converter.baseUnitValue(fromValue: 1.0))
57.29577951308232
3> print(180.0 / .pi)
57.29577951308232
4> print(UnitAngle.radians.converter.baseUnitValue(fromValue: .pi))
180.0
Is it possible to set the conversion factor to a different value (or subclass Measurement to achieve the same).
Similarly as in Swift - Measurement convert(to:) miles to feet gives wrong result, you can
define your own unit:
extension UnitAngle {
static let preciseRadians = UnitAngle(symbol: "rad",
converter: UnitConverterLinear(coefficient: 180.0 / .pi))
}
print(UnitAngle.preciseRadians.converter.baseUnitValue(fromValue: 1.0)) // 57.2957795130823
print(UnitAngle.preciseRadians.converter.baseUnitValue(fromValue: .pi)) // 180.0
Should I roll my own Measurement class or just abandon the idea of unit conversion and stick to SI units internally?
That is for you to decide :)
Format distance measurement as either kilometers or imperial miles, not metric miles
.general
instead of .road
will give you kilometres or miles according to locale. .asProvided
will give you the units defined on the original measurement.
I think .general
gets you closest to what you want, though it may end up giving you metres or yards for non-kilometre ranged values.
NSLengthFormatter get stringFromMeters: in miles/kilometers only
This is what I have ended up using:
-(NSString *)formattedDistanceForMeters:(CLLocationDistance)distance
{
NSLengthFormatter *lengthFormatter = [NSLengthFormatter new];
[lengthFormatter.numberFormatter setMaximumFractionDigits:1];
if ([[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue])
{
return [lengthFormatter stringFromValue:distance / 1000 unit:NSLengthFormatterUnitKilometer];
}
else
{
return [lengthFormatter stringFromValue:distance / 1609.34 unit:NSLengthFormatterUnitMile];
}
}
EDIT:
The same in Swift would look like:
func formattedDistanceForMeters(distance:CLLocationDistance) -> String {
let lengthFormatter:NSLengthFormatter! = NSLengthFormatter()
lengthFormatter.numberFormatter.maximumFractionDigits = 1
if NSLocale.currentLocale().objectForKey(NSLocaleUsesMetricSystem).boolValue()
{
return lengthFormatter.stringFromValue(distance / 1000, unit:NSLengthFormatterUnitKilometer)
}
else
{
return lengthFormatter.stringFromValue(distance / 1609.34, unit:NSLengthFormatterUnitMile)
}
}
MKDistanceFormatter feet to miles
The short answer
So you find MKDistanceFormatter
starts outputting "feet" for small numbers and switches to "miles" only when the distance becomes large enough; I'm afraid this is intentional.
Since different combos of locales and MKDistanceFormatterUnits
produce different results, there's no way to control the granularity in a general fashion anyway. With my default settings, passing 2
to the formatter produces "2 m", not "2 cm", for example. CLLocationDistance
(which is just a Double
) is documented to be measured in meters.
So the short answer is: you're screwed. :)
The long answer
All of the following depends on your use case and may not be directly applicable.
What you want is use a type or a formatter that combines a numerical value with a specific unit. This also means you have to make sure you're switching from "miles" to "kilometers", depending on the locale. You could totally code this up on your own.
You can also interface with Foundation's new NSMeasurements
API (requires macOS 10.12 and iOS 10):
https://developer.apple.com/documentation/foundation/nsmeasurement
Still, you have to convert from the CLLocationDistance
distance in meters to the proper NSUnitLength
. The conversion could be done with a set of NSUnitConverter
instances.
Something along the lines of this:
CLLocationDistance distanceInMeters = ...; // Obtain from notification
NSMeasurement *measurementInMeters = [[NSMeasurement alloc] initWithDoubleValue:distanceInMeters unit:[NSUnitLength meters]];
NSMeasurement *measurement = [[NSLocale currentLocale] usesMetricSystem]
? [measurementInMeters measurementByConvertingToUnit:[NSUnitLength kilometers]]
: [measurementInMeters measurementByConvertingToUnit:[NSUnitLength miles]];
NSMeasurementFormatter *formatter = [[NSMeasurementFormatter alloc] init];
NSString *distanceText = [formatter stringFromMeasurement: measurement];
self.distanceValueLabel.text = distanceText;
Interesting reads for unit conversions:
MKUnits
library as a reference for unit conversions: https://github.com/michalkonturek/MKUnits/tree/archive-objc- Swift code showing formatting extensions and calculations (with hard-coded kilometers-to-miles ratios): https://github.com/mapbox/mapbox-navigation-ios/blob/master/MapboxCoreNavigation/DistanceFormatter.swift
How to find out distance between coordinates?
CLLocation has a distanceFromLocation method so given two CLLocations:
CLLocationDistance distanceInMeters = [location1 distanceFromLocation:location2];
or in Swift 4:
//: Playground - noun: a place where people can play
import CoreLocation
let coordinate₀ = CLLocation(latitude: 5.0, longitude: 5.0)
let coordinate₁ = CLLocation(latitude: 5.0, longitude: 3.0)
let distanceInMeters = coordinate₀.distance(from: coordinate₁) // result is in meters
you get here distance in meter so 1 miles = 1609 meter
if(distanceInMeters <= 1609)
{
// under 1 mile
}
else
{
// out of 1 mile
}
Convert array of Measurement() of UnitLength to same natural scale
Let's start with this array:
let dataPoints = [0.0001, 0.1, 1, 2, 3, 1000, 2000]
The most common unit appropriate for this array would be the one for the median value:
let median = dataPoints.sorted(by: <)[dataPoints.count / 2]
let medianMeasurement = Measurement(value: 1700, unit: UnitLength.meters)
In the following snippet we figure out the most appropriate unit, if unit is just less than the data point then it's considered as the natural unit:
let imperialUnitsNames: [UnitLength] = [.inches,
.feet,
.yards,
.fathoms,
.furlongs,
.miles,
]
let imperialUnitsInMeters: [Any] = imperialUnitsNames.map { unit in
let m = Measurement(value: 1, unit: unit).converted(to: .meters)
return m.value
}
let zipped = zip(imperialUnitsInMeters, imperialUnitsNames)
let naturalUnit = zipped.reversed()
.first(where: { $0.0 < median})!
.1
You could customize the possible units in imperialUnitsNames
.
Let's create a measurement formatter:
let measurementFormatter = MeasurementFormatter()
measurementFormatter.unitOptions = .providedUnit
Now we're ready to format the dataPoints
:
let measurementStrings: [String] = dataPoints.map { dataPoint in
let measurement = Measurement(value: dataPoint, unit: UnitLength.meters)
let newMeasurement = measurement.converted(to: naturalUnit)
return measurementFormatter.string(from: newMeasurement)
}
print(measurementStrings) //["0 ftm", "0.055 ftm", "0.547 ftm", "1.094 ftm", "1.64 ftm", "546.807 ftm", "1,093.613 ftm"]
Related Topics
Protocol Associated Type Typealias Assignment Compile Error
Conversion Between Cgfloat and Nsnumber Without Unnecessary Promotion to Double
Enum of String Type Vs Struct with Static Constant
Localizing The Reading of Emoji on iOS 10.0 or Higher
Enum Named 'Type' in Nested Class Fails
Stored Variable of Self Type (Especially When Subclassing)
How to Use The Snapchat Sdk (Snapkit) with Swiftui
Uicolor Code in a Variable in Swift
Sknode Subclass Generates Error: Cannot Invoke Initializer for Type "X" with No Arguments
Viewwilllayoutsubviews in Swift
Using Multiple Hosting Controllers in Swiftui on Watchos
Swift Cannot Convert The Expression's Type 'Void' to Type 'string!'
Accessing an Actor's Isolated State from Within a Swiftui View