職業プログラマの休日出勤

職業プログラマによる日曜自宅プログラミングや思考実験の成果たち。リアル休日出勤が発生すると更新が滞りがちになる。記事の内容は個人の意見であり、所属している(いた)組織の意見ではない。

正接90°

正接とは、三角関数タンジェント(tan)のことです。
突然ですが、tan 90° の値は何でしょうか?
普通に考えれば未定義、極限的な考え方をすれば、近づく方向によって正の無限大だったり負の無限大だったり、といったところでしょう。

では、コンピュータに tan 90° を計算させると、何が起きるのでしょうか?

f:id:t_motooka:20190609084423p:plain
macOS 付属の「Grapher」で y = tan x を描いたもの

JavaScriptでの計算

Math.tanが取る引数はラジアン単位の角度なので、JSで tan 90° を計算するには Math.tan(Math.PI / 2) となります。
この計算結果は、なんと 16331239353195370 という、とても大きな正の整数になります。

何が起きているのでしょうか?

勘の良い皆さんは既に想像がついていることだろうと思いますが、そう、ここで使っている円周率の値 Math.PI は理論値ではなくて近似値なのです。3.141592653589793という、有効数字桁数が16桁である値を返してきます。ちなみに、円周率のこの次の数字は「2」です。つまり、このMath.PIの近似値は、理論値よりもちょっと小さいということが言え、これこそが、正接の関数がとても大きな値を返した理由です。

Infinity あるいは NaN を出したい!

こういうふうに背景を考察していくと、理論値通りの π/2 をtan関数に与えるのは無理なような気がしてきます。
でも、人生あきらめるのはまだ早い!若干無茶な欲望かもしれませんが、Infinity あるいは NaN を出してみようではありませんか!

1000桁のπ

Wikipediaの円周率のページに、小数点以下1000桁まで円周率が掲載されていますので、これを与えてみましょう。

Math.tan(3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989 / 2)

この結果は、残念ながら変わらず 16331239353195370 でした。
実行した後に「あっ、もしかして、2で除算した瞬間に有効数字桁数16桁くらいに丸められているのでは?」と思って確認したところ、やはり丸められていました。この方法では、長い桁数を与えても無駄そうです。やるとしたら、π/2 の値を直接指定するのが良さそうです。

π/2 計算をしてみる

π計算と言えば様々な数学者や計算機技術者を魅了して止まないプロジェクトです。今年の3月14日に、Googleさんの中の人がGCP上でπ計算をやって新記録を樹立したというニュースも記憶に新しいところです。
japan.googleblog.com

いっぽう私は、Chromeの開発者コンソールのJavaScript実行環境において、次のような現象を発見していました。

Math.tan(1.570796326794896669021284196)
16331239353195370
Math.tan(1.570796326794896669021284197)
-6218431163823738
Math.tan("1.570796326794896669021284196")
16331239353195370
Math.tan("1.570796326794896669021284197")
-6218431163823738

この結果からは、次のようなことが言えます。

  • JSの Math.tan 関数が NaN や ±∞ を返す値は、もしも存在するのならば、1.570796326794896669021284196 よりも大きく、 1.570796326794896669021284197 よりも小さい。
    • そして、この値は π/2 の理論値からはちょっとズレている。 π/2 の理論値は 1.570796326794896619 となっていくはずなのに、この桁数でいう最後の「19」の部分が、「69」になっている。このズレの箇所は、ちょうど有効数字桁数16桁を超えたあたりの場所である。
  • 数値計算を伴わずに、とても大きな数を Math.tan 関数に与えれば、有効数字桁数28桁の数字もきちんと読み取ってくれる。そして、もっと大きな桁のものも読み取ってくれそうな気がする。
  • 同じことが、文字列として数値を与えた場合についても言える。

ということは、「JSの Math.tan 関数が NaN や ±∞ を返す値」を二分探索的な手法で求めることができそうです。
そこで、次のようなプログラムを書いてみました。「普通の」再帰呼び出しのある二分探索のコードを書くと stack overflow の臭いがプンプンしてしまいますので、ちょっと遅いですが1桁ずつ丁寧に心を込めて線形探索していきましょう。(1桁ずつ二分探索しても良いのですが…😅)
ちなみに、Chromeのconsoleから無限ループを仕込むと、最悪の場合はChromeを強制終了する必要に迫られるので、プログラムを書き換えて使う場合は十分に注意して下さい。コピペミス等にも注意です。また、以下のコードであっても処理系によっては無限ループに陥る可能性があります。兵庫県警とかに逮捕されないように気を付けましょう。また、ミスった場合はOS強制再起動を迫られる可能性もありますので、重要なデータは保存してバックアップした状態で実行するのが良いでしょう。一方で、NodeJSのCLIから動かすときはCtrl-Cで簡単に停止できるので、不安な方はこちらの方が良いでしょう。

let x="1.570796326794896669021284196";
while(true) {
  for(i=1; i<10; i++) {
    let y=x+i;
    let tan=Math.tan(y);
    if(tan===Infinity || tan===-Infinity || tan===NaN || tan===undefined){console.log(y); throw "Done.";}
    if(tan<0){x=x+(i-1); break;}
    if(i===9){x=x+i; break;}
  }
  if(x.length>1000) {console.log((x.length-1)+"digits"); console.log(x);break;}
}

この実行結果は

1.5707963267948966690212841967877466231584548950195312500000000...

となり、最後は「0」がループする循環小数になってしまいました。3万桁まで計算しても同じだったので、恐らく、現行のバージョンでは Math.tan 関数が ±∞ や NaN などを返すことは無さそうです。残念!

なぜ tan 90° に目を向けたのか?

SVGの skew (shear、剪断) に関するプログラムを書いている途中で、「90°のskewを与えたらSVGレンダラはエラーを吐くんだろうか?」と疑問に思ったことが始まりです。
Coordinate Systems, Transformations and Units – SVG 1.1 (Second Edition)
ちょっと長いですが、この説明から、skew の処理はアフィン変換の変換行列の中に正接が登場することが述べられていることがわかるでしょう。

おまけ1:Google先生正接90°

  • tan 90degググると計算結果として 1.6331239e+16 が得られます。
  • tan 90.000000000000001degググると計算結果として 1.6331239e+16 が得られます。つまりtan 90degと同じです。
  • tan 90.00000000000001degググると計算結果として -6.2184312e+15 が得られます。

これらことから考えると、Google先生の計算機も、やはり有効数字桁数16〜17桁程度の計算をしているのだろうと思われます。

おまけ2:カシオ計算機さんの「KEISAN」

keisan.casio.jp

  • tan pi/2は正の無限大が得られます。
  • tan pi/2*3は負の無限大が得られます。
  • tan pi/2*5は正の無限大が得られます。

中身がどうなっているのかよくわかりませんが、冒頭に掲げた、極限の考え方をしたときの解が得られます。90°相当のときは正の無限大に、270°相当のときは負の無限大が得られるところは、とっても興味深いところですが、これは恐らく sin θ の符号をそのまま使っているということなのでしょう。

環境情報

検証に使った環境の情報です。