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が作られています。NSDecimalNumber
をDouble
から作るのは安全なのかと聞かれて、一瞬悩んでしまったのでした。とりあえず、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進数に変換するのと同じことですから、NSDecimalNumber
にDouble
を与えるとおかしくなるというのは辻褄が合わない話に思えます。
もちろん、変な数を考えるとうまく行かなくなることはあります。
let x = 0.09999999999999986 let y = 0.09999999999999987 _ x == y // => true
ここで、x
とy
は区別できないので、文字列表現も元には戻りません。
_ = (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.97
と15.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.09999999999999986
と0.09999999999999983616
の間には0.09999999999999985
があります。
正しい実装の例
さて、Double
をNSDecimalNumber
に変換する正しいコードは次のようになります。この関数にFloatの15.97
を与えると15.97を指すNSDecimalNumber
が得られます。0.09999999999999986
と0.09999999999999987
のような場合に注意は必要ですが、それは仕方ないでしょう。
ちょっと難しいかもしれませんが、がんばって読んでみてください。
func decimalNumberWithDouble(double: Double) -> NSDecimalNumber { let number = double as NSNumber return NSDecimalNumber(string: number.stringValue) }
参考にしたもの
直接この問題について理解するというよりは、ぼんやり眺めて浮動小数点の周りの空気がわかった気がした。