NSDecimalNumber(floatLiteral:)は使ってはいけない

私がSwiftで気に入っている改善として、NSDecimalNumberインスタンスがちょう簡単に作れることがあります。

let d : NSDecimalNumber = 15.97

すばらしい。Objective Cだとこうなって、辛い。

NSDecimalNumber *d = [NSDecimalNumber decimalNumberWithString:@"15.97"];

このSwiftの機能はFloatLiteralConvertibleというプロトコルを使って実装されていて、floatのリテラルの型がFloatLiteralConvertibleなクラス(構造体もできる?)のときに、適当なイニシャライザを呼んで変換してくれるというものです。

init(floatLiteral: Double)

わーかっこいい。かっこいいのですが、ところがしかし型をよく見ると一回Doubleが作られています。NSDecimalNumberDoubleから作るのは安全なのかと聞かれて、一瞬悩んでしまったのでした。とりあえず、NSDecimalNumberについて言えば明らかにダメです。

let d : NSDecimalNumber = 15.97
_ = d.stringValue     // => "15.970000000000004096"

15.97とは似ても似つかない値になりました。Stringから作る場合と見比べれば明らかですね。

let d = NSDecimalNumber(string: "15.97")
_ = d.stringValue     // => "15.97"

やはり、NSDecimalNumberを作るときには、整数か文字列を与えるのが安全なようです。

さて、これはなぜでしょう?「浮動小数点はそんなもんだよ」と言われて、一瞬納得してしまいそうになったのですが、やっぱりよく考えた結果、これはNSDecimalNumberが壊れているというのが私の現時点の理解です。

浮動小数点に関しては、僕は全然経験がないので、ここから先まったく間違ったことを言うかもしれません。 自信はありません。 間違っていたら、教えてもらえると嬉しいです。

Floatのリテラルとは

0.1を10回足しても1.0にならないことはよく知られていると思います。

let a = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1
_ = (a as NSNumber).stringValue // => "0.9999999999999999"

ソースコード0.1と書いても、それは実数の0.1ではありません。Doubleで表現できる中で一番実数の0.1に近い数です。この辺りの感覚があると、15.97が15.970000000000004096になると言われても納得しても良さそうな気がする感じもします。

ところが、0.1を一度Doubleにしてから文字列表現に直すと、"0.1"が得られます。0.1ではないのに。少し不思議な気がしますね。これは、Doubleを表現する10進数の中に0.1よりも近いものが他にないことが理由です(多分)。Floatから文字列への変換をどういうアルゴリズムで実装するかによって違いが生まれますが、SwiftとかNSNumberではそうなっているようです。Swiftで試すと、0.1に対応するFloatの文字列表現は"0.1"ですし、15.97に対応するFloatの文字列表現は"15.97"です。

FloatをNSDecimalNumberに変換するというのは10進数に変換するのと同じことですから、NSDecimalNumberDoubleを与えるとおかしくなるというのは辻褄が合わない話に思えます。

もちろん、変な数を考えるとうまく行かなくなることはあります。

let x = 0.09999999999999986
let y = 0.09999999999999987
_ x == y  // => true

ここで、xyは区別できないので、文字列表現も元には戻りません。

_ = (x as NSNumber).stringValue   // => "0.09999999999999987"
_ = (y as NSNumber).stringValue   // => "0.09999999999999987"

まあ仕方ないですね。NSDecimalNumberにすればこの二つは区別できるようになります。

let x = NSDecimalNumber(string: "0.09999999999999986")
let y = NSDecimalNumber(string: "0.09999999999999987")

_ = x.stringValue   // => "0.09999999999999986"
_ = y.stringValue   // => "0.09999999999999987"

15.970000000000004096

それでは、15.970000000000004096とはなんなのでしょうか。15.97は15.970000000000004096だったのでしょうか。

let s = 15.97
let t = 15.970000000000004096
_ = s == t   // => false

もちろん違います。この二つは別々の数として認識されています。15.9715.970000000000004096の間の数すら、Doubleは表現できます。

let u = (s + t)/2
_ = s < u && u < t  // => true

15.970000000000004096は、意味がわからない謎の数字なのです。同じ例は、0.09999999999999986でも見ることができます。

let x : NSDecimalNumber = 0.09999999999999986
_ = x.stringValue  // => "0.09999999999999983616"

0.09999999999999983616はどこから出てきたのでしょうか。ちなみに0.099999999999999860.09999999999999983616の間には0.09999999999999985があります。

正しい実装の例

さて、DoubleNSDecimalNumberに変換する正しいコードは次のようになります。この関数にFloatの15.97を与えると15.97を指すNSDecimalNumberが得られます。0.099999999999999860.09999999999999987のような場合に注意は必要ですが、それは仕方ないでしょう。

ちょっと難しいかもしれませんが、がんばって読んでみてください。

func decimalNumberWithDouble(double: Double) -> NSDecimalNumber {
    let number = double as NSNumber
    return NSDecimalNumber(string: number.stringValue)
}

参考にしたもの

直接この問題について理解するというよりは、ぼんやり眺めて浮動小数点の周りの空気がわかった気がした。