Kotlinの四捨五入について

こんにちは。株式会社 Interfamilia の Waka です。

気づけばもう12月。2022年も残り僅か。冬らしい気候になり、肌寒い日も増えてきましたね。

個人的な話ですが、年末はとあるイベントに参加予定で、忙しくなりそうです。
年の瀬まで全力で頑張りたいと思います💪

今回の記事では、自分が日々の開発業務で学んだ、Kotlinに関するTIPSをご紹介したいと思います!


目次

Kotlin の round() は四捨五入ではなく「銀行丸め」!

プログラミング言語には、四捨五入を行うための様々なクラス・関数が用意されています。
例えば Javaでは、Math.round() を使うと、小数第一位の四捨五入が可能です。

class Main {
    public static void main(String[ ] args) {
        double test1 = 123.4;
        System.out.println(Math.round(test1)); // 123

        double test2 = 123.5;
        System.out.println(Math.round(test2)); // 124
    }
}

しかし、Javaから派生した言語のKotlinでは、round() が四捨五入ではなく「銀行丸め」 なので注意が必要です。
公式ドキュメントでも、次のように言及されています。

Rounds the given value x towards the closest integer with ties rounded towards even integer.

【日本語訳】 指定された値xを最も近い整数に丸め、同順位は偶数に丸めます。

引用:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.math/round.html


銀行丸めとは?

