-4
let n = "77777777"
let i = Int(n)!
let f = Float(n)!
let d = Double(n)!

print(i)
print(f)
print(d)
print(Int(f))
print(Int(d))

produces:

77777777

7.777778e+07

77777777.0

77777776

77777777

Why? Using Xcode 11, Swift 5. This question looks similar but it starts with a decimal number, not an integer. Also, there's really no floating-point math going on here.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219
  • 1
    You should also print the float and double, and the reason will be clear. – unxnut Jul 12 '20 at 00:41
  • 2
    `Float` lacks precision that `Double` has at this order of magnitude. There are tons of related questions, but basically you need to understand the fundamentals of floating point format. https://stackoverflow.com/questions/588004/is-floating-point-math-broken – New Dev Jul 12 '20 at 00:44
  • @unxnut Did that, and it'd seem that the float result should have an 8, not a 6. – Abhijit Sarkar Jul 12 '20 at 00:45
  • @NewDev It seems people have that SO question bookmarked as a pet peeve and throw it out whenever they see the word "float" in a question. The question here isn't about doing floating-point operation using float data type where precision errors creep it, it's about representing a trivial integer well within the range. – Abhijit Sarkar Jul 12 '20 at 00:47
  • 1
    @AbhijitSarkar, I don't know what you want to hear. This is how floating point numbers work. Try this line in Playground: `let a: Float = 77777777` and XCode will literally tell you that Float lacks precision to represent this number. The "Why" behind is in the crux of a floating point number representation of a number, whether it's integer or not – New Dev Jul 12 '20 at 01:01
  • 1
    @AbhijitSarkar You should take a look at Swift `Decimal` type. Make sure to use the string initializer when creating your objects. – Leo Dabus Jul 12 '20 at 01:39
  • @LeoDabus `Decimal` seems to be exactly suitable for my purpose, thanks. – Abhijit Sarkar Jul 12 '20 at 02:17

2 Answers2

1

OK, please look at the floating point converter at https://www.h-schmidt.net/FloatConverter/IEEE754.html. It shows you the bits stored when you enter a number in binary and hex representation, and also gives you the error due to conversion. The issue is with the way the number gets represented in the standard. In floating point, the error indeed comes out to be -1.

Actually, any number in the range 77777772 to 77777780 gives you 77777776 as the internal representation of mantissa.

unxnut
  • 8,509
  • 3
  • 27
  • 41
1

Numbers are stored in finite memory. Whether or not you do floating point arithmetic, you need a way to encode a decimal number in binary memory. So long as you have finite memory (i.e. always, in the real world), you have to choose to spend your bits on having high range, or high precision, or some trade off of the two.

Going above 7 digits gets you into the first "area" of trade-off of Float. You can "do it", but there's a trade off: at this high magnitude, you lose some precision. In this case, it's that whole numbers are rounded to the closest 10.

Float is a single-precision IEEE 754 floating pointer number. Its first "trade off" area is at 16,777,217. From 0 to 16,777,216, every whole number is precisely representable. After that, there isn't enough precision to specify a number down to 2^0 (ones, a.k.a. units). The next best thing is to represent it correctly down to the closest 2^1 (twos).

Check this out:

import Foundation

for originalInt in 16_777_210 ... 16_777_227 {
    let interMediateFloat = Float(originalInt)
    let backAsInt = Int(interMediateFloat)
    print("\(originalInt) -> \(backAsInt)")
}

print("\n...\n")

for originalInt in 33_554_430 ... 33_554_443 {
    let interMediateFloat = Float(originalInt)
    let backAsInt = Int(interMediateFloat)
    print("\(originalInt) -> \(backAsInt)")
}

prints:

16777210 -> 16777210
16777211 -> 16777211
16777212 -> 16777212
16777213 -> 16777213
16777214 -> 16777214
16777215 -> 16777215
16777216 -> 16777216 // Last precisely representable whole number
16777217 -> 16777216 // rounds down
16777218 -> 16777218 // starts skipping by 2s
16777219 -> 16777220
16777220 -> 16777220
16777221 -> 16777220
16777222 -> 16777222
16777223 -> 16777224
16777224 -> 16777224
16777225 -> 16777224
16777226 -> 16777226
16777227 -> 16777228

...

