Swift - Measurement Convert(To:) Miles to Feet Gives Wrong Result

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 return
Double 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



Leave a reply



Submit