LoginSignup
2
2

More than 3 years have passed since last update.

Gopher君がゆく Episode I ~ 端数処理と暗黒面

Last updated at Posted at 2020-07-17

まえがき

本稿は、各言語の標準ライブラリにおける端数処理の仕様を通じて、プログラミングの暗黒面に立ち向かう者たちの物語です。

登場言語

言語 処理系 言語 処理系
C GCC 8.3.1 JavaScript node v12.16.1
C++ GCC 8.3.1 Python Python 3.6.8
Golang go1.12.12 Ruby ruby 2.7.0
Java openjdk 11.0.6 Rust rustc 1.43.0

本文中のコードの実行結果はすべて、上記処理系をオプションなしでコンパイルまたは実行したものです。

登場人物

呼称 居住地 概要
パダワン 惑星タトゥイーン 類まれな才能と驚異的なミディ=クロリアン値を持つ。
イニシエイトを卒業したばかりで、未だ修行中の身。
マスター 惑星タトゥイーン 評議会の意向を無視するなど、型破りな騎士。
高い実力と指導力を持つ。
Gopher 君 不定 銀河各所に派遣されるエージェント。
驚異的な身体能力とバランス感覚とを合わせ持つ。
若きナイト
(リクエスター)
惑星コルサント 正義に燃える熱血漢。
パダワンからナイトへ昇格して数年経つ。
グランド・マスター 惑星コルサント 最高評議会を統率する長老。

免責事項

  • この記事は 10 進数における整数への丸め処理(少数第1位以下の切り捨て、切り下げ、切り上げ、四捨五入)を扱うものであり、任意の桁位置における丸め処理については扱っていません。
    • 本稿では、負の無限大への丸め (rounding toward minus infinity) を「切り下げ」、ゼロへの丸め (rounding toward zero) を「切り捨て」と呼んでいます。
  • また、有限桁数の 2 進数表現に伴う浮動小数点数の丸め誤差、および、それに伴う 10 進数表現への影響については扱っていません。
  • 本編中で使われている以下の用語は物語の性質上必要となるものですが、2020年7月現在、日本国内で一般的に認知されている表現ではありません。
    • プログラム (program)、および、プログラミング言語 (programming language) のことを「セーバー」と呼ぶことがあります。
    • また同様に、プログラマ (programmer) のことを「セーバー使い」と呼ぶことがあります。
    • 数直線上の「負の無限大から原点(0)までの範囲」を「ダークサイド」または「暗黒面」と呼ぶことがあります。
    • また同様に、数直線上の「原点(0)から正の無限大までの範囲」を「ライトサイド」と呼ぶことがあります。
  • 本文中にエンベデッドされた映像 (GIF animation) には音声が含まれておりません。臨場感が不足していると感じられる場合は、ご自身で音源をご用意いただいた上で再生してください。5.1ch または 7.1ch の音源と再生装置をご利用いただくことにより、より臨場感を味わうことができるでしょう。なお、適切な音源がご用意できない場合は、こちら よりお選びいただくこともできます。

本編

A long time ago in a galaxy far, far away....
(遠い昔、はるか彼方の銀河系で …)

銀河共和国に暗雲が立ち込めていた。

暗黒卿に操られた独立星系連合の動きによって、銀河の平和と秩序を維持する共和騎士たちがダークサイドにおびき寄せられる事件が多発しているのだ。

数直線上のダークサイドである「負の領域」に一旦足を踏み入れてしまうと、修練を積んだナイトと言えども 「切り捨て」と「切り下げ」を誤用してしまう恐れが大きい。

これを危惧した最高評議会は、イニシエイト卒業者向けの指導方針を策定、各地へ Gohper君 を派遣することを決議した。

プロローグ

その頃、惑星タトゥイーン では …

小遣い欲しさに こっそりと ピット・ドロイド を売ってしまったことがマスターに見つかり、パダワンは泣く泣くそれを ジャワ族 から返品してもらったところだった。

