自己紹介
ハトネコエです。
「プログラマのための数学LT会」を主催しております。
次回の第3回を年明けに開催したい気持ちですので、その際はみなさまご登壇などのご協力をよろしくお願いします。
前置き
四捨五入と言えば、 1.4 が 1 になり、 1.5 が 2 になることでおなじみです。
ですが、ここで 2.5 を四捨五入すると 2 になる と聞くと驚くでしょうか?
私は驚きました、知りませんでした。
JIS Z 8401 や ISO 31-0 (およびその改定である ISO 80000-1 )や IEEE 754 といった標準規格の制定書には、
1.5 や 2.5 のような、丸め先の整数に同じだけ近い数(小数点第1位が5である小数)は、偶数となるほうに丸めるという方法が紹介されています。
そのほうが誤差が少なくなるそうです。
こちらの記事が詳しいです。(※記事中に ISO 3110 とありますが、それは ISO 31-0 の誤りです)
JIS, ISO 式四捨五入
では、各プログラミング言語やソフトウェアでは四捨五入はどのような実装になっているのでしょうか?
気になって、いくつかのプログラミング言語での round
メソッドの挙動を見てみることにしました。
各言語で四捨五入してみよう
- 1.5 + 2.5 + 3.5 + 4.5 = 12
- 1.5 * 2.5 * 3.5 = 13.125
- -0.5 + -1.5 + -2.5 = -4.5
これらの式について、それぞれの数を四捨五入した上で計算してみましょう。
各プログラミング言語での実行にあたっては paiza.io を大いに活用させていただきました。ありがとうございました。
以下、2.5を四捨五入して3にするものは学校教育と同じなので School タイプ、近い偶数である2にするものは Even タイプと記すことにします。
(これはSwiftのドキュメントにて "schoolbook rounding" という表記がなされていることに影響されています)
C++
School!
#include <iostream>
#include <cmath>
int main(void){
std::cout << std::round(1.5) + std::round(2.5) + std::round(3.5) + std::round(4.5) << '\n'; // 14 = 2 + 3 + 4 + 5
std::cout << std::round(1.5) * std::round(2.5) * std::round(3.5) << '\n'; // 24 = 2 * 3 * 4
std::cout << std::round(-0.5) + std::round(-1.5) + std::round(-2.5) << '\n'; // -6 = -1 + -2 + -3
}
C#
Even!
using System;
public class Test{
public static void Main(){
Console.WriteLine(Math.Round(1.5) + Math.Round(2.5) + Math.Round(3.5) + Math.Round(4.5)); // 12 = 2 + 2 + 4 + 4
Console.WriteLine(Math.Round(1.5) * Math.Round(2.5) * Math.Round(3.5)); // 16 = 2 * 2 * 4
Console.WriteLine(Math.Round(-0.5) + Math.Round(-1.5) + Math.Round(-2.5)); // -4 = 0 + -2 + -2
Console.WriteLine(Math.Round(1.5, MidpointRounding.AwayFromZero) + Math.Round(2.5, MidpointRounding.AwayFromZero) + Math.Round(3.5, MidpointRounding.AwayFromZero) + Math.Round(4.5, MidpointRounding.AwayFromZero)); // 14 = 2 + 3 + 4 + 5
Console.WriteLine(Math.Round(1.5, MidpointRounding.AwayFromZero) * Math.Round(2.5, MidpointRounding.AwayFromZero) * Math.Round(3.5, MidpointRounding.AwayFromZero)); // 24 = 2 * 3 * 4
Console.WriteLine(Math.Round(-0.5, MidpointRounding.AwayFromZero) + Math.Round(-1.5, MidpointRounding.AwayFromZero) + Math.Round(-2.5, MidpointRounding.AwayFromZero)); // -6 = -1 + -2 + -3
}
}
C#での Round
はデフォルトではEven型ですが、
引数に MidpointRounding.AwayFromZero
を指定してあげるとSchool型になります。
デフォルトの場合は、引数に MidpointRounding.ToEven
が指定されているときと同じ挙動のようです。
Java
School!
System.out.println(Math.round(1.5) + Math.round(2.5) + Math.round(3.5) + Math.round(4.5)); // 14 = 2 + 3 + 4 + 5
System.out.println(Math.round(1.5) * Math.round(2.5) * Math.round(3.5)); // 24 = 2 * 3 * 4
System.out.println(Math.round(-0.5) + Math.round(-1.5) + Math.round(-2.5)); // -3 = 0 + -1 + -2
注目すべきは負の数での四捨五入です。
今回検証した中では Java と JavaScript だけがこの挙動を見せました。
-0.4 は 0 に、 -0.6 は -1 に round されますが、 -0.5 は 0 に round されます。
小数点第1位が5であるときは必ず +∞ 方向へ繰り上げがなされるようですね。
他の言語では絶対値としての四捨五入でしたので、この違いは注意です。
Kotlin
Even!
round
メソッドは Kotlin 1.2 から導入されています。
( http://kotlinlang.org/api/latest/jvm/stdlib/kotlin.math/round.html )
paiza.io はまだ1.1系ですので Playground を使うか、もしくはコンソールから kotlinc-jvm
でREPLを呼び出すことで試せます。
println(kotlin.math.round(1.5) + kotlin.math.round(2.5) + kotlin.math.round(3.5) + kotlin.math.round(4.5)) // 12.0 = 2 + 2 + 4 + 4
println(kotlin.math.round(1.5) * kotlin.math.round(2.5) * kotlin.math.round(3.5)) // 16.0 = 2 * 2 * 4
println(kotlin.math.round(-0.5) + kotlin.math.round(-1.5) + kotlin.math.round(-2.5)) // -4.0 = 0 + -2 + -2
これは内部実装としてはJavaの Math.rint
を呼んでいるだけなので、以下のようにしても同じ結果になります。
println(Math.rint(1.5) + Math.rint(2.5) + Math.rint(3.5) + Math.rint(4.5)) // 12.0 = 2 + 2 + 4 + 4
println(Math.rint(1.5) * Math.rint(2.5) * Math.rint(3.5)) // 16.0 = 2 * 2 * 4
println(Math.rint(-0.5) + Math.rint(-1.5) + Math.rint(-2.5)) // -4.0 = 0 + -2 + -2
もちろん Math.round
を利用すると、先ほどのJavaと同じ結果になります。
println(Math.round(1.5) + Math.round(2.5) + Math.round(3.5) + Math.round(4.5)); // 14 = 2 + 3 + 4 + 5
println(Math.round(1.5) * Math.round(2.5) * Math.round(3.5)); // 24 = 2 * 3 * 4
println(Math.round(-0.5) + Math.round(-1.5) + Math.round(-2.5)) // -3 = 0 + -1 + -2
JavaScript
School!
Javaと名前が似ているJavaScriptですがコードも似ています。
そしてJavaと同じ結果を示します。
console.log(Math.round(1.5) + Math.round(2.5) + Math.round(3.5) + Math.round(4.5)); // 14 = 2 + 3 + 4 + 5
console.log(Math.round(1.5) * Math.round(2.5) * Math.round(3.5)); // 24 = 2 * 3 * 4
console.log(Math.round(-0.5) + Math.round(-1.5) + Math.round(-2.5)); // -3 = 0 + -1 + -2
R言語
Even!
数値計算でよく使われるプログラミング言語なだけあって、やはり誤差が少なくなる偶数への丸めの手法をとっています。
cat(round(1.5) + round(2.5) + round(3.5) + round(4.5)) # 12 = 2 + 2 + 4 + 4
cat(round(1.5) * round(2.5) * round(3.5)) # 16 = 2 * 2 * 4
cat(round(-0.5) + round(-1.5) + round(-2.5)) # -4 = 0 + -2 + -2
Python2
School!
print(round(1.5) + round(2.5) + round(3.5) + round(4.5)) # 14.0 = 2 + 3 + 4 + 5
print(round(1.5) * round(2.5) * round(3.5)) # 24.0 = 2 * 3 * 4
print(round(-0.5) + round(-1.5) + round(-2.5)) # -6.0 = -1 + -2 + -3
Python3
Even!
print(round(1.5) + round(2.5) + round(3.5) + round(4.5)) # 12 = 2 + 2 + 4 + 4
print(round(1.5) * round(2.5) * round(3.5)) # 16 = 2 * 2 * 4
print(round(-0.5) + round(-1.5) + round(-2.5)) # -4 = 0 + -2 + -2
おもしろいことにPython2と違う結果になりました。
Python2のコードをPython3へ移植するときは要注意なポイントです。
NumPy
Even!
Pythonの数値計算ライブラリである NumPy を用いた round
も試してみました。
結果は以下の通りで、これはありがたいことにPython2でもPython3でも同じ実行結果を示してくれます。
import numpy
print(numpy.round(1.5) + numpy.round(2.5) + numpy.round(3.5) + numpy.round(4.5)) # 12.0 = 2 + 2 + 4 + 4
print(numpy.round(1.5) * numpy.round(2.5) * numpy.round(3.5)) # 16.0 = 2 * 2 * 4
print(numpy.round(-0.5) + numpy.round(-1.5) + numpy.round(-2.5)) # -4.0 = 0 + -2 + -2
PHP
School!
echo round(1.5) + round(2.5) + round(3.5) + round(4.5) . "\n"; // 14 = 2 + 3 + 4 + 5
echo round(1.5) * round(2.5) * round(3.5) . "\n"; // 24 = 2 * 3 * 4
echo round(-0.5) + round(-1.5) + round(-2.5) . "\n"; // -6 = -1 + -2 + -3
Ruby
School!
puts 1.5.round + 2.5.round + 3.5.round + 4.5.round # 14 = 2 + 3 + 4 + 5
puts 1.5.round * 2.5.round * 3.5.round # 24 = 2 * 3 * 4
puts -0.5.round + -1.5.round + -2.5.round # -6 = -1 + -2 + -3
デフォルトではSchoolの結果です。しかし、キーワード引数halfを指定することで、Evenの挙動にすることが可能です。
puts 1.5.round(half: :even) + 2.5.round(half: :even) + 3.5.round(half: :even) + 4.5.round(half: :even) # 12 = 2 + 2 + 4 + 4
puts 1.5.round(half: :even) * 2.5.round(half: :even) * 3.5.round(half: :even) # 16 = 2 * 2 * 4
puts -0.5.round(half: :even) + -1.5.round(half: :even) + -2.5.round(half: :even) # -4 = 0 + -2 + -2
また、小数点第1位が5のときは切り上げをおこなう up、切り捨てをおこなう down も、引数として指定できます。
負の数に関しては、絶対値における切り上げ・切り捨てがおこなわれます。
puts 1.5.round(half: :up) + 2.5.round(half: :up) + 3.5.round(half: :up) + 4.5.round(half: :up) # 14 = 2 + 3 + 4 + 5
puts 1.5.round(half: :up) * 2.5.round(half: :up) * 3.5.round(half: :up) # 24 = 2 * 3 * 4
puts -0.5.round(half: :up) + -1.5.round(half: :up) + -2.5.round(half: :up) # -6 = -1 + -2 + -3
puts 1.5.round(half: :down) + 2.5.round(half: :down) + 3.5.round(half: :down) + 4.5.round(half: :down) # 10 = 1 + 2 + 3 + 4
puts 1.5.round(half: :down) * 2.5.round(half: :down) * 3.5.round(half: :down) # 6 = 1 * 2 * 3
puts -0.5.round(half: :down) + -1.5.round(half: :down) + -2.5.round(half: :down) # -3 = 0 + -1 + -2
Swift
School!
var a = 1.5; var b = 2.5; var c = 3.5; var d = 4.5
a.round(); b.round(); c.round(); d.round()
print(a + b + c + d) // 14.0 = 2 + 3 + 4 + 5
print(a * b * c) // 24.0 = 2 * 3 * 4
a = -0.5; b = -1.5; c = -2.5
a.round(); b.round(); c.round()
print(a + b + c) // -6.0 = -1 + -2 + -3
round()
の挙動は rounded(.toNearestOrAwayFromZero)
の挙動と同じです。
print(1.5.rounded(.toNearestOrAwayFromZero) + 2.5.rounded(.toNearestOrAwayFromZero) + 3.5.rounded(.toNearestOrAwayFromZero) + 4.5.rounded(.toNearestOrAwayFromZero)) // 14.0 = 2 + 3 + 4 + 5
print(1.5.rounded(.toNearestOrAwayFromZero) * 2.5.rounded(.toNearestOrAwayFromZero) * 3.5.rounded(.toNearestOrAwayFromZero)) // 24.0 = 2 * 3 * 4
print(-0.5.rounded(.toNearestOrAwayFromZero) + -1.5.rounded(.toNearestOrAwayFromZero) + -2.5.rounded(.toNearestOrAwayFromZero)) // -6.0 = -1 + -2 + -3
一方で rounded(.toNearestOrEven)
の挙動は異なります。
OrEvenという名前の付いている通り、小数点第1位が5であれば、近い偶数への丸めがおこなわれます。
print(1.5.rounded(.toNearestOrEven) + 2.5.rounded(.toNearestOrEven) + 3.5.rounded(.toNearestOrEven) + 4.5.rounded(.toNearestOrEven)) // 12.0 = 2 + 2 + 4 + 4
print(1.5.rounded(.toNearestOrEven) * 2.5.rounded(.toNearestOrEven) * 3.5.rounded(.toNearestOrEven)) // 16.0 = 2 * 2 * 4
print(-0.5.rounded(.toNearestOrEven) + -1.5.rounded(.toNearestOrEven) + -2.5.rounded(.toNearestOrEven)) // -4.0 = 0 + -2 + -2
MySQL
School!
SELECT ROUND(1.5) + ROUND(2.5) + ROUND(3.5) + ROUND(4.5); -- 14 = 2 + 3 + 4 + 5
SELECT ROUND(1.5) * ROUND(2.5) * ROUND(3.5); -- 24 = 2 * 3 * 4
SELECT ROUND(-0.5) + ROUND(-1.5) + ROUND(-2.5); -- -6 = -1 + -2 + -3
Googleスプレッドシート (Google Sheets)
スプレッドシートの式にコメントはないのですが、MySQL同様 --
をコメントだと思って以下お読みください。
=ROUND(1.5) + ROUND(2.5) + ROUND(3.5) + ROUND(4.5) -- 14 = 2 + 3 + 4 + 5
=ROUND(1.5) * ROUND(2.5) * ROUND(3.5) -- 24 = 2 * 3 * 4
=ROUND(-0.5) + ROUND(-1.5) + ROUND(-2.5) -- -6 = -1 + -2 + -3
なお、 ROUNDUP
, ROUNDDOWN
という関数がありますが、こちらはそれぞれ「切り上げ」「切り捨て」に該当します。
Rubyの round(half: :up)
や round(half: :down)
とは違いますのでご注意を。
ちなみに
=ROUNDUP(-0.5) + ROUNDUP(-1.5) + ROUNDUP(-2.5) -- -6 = -1 + -2 + -3
=ROUNDDOWN(-0.5) + ROUNDDOWN(-1.5) + ROUNDDOWN(-2.5) -- -3 = 0 + -1 + -2
負の数における切り上げ・切り捨ては、絶対値での評価がなされるようです。
Microsoft Excel
Excel Online を使用してみました。
上記Googleスプレッドシートと同様に --
をコメントだと思ってお読みください。
=ROUND(1.5, 0) + ROUND(2.5, 0) + ROUND(3.5, 0) + ROUND(4.5, 0) -- 14 = 2 + 3 + 4 + 5
=ROUND(1.5, 0) * ROUND(2.5, 0) * ROUND(3.5, 0) -- 24 = 2 * 3 * 4
=ROUND(-0.5, 0) + ROUND(-1.5, 0) + ROUND(-2.5, 0) -- -6 = -1 + -2 + -3
Excelの ROUNDUP
, ROUNDDOWN
関数に関しても、試したところGoogleスプレッドシートと同様に、
負の数における切り上げ・切り捨ては絶対値を基準とした評価がされることを確認しました。
一覧表
1.5 + 2.5 + 3.5 + 4.5 | 1.5 * 2.5 * 3.5 | -0.5 + -1.5 + -2.5 | タイプ | |
---|---|---|---|---|
正答 | 12 | 13.125 | -4.5 | - |
C++ | 14 | 24 | -6 | School |
C# | 12 | 16 | -4 | Even |
Java | 14 | 24 | -3 | School |
Kotlin | 12.0 | 16.0 | -4.0 | Even |
JavaScript | 14 | 24 | -3 | School |
R言語 | 12 | 16 | -4 | Even |
Python2 | 14.0 | 24.0 | -6.0 | School |
Python3 | 12 | 16 | -4 | Even |
NumPy | 12.0 | 16.0 | -4.0 | Even |
PHP | 14 | 24 | -6 | School |
Ruby | 14 | 24 | -6 | School |
Swift | 14.0 | 24.0 | -6.0 | School |
Google Sheets | 14 | 24 | -6 | School |
Microsoft Excel | 14 | 24 | -6 | School |
小数点第1位がすべて5の場合という、非常に極端な計算例ではありますが、
こうして並べてみると近い偶数へと丸めるEvenタイプがより正答に近付けるとわかります。
感想
2.5 を四捨五入したら 3 に、
-2.5 を四捨五入したら -3 になる気持ちでいました。
しかし実際のところ、C#, Python3, Kotlin, Java, JavaScript ではそのような挙動を見せません。
特にJavaScriptは付き合いが長かったぶん、今回、負の数の四捨五入で意外なクセを見つけて驚きました。
round, ceil, floor などはお金の計算で扱うことも多いゆえ、
その挙動の違いにはよぉ〜く気を付けたほうがいいでしょう。異なる言語への移植の際はお気を付けください!