Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2017-06-26

Strange NSNumber conversion behaviour in Swift

I have been doing some numerical work lately and see some strange behaviour when using NSNumber in Swift especially in the conversions to/from the int types.

First some expected behaviour in finding the integer ranges:

print("Int8 range: \(Int8.min) ... \(Int8.max)")
print("UInt8 range: \(UInt8.min) ... \(UInt8.max)")
print("Int16 range: \(Int16.min) ... \(Int16.max)")
print("UInt16 range: \(UInt16.min) ... \(UInt16.max)")
print("Int32 range: \(Int32.min) ... \(Int32.max)")
print("UInt32 range: \(UInt32.min) ... \(UInt32.max)")
print("Int64 range: \(Int64.min) ... \(Int64.max)")

print("UInt64 range: \(UInt64.min) ... \(UInt64.max)")

This prints:

Int8 range: -128 ... 127
UInt8 range: 0 ... 255
Int16 range: -32768 ... 32767
UInt16 range: 0 ... 65535
Int32 range: -2147483648 ... 2147483647
UInt32 range: 0 ... 4294967295
Int64 range: -9223372036854775808 ... 9223372036854775807
UInt64 range: 0 ... 18446744073709551615

As I said, that was to be expected. But pay attention to the Int64 and UInt64

Now some expected behaviour for NSNumber:

var n = NSNumber(value: 0x1020304050607080)

print("Int8:   \(n.int8Value)")
print("UInt8:  \(n.uint8Value)")
print("Int16:  \(n.int16Value)")
print("UInt16: \(n.uint16Value)")
print("Int32:  \(n.int32Value)")
print("UInt32: \(n.uint32Value)")
print("Int64:  \(n.int64Value)")

print("UInt64: \(n.uint64Value)")

Prints:

Int8:   -128
UInt8:  128
Int16:  28800
UInt16: 28800
Int32:  1348497536
UInt32: 1348497536
Int64:  1161981756646125696

UInt64: 1161981756646125696

Interesting, here we see that the integers don't clip (or clamp) to max, but are in effect simple bit mapped conversions. For example the -128 for the Int8 is clearly caused by the ....80 at the end of the literal.

We can make this even clearer by:

var n = NSNumber(value: 0x1020304080608080)

...
Output:

Int8:   -128
UInt8:  128
Int16:  -32640
UInt16: 32896
Int32:  -2141159296
UInt32: 2153808000
Int64:  1161981757451436160

UInt64: 1161981757451436160

So far so good.
So what would we expect if the we change 0x1020.. to 0x8020..
I don't know about you, but I would expect the Int64 to go negative and the UInt64 to remain positive.

However:

var n = NSNumber(value: 0x8020304080608080)

Generates the error:

Playground execution failed: error: MyPlayground8.playground:8:25: error: integer literal '9232432289699364992' overflows when stored into 'Int'

So let's fix that by:

var n = NSNumber(value: UInt64(0x8020304080608080))

No such luck, the same error occurs.
Next step:

var k: UInt64 = 0x8020304080608080
var n = NSNumber(value: k)
...
Int8:   -128
UInt8:  128
Int16:  -32640
UInt16: 32896
Int32:  -2141159296
UInt32: 2153808000
Int64:  -9214311784010186624

UInt64: 9232432289699364992

So far, the only strange thing is that I cannot initialize NSNumber directly from an integerLiteral. Not nice, but hey, no big deal.

Now, what would happen when NSNumber is initialized from a float or double?

Try that:

var n = NSNumber(value: 10.0)
...
Int8:   10
UInt8:  10
Int16:  10
UInt16: 10
Int32:  10
UInt32: 10
Int64:  10
UInt64: 10

Ok, next try:

var n = NSNumber(value: -10.0)
...
Int8:   -10
UInt8:  246
Int16:  -10
UInt16: 65526
Int32:  -10
UInt32: 4294967286
Int64:  -10
UInt64: 18446744073709551606

Aha, apparently the float is first converted to an Int and then a simple bit map conversion is made. This true even for the UInt64 (!)

How about rounding?

var n = NSNumber(value: 10.8)
...
Int8:   10
UInt8:  10
Int16:  10
UInt16: 10
Int32:  10
UInt32: 10
Int64:  10
UInt64: 10

Nope, no rounding here (expected behaviour)

var n = NSNumber(value: -10.8)
...
Int8:   -10
UInt8:  246
Int16:  -10
UInt16: 65526
Int32:  -10
UInt32: 4294967286
Int64:  -10
UInt64: 18446744073709551605

This may seem strange, but I am content to call that expected behaviour. As long as we keep in mind that rounding down is always in the direction of 0. Even for negative numbers.

Now, lets get wild:

var n = NSNumber(value: 1e10)
...
Int8:   0
UInt8:  0
Int16:  -7168
UInt16: 58368
Int32:  1410065408
UInt32: 1410065408
Int64:  10000000000
UInt64: 10000000000

Ok, not all that wild, still as expected. Lets get wilder still. But keep in mind that 100.000.000 is a special number, it translates to 0x5F5E100, which is to say that all multiples of that number will also result in 0xXXXXXX00. Thus simply multiplying 100.000.000 by *10 and again, and again will eventually create 0 values for all Int's.
Weird things start to happen at 1e19:

var n = NSNumber(value: 1e19)
...
Int8:   0
UInt8:  0
Int16:  0
UInt16: 0
Int32:  0
UInt32: 0
Int64:  -9223372036854775808
UInt64: 10000000000000000000

The 0 values are explainable (see above). The UInt64 value is correct, but the Int64 is odd, it seems clipped to me. (Compare it to the ranges above)
And this behaviour continues as we use ever greater numbers. And with much bigger numbers the clipping behaviour also occurs in the UInt64 (at 1e39):

var n = NSNumber(value: 1e39)
...
Int8:   0
UInt8:  0
Int16:  0
UInt16: 0
Int32:  0
UInt32: 0
Int64:  -9223372036854775808
UInt64: 18446744073709551615

Now the behaviour is odd: For an NSNumber with a very large positive value the smaller int's are zero, the Int64 is clipped against its negative (minimum) limit and the UInt64 is clipped to its maximum limit.

Well, now you know...

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.