モス・エスパに発着するスターシップを見上げながら、ゼルリック・ドローを歩いて帰宅すると、早速、彼は お小遣い帳に 返金の登録を行った。
だが、登録した画面を見てパダワンは驚愕した。

お小遣い帳

摘要 金額 消費税 10% 税込金額 残高
繰り越し 5 credit
ドロイド販売 305 credit 30 credit 335 credit 340 credit
ドロイド返品 -305 credit -31 credit -336 credit 4 credit

パダワン「げっ、バグだ!」
パダワン「1 クレジット損したみたいになってんじゃん!」
パダワン「現金と帳簿が合わないと、またマスタにどやされちゃうよ」

パダワン「えーと、 305 の 10% は 30.5 だから、これを切り捨てると 30 で …」
パダワン「えーと、-305 の 10% は-30.5 だから、これを切り捨てると-30 になるはず …」

JavaScript 製の セーバー を開いて見てみたが、一向に原因がわからなかった。

calcTax.js
function calcTax(amount) {
  const  tax = 0.1;
  return Math.floor(amount * tax);
}

パダワン「税金の計算は『販売』も『返品』もこの calcTax() の中で …」
パダワン「金額(amount) に税率 (tax=0.1) を掛けてから」
パダワン「floor() 関数で少数第1位を『切り捨て』て、整数に丸めてる …」

パダワン「なんで 販売のときは 30 credit で、返品の時は -31 credit なんだ???」

パダワン「JavaScript のバグっしょ、これー!」

そこでパダワンは、習いたての Golang でセーバーを書き換えてみた。

calc_tax.go
import ("math")
...
func CalcTax(amount float64) float64 {
  const tax = 0.1
  return math.Floor(amount * tax)
}

パダワン「JavaScript の Math.floor() は …」
パダワン「Golang では math.Floor() …」

パダワン「あ、大文字と小文字が逆だ (笑)」

パダワン「なんで?!」
パダワン「仲悪いのかなぁ。。」

パダワン「よし!これで実行してみっぞ!」

だが、それでも結果は変わらなかった。
原因が一向に分からないので、彼はマスターに相談することにした。

パダワン「ねえマスタ」
パダワン「JavaScript も Golang もバグってんだけど!」
そう言って彼は、マスターにお手製のセーバーを差し出した。

セーバーに目をやると、マスターはすぐにその原因に気が付いた。
マスター「おお、、パダワンよ…」

マスター(評議会の通達はこのことだったのか…)
マスター(こんな外縁星系まで暗黒面に蝕まれようとしているのか…)

マスター「良いかパダワンよ」
マスター「フォースは正しく使わねばならない」
マスター「これは『切り下げ』と『切り捨て』の誤用だ」

マスター「そもそも、返品の処理で税額を計算し直しているロジックの方にも問題があるのだが…」

マスター「まあいい。ちょうどいいタイミングだ」
マスター「『切り上げ』や『切り捨て』には種類があるのだ」
マスター「お前はそれらの正しい使い方を覚える必要がある」

マスター「今日は Gopher 君に協力してもらい、Ceil, Floor, Truncate の使い方を伝授する」

マスター「どう直せばいいかは教えんぞ!」
マスター「これらの使い方を覚えたら、自分で答えを探すんだ」

Ceil (正の無限大への丸め)

マスター「これから Ceil を一発で覚える 方法を伝授する」

パダワン「天井!」

マスター「そう。いわゆる『切り上げ』だ。いいか?」
パダワン「うん」

マスター「まず数直線上に立つ」
マスター「次に 正の無限大の方角 に身を構えるのだ!」

マスター「Ceil だけに 正(Cei) だ!」

パダワン(ダジャレかよ!)

マスター「いいか!どこに立っていても同じだ」
マスター「ダークサイド(負の領域)だろうが、ライトサイド(正の領域)だろうが、」
マスター「『正の無限大』方向へ身を構えるのだ!」

マスター「あとはキリのいいポジションまで跳躍すれば良い」

マスター「もう一度言う!」
マスター「Ceil だけに 正(Cei) だ!」

マスター「Gopherくん。お手本を頼む」

