6

I've been using the Measurement object to convert from mostly lengths. But I have a strange issue. If I convert from miles to feet I get almost the right answer.

import Foundation

let heightFeet = Measurement(value: 6, unit: UnitLength.feet) // 6.0ft
let heightInches = heightFeet.converted(to: UnitLength.inches) // 72.0 in
let heightMeters = heightFeet.converted(to: UnitLength.meters) // 1.8288 m

let lengthMiles = Measurement(value: 1, unit: UnitLength.miles) // 1.0 mi

let lengthFeet = lengthMiles.converted(to: UnitLength.feet) // 5279.98687664042 ft

// Should be 5280.0

They all work except the last one lengthFeet. In my playground (Xcode Version 9.2 (9C40b)) it returns 5279.98687664042 ft. I also tested in a regular app build and same results.

Any ideas what is going on?

Jeff Kempster
  • 63
  • 2
  • 6
  • 1
    It's a typical fixed-precision floating point precision issue. [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – Alexander Mar 19 '18 at 22:22
  • I've checked the above code and it works well. Seems with the time it was fixed. – IvanovDeveloper Nov 09 '20 at 18:27

2 Answers2

5

The “miles” unit is defined incorrectly in the Foundation library, as can be seen with

print(UnitLength.miles.converter.baseUnitValue(fromValue: 1.0))
// 1609.34

where as the correct value is 1.609344. As a workaround for that flaw in the Foundation library you can define your “better” mile unit:

extension UnitLength {
    static var preciseMiles: UnitLength {
        return UnitLength(symbol: "mile",
                          converter: UnitConverterLinear(coefficient: 1609.344))
    }
}

and using that gives the intended result:

let lengthMiles = Measurement(value: 1, unit: UnitLength.preciseMiles)
let lengthFeet = lengthMiles.converted(to: UnitLength.feet)
print(lengthFeet) // 5280.0 ft

Of course, as Alexander said, rounding errors can occur when doing calculations with the units, because the measurements use binary floating point values as underlying storage. But the reason for that “blatantly off” result is the wrong definition of the miles unit.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • I wonder whether this should be described as a matter of _representation_ rather than a "rounding error". (And indeed, now that I look closely, that is what Alexander said.) – matt Mar 20 '18 at 18:00
  • @matt: According to that Wikipedia article, a mile is 25146/15625 = 1.609344 km, so the definition as 1609.34 in Foundation is simply wrong (and cannot be explained with the limited precision of 64-bit doubles, which is about 16 decimal digits). – What I meant is that even with the precise definition there can be some rounding errors when doing calculations with the units. – Martin R Mar 20 '18 at 18:04
  • I guess what I'm wondering is where there is any rounding. To me, rounding means "let's willfully remove some significant digits". The point here would be that there are limits to calculation precision when using a simple numerical representation (as opposed to, say, rational number calculations like what CMTime does). But I could be totally wrong. – matt Mar 20 '18 at 18:08
  • @matt: The precision of a 64-bit IEEE floating point is about 16 decimal digits. So yes, 1.609344 cannot be represented exactly. But that is no excuse to use 1.60934 instead. Rounding errors alone do not explain the result of 5279.98687664042 feet in a mile instead of 5280. – Martin R Mar 20 '18 at 18:31
2

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.

nVitius
  • 2,024
  • 15
  • 21
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • The definition in https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/Unit.swift#L1209 is surprisingly imprecise. According to https://en.wikipedia.org/wiki/Mile#International_mile, a mile is 1.609344 km. – With that value, 1.609344/0.3048 evaluates to 5280. – Martin R Mar 19 '18 at 22:43
  • Hmm yes, that is concerning. Should we file a bug? – Alexander Mar 19 '18 at 22:52
  • I would consider it a bug, other units are (unnecessarily) imprecise as well. It is a part of Foundation, therefore I *assume* that one has to file it at bugreporter.apple.com. – Martin R Mar 19 '18 at 22:59
  • Thanks for the great answer. I am going to file a bug report at apple. In my use case, the provided result is pointless. I think a class like Measurement should account for this issue and provide the correct number. – Jeff Kempster Mar 19 '18 at 23:38
  • @JeffKempster Keep in mind that even `1.609344/0.3048` is not `5280`, it's `5.2800000000000002`. I guess the real shortfall here is that `Measurement` only works with `Double`, not `Int` or `Decimal` – Alexander Mar 20 '18 at 01:14
  • @Alexander: Actually, `1609.344/0.3048` evaluates exactly to `5280` :) – Martin R Mar 20 '18 at 18:44
  • @MartinR Oops, had the decimal separator in the wrong spot – Alexander Mar 20 '18 at 19:40
  • I submitted a PR for this to the swift-corelibs repo: https://github.com/apple/swift-corelibs-foundation/pull/3033 – nVitius Aug 10 '21 at 22:47