"%.17g" = doubleの出力をポータブルにする魔法の呪文?

遅まきながら、明けましておめでとうございます。

諸般の事情で浮動小数点と取っ組み合っています。

懸命な読者であれば、浮動小数点表記は"eyes only"なものであり、データの交換に用いるのは慎むべきであることはご存知かと思われますが、実はそうも言ってられなくなっていることに気づいた人はどれくらいいらっしゃるのでしょうか…

そう。[JSON]です。そのままJavaScriptで`eval`できちゃう、[RFC7159]で規定された、今や世界で最も利用されているであろうデータ交換フォーマット。

[JSON]: http://json.org/

JavaScriptで`eval`できちゃうだけあって、数値はJavaScriptの数値リテラルそのものです。

[RFC7159]: https://tools.ietf.org/html/rfc7159

![](http://json.org/number.gif)

[Hexadecimal Floating Point]なんてお洒落なものは使えません(Hex Floatについては記事を改めて)。

[Hexadecimal Floating Point]: http://www.exploringbinary.com/hexadecimal-floating-point-constants/

つまり、Cにおける`double`、JavaScriptにおける`Number`を、復元できる形で文字列化する必要性は、JSON以前と以後では`1e-64`が`1e+64`になるぐらい変わっているのです。

ところで、今やほとんどの言語は、浮動小数点数をそのまま文字列化できます。「`%`...`e`だっけ`f`だっけ`g`だっけ?桁数指定はいくつが適切か…」とか悩まなくとも、`print(number)`できちゃうのです。しかしその文字列を読み込んだ時、元の値を復元できるでしょうか?

## 調べてみた

* 考え方としては、1bitづつ仮数部を増やしていき、それを文字化した後数値化して、元の数値と一致すればOK、不一致が出たらNGです。

* 文字列変換はなるべく暗示的に行ってます。なので暗示的な文字列変換を持たないCは除外しています。C++の`iostream`は暗示的な文字列変換をやってくれはしますが、ここで確認したいのは「ストリーム」ではなく「バッファー」なのでこれも除外。


* [ideone]や[repl.it]や[IBM Swift Sandbox]など、Webで実行可能な環境がある場合はリンクしてあります。

[ideone]: http://ideone.com
[repl.it]: http://repl.it
[IBM Swift Sandbox]: http://swiftlang.ng.bluemix.net/


### Java 

OK http://ideone.com/OSeb5S

### JavaScript

OK http://ideone.com/qonDEO

さすが[JSON]のふるさとだけあって、きちんと復元できるように文字列化してくれてます。手元で調べた限り、node.jsだけではなく、今日日のブラウザはいずれも大丈夫なようです。

### Perl 5

Not OK http://ideone.com/J7kVwv

残念ながら、Perl本体のみならず、[JSON::XS]などのXSも、Perl APIで文字列化していればやはりNGです。

[JSON::XS]: http://search.cpan.org/dist/JSON-XS/XS.pm

バグレポート: https://rt.perl.org/Public/Bug/Display.html?id=127182

### Perl 6

[rakudo]を実行できるWeb環境が見当たらなかったのでソースも。

[rakudo]: https://github.com/rakudo/rakudo

````ruby
#!/usr/bin/env perl6
use v6;
my Num $finish  = Num(@*ARGS ?? @*ARGS[0] !! 2.0);
die if +$finish == 0;
my Str $format = '%.17g';
my Num $d = $finish / 2;
my Num $f = $d;
for 0..53 {
    my Str $s = ~$d;
    die "Round trip failed for {$d}: off by {+$s - $d}" if +$s != $d;
    say sprintf "%s = 0x%sp0", $s, $d.base(16,16);
    last if $d == $finish;
    $f /= 2, $d += $f
}
````

````
1 = 0x1.0000000000000000p0
1.5 = 0x1.8000000000000000p0
1.75 = 0x1.C000000000000000p0
1.875 = 0x1.E000000000000000p0
1.9375 = 0x1.F000000000000000p0
1.96875 = 0x1.F800000000000000p0
1.984375 = 0x1.FC00000000000000p0
1.9921875 = 0x1.FE00000000000000p0
1.99609375 = 0x1.FF00000000000000p0
1.998046875 = 0x1.FF80000000000000p0
1.9990234375 = 0x1.FFC0000000000000p0
1.99951171875 = 0x1.FFE0000000000000p0
1.999755859375 = 0x1.FFF0000000000000p0
1.9998779296875 = 0x1.FFF8000000000000p0
1.99993896484375 = 0x1.FFFC000000000000p0
Round trip failed for 1.99996948242188: off by 5.10702591327572e-15
  in block <unit> at ./dsd-test.p6 line 10
````

バグレポート: https://rt.perl.org/Public/Bug/Display.html?id=127184

### Python 2

NOT OK https://repl.it/BcF7/0

[ideone]ではなぜかassertionで止まらないので[repl.it]で。

### Python 3

OK https://repl.it/BcFb/0

Python 2との比較のためにこちらも[repl.it]で。

コードはPython 2の場合と全く同一です。

### Ruby

OK http://ideone.com/BrfiBO

### Swift

Not OK   http://swiftlang.ng.bluemix.net/#/repl/937bf49dd2f110c6f4c0fd942de051d734b3e051887b1c6b762c39e2c7956146

````
1.0
1.5
1.75
1.875
1.9375
1.96875
1.984375
1.9921875
1.99609375
1.998046875
1.9990234375
1.99951171875
1.999755859375
1.9998779296875
1.99993896484375
Round trip failed for 1.99996948242188: off by 5.10702591327572e-15
````

これが一番意外だったかも。

バグレポート: https://bugs.swift.org/browse/SR-491

### NGな言語実装の問題点

桁数が足りない、これに尽きます。それもたいていの場合わずか1桁。

## 回避策

### C99 Hexadecimal Floating Point を使う

別の記事にも詳しく書く予定ですが、C99から導入された Hexadecimal Floating Point (hexfloat)を使うと、この問題が一気に解決します。例えば*π*=`0x1.921fb54442d18p+1`、*e*=`0x1.5bf0a8b145769p+1`といった塩梅です。

しかし残念ながら、登場が遅すぎてネイティブでサポートされている環境が少ないのが難点です。ああ、JSONでこれが使えたら…

とはいえ、実装は10進数の場合よりずっと簡単です。私もJS用とPerl 6用を書きました。

* https://github.com/dankogai/js-hexfloat
* https://github.com/dankogai/p6-num-hexfloat

どちらもコメント込みで100行切ってます。そんなに難しくないのですから、各言語とももっと積極的にサポートして欲しいものです。

### "%.17g" でフォーマットする

とはいえ、問題なのは明日の言語より今日のデータ。全ての浮動小数点数リテラルを、生まれる前に置き換えたい。全てのJSON、過去と未来の全ての浮動小数点数リテラルを、この手で、という願いは、ネ申Excelに対する反逆より大変そうです。

で、結論から言うと、`sprintf`のフォーマットをサポートしている環境なら、`"%.17g"`を使えばいいということになります。

* [Printing double without losing precision]によると、Cの`double`、[IEEE 754]のbinary64の精度を10進数で復元するのに必要な(仮数部の)桁数は、17桁。

[Printing double without losing precision]: http://stackoverflow.com/questions/4738768/printing-double-without-losing-precision
[IEEE 754]: https://ja.wikipedia.org/wiki/IEEE_754

* ただしそれ以下で十分な場合、それ以下で済ませたい。なので`"%.16e"`とか`"%.16f"`とかは避けたい。

* ここで`printf`のフォーマットについておさらいすると…

> double in either normal or exponential notation, whichever is more appropriate for its magnitude. 'g' uses lower-case letters, 'G' uses upper-case letters. This type differs slightly from fixed-point notation in that insignificant zeroes to the right of the decimal point are not included. Also, the decimal point is not included on whole numbers. - https://en.wikipedia.org/wiki/Printf_format_string#Type_field

とあります。実際に使ってみると、整数なら小数点なしで表示してくれますし、17桁も必要なければ端折ってくれますし、指数表記の方がコンパクトになる場合はそうしてくれます。

ここでNGな言語実装を見てみると、どうも`"%.16g"`相当のことをやっているように見受けられます。特にSwiftは不思議ちゃんで、標準の文字列化メソッド(`.description`)では一桁足りないのに、REPLだと

````
% swift
Welcome to Apple Swift version 2.1.1 (swiftlang-700.1.101.15 clang-700.1.81). Type :help for assistance.
  1> import Foundation 
  2> log(2.0).description 
$R0: String = "0.693147180559945"
  3> log(2.0)
$R1: Double = 0.69314718055994529
````

と逆に一桁多いのです。

で、実際に`"%.17g"`が使える処理系で試したのが以下のとおりです。

#### C: [計画通り]

* http://ideone.com/5Dq4ip

#### C++: [計画通り]

* http://ideone.com/FOT1cS

#### Java: [あれ?なんか違うかも?]

  * http://ideone.com/2e4Svz
  * 末尾の`0`を省略してくれない。`%.16f`っぽい。

#### Perl 5: [計画通り]

* http://codepad.org/O1PzVIFZ
* お古のPerl 5.8でも動く!

#### Perl 6: [あれ?なんか違うかも?]

`sprintf()`の実装自体にバグ!

````ruby
#!/usr/bin/env perl6
use v6;
my $log2 = log(2);
for 0..20 {
    say sprintf "%%.%dg:\t%.{$_}g", $_, $log2;
}
````

````
%.0g:	1
%.1g:	0.7
%.2g:	0.69
%.3g:	0.693
%.4g:	0.6931
%.5g:	0.69315
%.6g:	0.693147
%.7g:	0.6931472
%.8g:	0.69314718
%.9g:	0.693147181
%.10g:	0.6931471806
%.11g:	0.69314718056
%.12g:	0.69314718056
%.13g:	0.6931471805599
%.14g:	0.69314718055995
%.15g:	0.693147180559945
%.16g:	0.693147180559945
%.17g:	0.693147180559945
%.18g:	0.693147180559945
%.19g:	0.693147180559945
%.20g:	0.693147180559945
````

バグレポート: https://rt.perl.org/Public/Bug/Display.html?id=127201

#### Ruby: [計画通り]

  * http://ideone.com/RVRwGy

#### Python 2: [計画通り]

* http://ideone.com/OJR8or
* Python 3でも動く!

#### Swift: [計画通り]

* `String(format: ...)`がLinuxではNGなのでOS Xで動作確認したソースを以下に。

````swift
#!/usr/bin/env swift
#if os(OSX)
    import Cocoa
#elseif os(iOS)
    import UIKit
#elseif os(Linux)
    import Glibc
#endif
let finish = 2.0
var d = finish / 2
var f = d
for i in 0...53 {
    let s = String(format:"%.17g", d)
    if Double(s)! != d {
        print("Round trip failed: \(Double(s)! - d)")
        break;
    }
    print(s)
    f /= 2
    d += f
}
````

[計画通り]: http://dic.pixiv.net/a/%E8%A8%88%E7%94%BB%E9%80%9A%E3%82%8A
[あれ?なんか違うかも?]: http://dic.nicovideo.jp/a/%E3%81%82%E3%82%8C%3F%E3%81%AA%E3%82%93%E3%81%8B%E9%81%95%E3%81%86%E3%81%8B%E3%82%82%3F

というわけで、迷ったら`"%.17g"`でフォーマットしとけというのは覚えておいても良さげです。(まあJavaやPerl 6のように、フォーマットが違ったりバグがあったりする場合を除けば)。覚えにくいのでデフォルトにしてもらいたいぐらいなのですが、`[efg]`のデフォルトは6桁なのはなぜだろう…

Dan the Bit-Picking Coder