gopher_ceil.gif

マスター「どうだ?」
マスター「覚えたか?」

パダワン「うん!」
パダワン「鳥がいっぱい」

マスター「…」
マスター「ホリネズミだ」

Floor (負の無限大への丸め)

マスター「これから Floor を一発で覚える 方法を伝授する」

パダワン「床!」

マスター「そう。いわゆる『切り下げ』だ。いいか?」
パダワン「うん」

マスター「まず数直線上に立つ」
マスター「次に 負の無限大の方角 に身を構えるのだ!」

マスター「Floor だけに 負(Fu) だ!」

マスター「いいか!どこに立っていても同じだ」
マスター「ダークサイド(負の領域)だろうが、ライトサイド(正の領域)だろうが、」
マスター「『負の無限大』方向へ身を構えるのだ!」

マスター「あとはキリのいいポジションまで跳躍すれば良い」

マスター「もう一度言う」
マスター「Floor だけに 負(Fu) だ!」

マスター「Gopherくん。お手本を頼む」

gopher_floor.gif

マスター「これがバグの原因だ」
マスター「セーバーに問題があるのではない。お前の修行が足りないのだ」
マスター「どうだ?」
マスター「覚えたか?」

パダワン「うん!」
パダワン「右端が手抜き」

Trancate (ゼロへの丸め)

マスター「これから Trancate を覚える 方法を伝授する」

パダワン「ん?」
パダワン「これは『一発で』覚えられないの?」

マスター「…」

マスター「これは、いわゆる『切り捨て』のことなんだが…」
マスター「これには流派がいろいろあるのだ」

マスター「例えば、 JavaScript なら parseInt() だ」

マスター「まず数直線上に立つ」
マスター「次にゼロの方角 、つまり内側に身を構えるのだ!」

マスター「Int だけに 内側(In) だ!」
マスター「あとはキリのいいポジションまで跳躍すれば良い」

マスター「もう一度言う」
マスター「Int だけに 内側(In) だ!」

パダワン「ちょっと待って!」
パダワン「これはダジャレにしてもちょっと無理があるよ…」
パダワン「それに僕のセーバーは Golang で書き換えたんだ!」

マスター「Golang なら、型変換 (type conversions) を使うことができる」
マスター「i := int(f) のような形だ」

パダワン「なんだよ!マスタ」
パダワン「むやみに縮小変換をするなって、あんなに言ってたじゃんか!」
パダワン「それに僕は math パッケージを使いたいんだ!」

マスター「math パッケージの場合は Trunc() なのだが…」

パダワン「なんだよ!マスタ」
パダワン「それじゃ 『Int だけに 内側(In) だ~~!』 も使えねーじゃん!」

マスター「その場合は …」
マスター「旅行かばんのトランクとか、車のトランクとか…」
マスター「中(in)に入るものをイメージするのだ!」

パダワン「それって Trunk じゃね??? 綴り違うし…」
パダワン「もういいよ!わかったよ!」

マスター「…」
マスター「それではGopherくん。お手本を頼む」

gopher_trunc.gif

マスター「どうだ?」
マスター「覚えたか?」

パダワン「うん!」
パダワン「なんかアクションが地味」

Round (四捨五入)

翌日のこと…

昨日の Truncate の修行でパダワンは自信をなくしていた。
「あんなん覚えられねーし!」
「あんな地味な技なんか使いたくねーよ!」

Golang への憧れは消え去り、気が付けばセーバーを JavaScript で書き直していた。
しかも「切り捨て」を諦め、マスターの居ないところで一人「四捨五入」を試していたのだ。

パダワン「今日から僕は消費税を『四捨五入』で徴収することに決めたんだ!」

calcTax.js round 版
function calcTax(amount) {
  const  tax = 0.1;
  return Math.round(amount * tax);
}

パダワン「えーと …」

パダワン「floor() 関数を、」
パダワン「round() 関数に書き換えて …」

パダワン「よし!これで実行してみっぞ!」

修正したセーバーを起動してパダワンは驚愕した。