33554430 -> 33554430
33554431 -> 33554432
33554432 -> 33554432
33554433 -> 33554432
33554434 -> 33554432 // Last whole number representable to the closest "two"
33554435 -> 33554436 // rounds up
33554436 -> 33554436
33554437 -> 33554436 // starts skipping by 4s
33554438 -> 33554440
33554439 -> 33554440
33554440 -> 33554440
33554441 -> 33554440
33554442 -> 33554440
33554443 -> 33554444

And so on. As the magnitude gets larger, whole numbers get represented with less and less precision. At the extreme, the largest whole number value (340,282,346,638,528,859,811,704,183,484,516,925,440) and the second largest whole number value (340,282,326,356,119,256,160,033,759,537,265,639,424) differ by 20,282,409,603,651,670,423,947,251,286,016 (2^104).

The ability to express such a high numbers comes precisely at the cost of the inability to precisely store many numbers around that magnitude. Rounding happens. Alternatively, binary integers like Swift's Int have perfect precision for whole numbers (always stored down to the correct ones/units), but pay for that with a vastly smaller max size (only 2,147,483,647 for a signed Int32).

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • converting from `Float` to `NSNumber` is pointless. Why not simply `nf.string(for: f)`? – Leo Dabus Jul 12 '20 at 04:28
  • @LeoDabus I didn't know there was an overload for that, I thought it *had* to go through `NSNumber`. Is that new? Anyway, I couldn't get that formatter approach to print just right. For some reason, it was rounding off elements to nearest 10s, not powers of 2 like I had expected. Do you know why that might be? – Alexander Jul 12 '20 at 14:05
  • If you subclass NumberFormatter you have to override this method. When you call NumberFormatter's `string(from: NSNumber)` it actually calls the Formatter's method `string(for: Any)` as I've posted in this question which you have also commented but I don't know why you did not pay attention to what was being asked. https://stackoverflow.com/q/62644196/2303865 – Leo Dabus Jul 12 '20 at 14:30
  • Oh yeah, I forgot about that. I guess I didn't notice it because it was an inherited member, and I was looking for something like `func string(for: I)`. Looks like that `func string(for: Any)` overload [is still going through NSNumber](https://github.com/apple/swift-corelibs-foundation/blob/2a5bc4d8a0b073532e60410682f5eb8f00144870/Sources/Foundation/NumberFormatter.swift#L80-L84), although it is nice that it tucks that implementation detail away and simplifies the call site. Do you know why that rounding to 10 was happening? – Alexander Jul 12 '20 at 14:36
  • I have no idea. As far as I remember I have tried to create a generic method to take a string as initializer, used NSNumberFormatter to create a NSNumber but when casting to FloatingPoint it would work only for 64bit types, 32bit Float would fail but I don't know the reason. It might be related to this. – Leo Dabus Jul 12 '20 at 14:57
  • `extension String {` `func floatingPoint() -> F {` `NumberFormatter().number(from: self) as! F` `}` `}` and try `let double: Double = "1.23".floatingPoint()` `let cgFloat: CGFloat = "1.23".floatingPoint()` `let float: Float = "1.23".floatingPoint()` If you figure out let me know – Leo Dabus Jul 12 '20 at 14:59
  • 1
    I have been trying to find a generic FloatingPoint initializer without success. I did find the generic initializer which takes a FixedWidthInteger though `extension StringProtocol {` `func binaryInteger() -> B? {` `B(self)` `}` `}` `let int: Int? = "2".binaryInteger()` – Leo Dabus Jul 12 '20 at 15:05
  • I finally figured out how to create a generic initializer for FloatingPoint but it would not work for CGFloat. It does not conform to LosslessStringConvertible – Leo Dabus Jul 12 '20 at 15:18
  • `extension String {` `func floatingPoint() -> F? where F: LosslessStringConvertible {` `F(self)` `}` `}` – Leo Dabus Jul 12 '20 at 15:19
  • It can actually be even more generic if you extend Numeric protocol `extension String {` `func number() -> N? where N: LosslessStringConvertible {` `N(self)` `}` `}` – Leo Dabus Jul 12 '20 at 15:22
  • To include CGFloat to the generic method `extension CGFloat: LosslessStringConvertible {` `public init?(_ description: String) {` `guard let cgFloat = NumberFormatter().number(from: description) as? CGFloat else { return nil }` `self = cgFloat ` `}` `}` – Leo Dabus Jul 12 '20 at 15:32