「銀行丸め (Bankers' rounding)」 は、小数の端数処理の一種です。
「最近接丸め」「JIS丸め」などとも呼ばれます。

※ 端数処理を行うことを「丸める」という風に表現することがあり、銀行家が好んで使ったことから、このように呼ばれているようです。

四捨五入と同じく、小数点以下の端数を整数に置き換える処理なのですが、端数の切り捨て・切り上げ方法が、四捨五入とは微妙に違います。

次の例を見てください。「四捨五入」「銀行丸め」の両方で、小数第一位の端数処理を行っています。

【四捨五入】

11.4 -> 11
11.5 -> 12
11.6 -> 12

12.4 -> 12
12.5 -> 13
12.6 -> 13
【銀行丸め】

11.4 -> 11
11.5 -> 12
11.6 -> 12

12.4 -> 12
12.5 -> 12
12.6 -> 13

四捨五入の場合は、小数第一位が 4以下であれば切り捨て、5以上であれば切り上げです。
しかし、銀行丸めの場合は以下のように処理します。

  • 丸めた先の桁が「奇数」の場合
    • 4以下であれば切り捨て、5以上であれば切り上げ (四捨五入と同じ)
  • 丸めた先の桁が「偶数」の場合
    • 5以下であれば切り捨て、6以上であれば切り上げ

基本は四捨五入と同じなのですが、小数第一位が 5 の場合が特殊な処理になると思えば良いです。

例えば 11.5 の場合、一の位が 1 で「奇数」なので、小数第一位が切り上げられ 12 となっています。
四捨五入と同じですね。

11.5 -> 12

一方 12.5 の場合は一の位が 2 で「偶数」です。
この場合は小数第一位が切り捨てられ、四捨五入とは異なり 12 のままとなっています。
「最近接丸め」という別名のように、近接した偶数に寄せられるイメージです。

12.5 -> 12

何故このような端数処理が存在するのかというと、統計を取る時に数値の誤差が小さくなるからだそうです。 シンプルな具体例を以下に挙げてみます。

【四捨五入】

48.5% -> 49%
51.5% -> 52%

合計:101%
【銀行丸め】

48.5% -> 48%
51.5% -> 52%

合計:100%

合計すると100%になる統計値があり、それぞれの値に対して「四捨五入」「銀行丸め」を適用した…という例です。

「四捨五入」を適用した場合、各統計値が大きく変わり、合計すると100%を超えてしまっています。
一方「銀行丸め」を適用した場合は、そこまで大きな誤差が出ず、合計しても100%のままとなっています。

こんな感じで、取り扱う数値が大きいほど、統計値がより多くなるほど、銀行丸めを使った方が誤差を抑えられる…ということらしいです。
自分は専門家ではないため、そこまで詳しくはないのですが「なるほどな~」という感想を抱きました。


Kotlinで四捨五入したい時は roundToInt() を使う!

Kotlinで四捨五入したい場合は round() ではなく、roundToInt() を使うのが良さそうです。
以下、Kotlin公式ドキュメントの引用となります。

Rounds this Double value to the nearest integer and converts the result to Int. Ties are rounded towards positive infinity.

【日本語訳】 指定された値xを最も近い整数に丸め、結果を Int に変換します。 同順位は、正の無限大に向かって丸められます。

引用:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.math/round-to-int.html


検証

実際に Kotlinの round()roundToInt() の実行結果を比較してみました。

バージョン・環境

kotlin: 1.5.31

round() の場合

検証用コード

Double値に対して round() を適用 → toInt()で計算結果をInt値に変換して出力します。

import kotlin.math.round

fun main() {
    val num1: Double = 123.4
    val num2: Double = 123.5
    val num3: Double = 123.6
    val num4: Double = 124.4
    val num5: Double = 124.5
    val num6: Double = 124.6
    println("$num1: " + round(num1).toInt())
    println("$num2: " + round(num2).toInt())
    println("$num3: " + round(num3).toInt())
    println("$num4: " + round(num4).toInt())
    println("$num5: " + round(num5).toInt())
    println("$num6: " + round(num6).toInt())
}

実行結果

round() の実行結果は以下の通りです。
最初に述べたように 「銀行丸め」 が適用されています。

123.4: 123
123.5: 124
123.6: 124
124.4: 124
124.5: 124
124.6: 125

round() が銀行丸めになる理由

Kotlinのround() は、内部的にはJavaの Math.rint() を呼び出しているようです。
rint() は「銀行丸め」のメソッドです。

IntelliJのサジェストを見ると、確かに Math.rint() = kotlin.math.round() で代替できることが分かります。
ktファイル上でMath.rint()を呼び出してみると、次のサジェストが出てきます。

round() は、内部的にはJavaの Math.rint() が呼ばれている
round() は、内部的にはJavaの Math.rint() が呼ばれている

roundToInt() の場合

検証用コード

Double値に対して roundToInt() を適用して、返ってくるInt値をそのまま出力しています。

round() とは異なり、roundToInt() は計算結果がInt値で返されます。

import kotlin.math.roundToInt

fun main() {
    val num1: Double = 123.4
    val num2: Double = 123.5
    val num3: Double = 123.6
    val num4: Double = 124.4
    val num5: Double = 124.5
    val num6: Double = 124.6
    println("$num1: " + num1.roundToInt())
    println("$num2: " + num2.roundToInt())
    println("$num3: " + num3.roundToInt())
    println("$num4: " + num4.roundToInt())
    println("$num5: " + num5.roundToInt())
    println("$num6: " + num6.roundToInt())
}

実行結果

roundToInt() の実行結果は以下の通りです。
想定通り 「四捨五入」 が適用されていました。

123.4: 123
123.5: 124
123.6: 124
124.4: 124
124.5: 125
124.6: 125

roundToInt() が四捨五入になる理由

roundToInt() は、内部的にはJavaの Math.round()を呼び出しているようです。
このため、計算結果が四捨五入された値となっています。

IntelliJのサジェストを見ると、確かに Math.round() = kotlin.math.roundToInt() で代替できることが分かります。
ktファイル上でMath.round()を呼び出してみると、次のサジェストが出てきます。

roundToInt() は、内部的にはJavaの Math.round() が呼ばれている
roundToInt() は、内部的にはJavaの Math.round() が呼ばれている

最後に

以上、Kotlinの四捨五入に関する調査記事でした。

Kotlinの round() は「銀行丸め」で「四捨五入」では無い!…意外な落とし穴だなぁと感じました。
Javaと同じ感覚で使ってしまうと、中には全然違う結果になるメソッドがあるので、Kotlinを使うときは注意したいですね…

それではまた次回!


参考リンク


会社を一緒に盛り上げてくれる仲間を募集しています!

応募フォームには、外部サービスengageを利用しています。
ご応募に際して、何かわからないことなどございましたら こちら からお気軽にお問い合わせください。