お小遣い帳 (JavaScript Round 版)

摘要 金額 消費税 10% 税込金額 残高
繰り越し 5 credit
ドロイド販売 305 credit 31 credit 336 credit 341 credit
ドロイド返品 -305 credit -30 credit -335 credit 6 credit

パダワン「げっ、バグだ!」

パダワン「今度は 1 クレジット得したみたいになってんじゃん!」
パダワン「現金と帳簿が合わないと、またマスタにどやされちゃうよ」

パダワン「えーと、 305 の 10% は 30.5 だから、これを四捨五入すると 31 で …」
パダワン「えーと、-305 の 10% は-30.5 だから、これを四捨五入すると-31 になるはず …」

パダワン「げっ、JavaScript のバグじゃん!」

しばらく考えたあと、、
パダワンは勇気を振り絞ってもう一度 Golang を試してみることにした。

calc_tax.go Round 版
import ("math")
...
func CalcTax(amount float64) float64 {
  const tax = 0.1
  return math.Round(amount * tax)
}

パダワン「えーと …」
パダワン「JavaScript の大文字と小文字を逆にすれば Golang になりますよっ、と…」

パダワン「よし!これで実行してみっぞ!」

お小遣い帳 (Golang Round 版)

摘要 金額 消費税 10% 税込金額 残高
繰り越し 5 credit
ドロイド販売 305 credit 31 credit 336 credit 341 credit
ドロイド返品 -305 credit -31 credit -336 credit 5 credit

パダワン「おお!直った!」
パダワン「Golang 最高じゃん!」

パダワンは笑顔と自信を取り戻していた。
それが自信ではなく、過信であることも知らずに。。

パダワン「JavaScript なんて二度と使わねーし!」
パダワン「もう Round しか使わねー!」

パダワン「Golang 最高!」

彼はこれを機に、少しづつ暗黒面へと足を踏み入れてゆくのだった …

エピローグ(言語による Round の振舞いの違い)

その頃、惑星コルサント では …

グランド・マスターはプルリクをレビューしていた。
その提案は、例の Ceil, Floor, Truncate の指導方針に Round を追加するものだった。

リクエスター「これは暗黒面に陥りやすいセーバーを明確にするものです」
リクエスター「-0.5 における Round の振舞いを Gopher君に検証させました」
リクエスター「それではご覧ください」
gopher_round-0.5.gif

グランド・マスター「…」
グランド・マスター(なんなのだ?)
グランド・マスター(この過剰な演出と恣意的な照明効果は …)

-0.5 を四捨五入する処理の言語による違い

言語 コード 標準出力 処理系
Java System.out.println( Math.round(-0.5) ); 0 openjdk 11.0.6
Python print( round(-0.50) ) 0 Python 3.6.8
JavaScript console.log( Math.round(-0.5) ); -0 node v12.16.1
C printf("%.0f\n", round(-0.5)); -1 GCC 8.3.1
C++ std::cout << std::round(-0.5) << std::endl; -1 GCC 8.3.1
Golang fmt.Printf( "%.0f\n", math.Round(-0.5) ) -1 go1.12.12
Ruby puts -0.5.round -1 ruby 2.7.0
Rust println!("{}", (-0.5 as f64).round() ); -1 rustc 1.43.0

JavaScript の出力が -0 になっている点については以下を参照してください。
Wikipedia IEEE 754における負のゼロ

リクエスター「Java, JavaScript, Python 以外のセーバーは暗黒面に陥りやすいことを示しています」
リクエスター「これらの セーバー使い から、騎士の称号を剥奪すべきです!」

グランド・マスター「…」

グランド・マスター「これを承認することはできん」
リクエスター「な、なぜです!?」

グランド・マスター「若きナイトよ」
グランド・マスター「お主はこれを 0.5 でも確認したのか?」

リクエスター「いえ、それが何か?」

グランド・マスター「Gopher君、お願いできるかな」

gopher_round+0.5.gif

グランド・マスター「…」
グランド・マスター(い、いかん、、)
グランド・マスター(照明効果がまだ残っておる …)

0.5 を四捨五入する処理の言語による違い

言語 コード 標準出力 処理系
Java System.out.println( Math.round(0.5) ); 1 openjdk 11.0.6
JavaScript console.log( Math.round(0.5) ); 1 node v12.16.1
Python print( round(0.50) ) 0 Python 3.6.8
C printf("%.0f\n", round( 0.5)); 1 GCC 8.3.1
C++ std::cout << std::round(0.5) << std::endl; 1 GCC 8.3.1
Golang fmt.Printf( "%.0f\n", math.Round(0.5) ) 1 go1.12.12
Ruby puts 0.5.round 1 ruby 2.7.0
Rust println!("{}", (0.5 as f64).round()); 1 rustc 1.43.0

リクエスター「な、なんと … 」
リクエスター「ライトサイドに居ながら Python が暗黒面に向かっているのがわかります!」

グランド・マスター「そうじゃ。Python では 半数 は偶数方向に丸められる」
グランド・マスター「セーバーがそれぞれ異なる仕様を持つのには理由があるのじゃ」

Python 3 の丸めの仕様については以下を参照してください。
Python 3 Documentation 組み込み関数 round

グランド・マスター「若きナイトよ」
グランド・マスター「お主が銀河共和国に平和と秩序をもたらしたいという気持ちは間違っておらん」

グランド・マスター「だが、多様性を排除しようとしてはならん。それこそ暗黒卿の思う壺じゃ」

グランド・マスター「それに、無限に連なる数直線上でちょうど半数に身を構えることなど、」
グランド・マスター「人生ではほとんどないのじゃ」

グランド・マスター「もし偶然半数に身を置いたとして、、」

グランド・マスター「少しでも重心を暗黒面の方に傾ければ、」
グランド・マスター「どのセーバー使いも、たちまち暗黒面に引き寄せられてしまう」

グランド・マスター「逆も同じじゃ」
グランド・マスター「常に重心をライトサイドに傾ける心構えができておれば、」
グランド・マスター「どのセーバー使いもアシュラに戻ることができる」

各言語の半数付近における Round の振舞い

言語 関数等 -方向
n=-0.51
半数
n=-0.5
+方向
n=-0.49
-方向
n=0.49
半数
n=0.5
+方向
n=0.51
備考
C round(n) -1 -1 -0 0 1 1 math.h
C++ std::round(n) -1 -1 -0 0 1 1 cmath
Golang math.Round(n) -1 -1 -0 0 1 1
Java Math.round(n) -1 0 0 0 1 1
JavaScript Math.round(n) -1 -0 -0 0 1 1
Python round(n) -1 0 0 0 0 1
Ruby n.round -1 -1 0 0 1 1
Rust n.round() -1 -1 0 0 1 1 f64

リクエスター「 … 」
リクエスター「わ、私はなんと愚かなことをしようとしていたのでしょうか …」

グランド・マスター「分かれば良い」
グランド・マスター「誰にでも間違いはある」

グランド・マスター「ダークサイドはセーバーに在らずじゃ」
グランド・マスター「それは常に …」

グランド・マスター「センシティブ達 それぞれの」
グランド・マスター「心の中に在るのじゃ」

そう言うと、彼は穏やかな笑みを浮かべた。

だが、この時、彼はまだ知らなかった。
やがてこの銀河に暗黒の時代が訪れようとしていることを …

巨大都市の煌びやかな光を放つビル群は、赤い夕陽にやさしく包まれていた。

あとがき

いかがでしたか?
みなさんも、正負による丸めの違いと、他言語への移植には充分ご注意くださいね。

そして、端数処理に限らずプログラミングの暗黒面はあらゆるところに潜んでいます。

「これ処理系のバグじゃん!」
「これミドルウェアのバグじゃん!」
「これ API のバグじゃん!」

「これアイツのバグじゃん!」

と思った時は、自分の書いたコードを冷静に振り返ってみましょう。

それでは

May the Force be with you!
(フォースと共にあらんことを!)

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2