できない.dev

小数を指定桁で丸めるには

小数を小数点以下の指定した桁に丸める基本形を各言語で示す。
ちょうど真ん中の値を偶数の側へ寄せるか 0 から遠い側へ寄せるかが言語ごとに違う点と、1.005 のように 2 進数で正確に表せない値が見た目どおりに四捨五入されない点までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

from decimal import Decimal, ROUND_HALF_UP
 
print(round(3.14159, 2))       # 3.14
print(round(2.5), round(3.5))  # 2 4(偶数の側へ丸める)
print(round(2.675, 2))         # 2.67(2.675 は内部で 2.67499… になっている)
 
# 四捨五入したいときは文字列から Decimal を作り、丸め方を指定する
d = Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(d)                       # 2.68
 
# 表示だけ丸めるなら書式指定を使う(値そのものは変わらない)
print(f"{3.14159:.2f}")        # 3.14

round は第 2 引数で桁数を指定して丸めるが、ちょうど真ん中の値は偶数の側へ寄せるため round(2.5) は 2 になる。
さらに 2.675 は 2 進数では 2.67499… として保持されているので、round(2.675, 2) は 2.67 を返す。
金額のように見た目どおりの四捨五入が必要な値は、文字列から作った Decimal を ROUND_HALF_UP 付きの quantize で丸める。

JavaScript 実行確認済み

const x = 3.14159;
console.log(Math.round(x * 100) / 100);         // 3.14
console.log(x.toFixed(2), typeof x.toFixed(2)); // 3.14 string
console.log(Math.round(2.5), Math.round(-2.5)); // 3 -2(+∞ の側へ丸める)
 
// 2 進数で正確に表せない値は、見た目どおりに丸まらないことがある
console.log((1.005).toFixed(2));            // 1.00
console.log(Math.round(1.005 * 100) / 100); // 1
 
// Intl.NumberFormat で書式化すると 1.01 になる(結果は文字列)
const fmt = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
console.log(fmt.format(1.005));             // 1.01

Math.round は整数にしか丸めないので、100 倍して丸めてから 100 で割り戻す。
ちょうど 0.5 のときは +∞ の側へ寄せるため、Math.round(-2.5) は -3 ではなく -2 になる。
toFixed は丸めた結果を文字列で返し、内部で 1.00499… になっている 1.005 は 1.00 になる。
Node.js で実行すると Intl.NumberFormat は 1.005 を 1.01 と書式化するので、表示だけが目的ならこちらを使う。

TypeScript 実行確認済み

// 丸めた結果を number のまま使うなら Math.round で桁をずらして丸める
function roundTo(value: number, digits: number): number {
  const factor: number = 10 ** digits;
  return Math.round(value * factor) / factor;
}
 
console.log(roundTo(3.14159, 2)); // 3.14
console.log(roundTo(1234.5, 0));  // 1235
console.log(roundTo(-1.5, 0));    // -1
 
// toFixed の戻り値は string なので、number を受ける引数には渡せない
const label: string = (3.14159).toFixed(2);
// roundTo(label, 1); // TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
console.log(roundTo(Number(label), 1)); // 3.1

roundTo は 10 の digits 乗を掛けて Math.round で丸め、同じ数で割り戻すので、戻り値を number のまま次の計算に使える。
内部で 1.00499… になっている 1.005 が roundTo(1.005, 2) でも 1 になる点は JavaScript と同じである。
toFixed の戻り値は string 型なので、number を受け取る引数にそのまま渡すと TS2345 でコンパイルが止まり、Number で数値に戻してから渡す必要がある。

うまくいかない時: TypeScript で「Argument of type ... is not assignable」が解消できない

Go 実行確認済み

package main
 
import (
	"fmt"
	"math"
)
 
func main() {
	x := 3.14159
	fmt.Println(math.Round(x*100) / 100) // 3.14
	fmt.Printf("%.2f\n", x)              // 3.14
 
	// math.Round は 0 から遠い側へ、math.RoundToEven は偶数の側へ丸める
	fmt.Println(math.Round(2.5), math.Round(-2.5))            // 3 -3
	fmt.Println(math.RoundToEven(2.5), math.RoundToEven(3.5)) // 2 4
 
	// 1.005 は内部で 1.00499… なので、100 倍しても 100.5 にならない
	y := 1.005
	fmt.Println(y*100, math.Round(y*100)/100) // 100.49999999999999 1
	fmt.Printf("%.2f\n", y)                   // 1.00
}

math.Round は 0 から遠い側へ、math.RoundToEven は偶数の側へ整数に丸める関数で、桁数を指定する引数は無いので 100 倍して丸めてから割り戻す。
1.005 を 100 倍すると 100.49999999999999 になるため、この方法では 1 に丸まる。
表示用の文字列が欲しいだけなら fmt.Printf の %.2f を使えばよいが、こちらも内部の値どおりに丸めるので 1.005 は 1.00 になる。

Rust 実行確認済み

fn main() {
    let x: f64 = 3.14159;
    println!("{}", (x * 100.0).round() / 100.0); // 3.14
    println!("{:.2}", x);                        // 3.14
 
    // round は 0 から遠い側へ、round_ties_even は偶数の側へ丸める(Rust 1.77 以降)
    println!("{} {}", 2.5_f64.round(), (-2.5_f64).round()); // 3 -3
    println!("{} {}", 2.5_f64.round_ties_even(), 3.5_f64.round_ties_even()); // 2 4
 
    // 1.005 は内部で 1.00499… なので、どちらの方法でも切り捨て側になる
    let y: f64 = 1.005;
    println!("{}", (y * 100.0).round() / 100.0); // 1
    println!("{:.2}", y);                        // 1.00
}

f64::round は 0 から遠い側へ、round_ties_even は偶数の側へ整数に丸める。
桁数を指定するメソッドは無いので、100 倍して丸めてから割り戻す。
format! や println! の {:.2} は丸めた文字列を作るが、1.005 は内部の値が 1.00499… なので 1.00 になる。
{:.0} で 2.5 を書式化すると 2 になり、round とは寄せる側が違う点にも注意する。

Java 実行確認済み

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Locale;
 
public class RoundNumber {
    public static void main(String[] args) {
        double x = 3.14159;
        System.out.println(Math.round(x * 100) / 100.0); // 3.14
        // Math.round は +∞ の側へ丸める
        System.out.println(Math.round(2.5) + " " + Math.round(-2.5)); // 3 -2
 
        // new BigDecimal(double) は 2 進数の誤差ごと引き継ぐ
        System.out.println(new BigDecimal(1.005).setScale(2, RoundingMode.HALF_UP)); // 1.00
        // BigDecimal.valueOf は見た目どおりの 1.005 から作る
        System.out.println(BigDecimal.valueOf(1.005).setScale(2, RoundingMode.HALF_UP)); // 1.01
        // 偶数の側へ丸めたいときは HALF_EVEN を指定する
        System.out.println(new BigDecimal("2.5").setScale(0, RoundingMode.HALF_EVEN)); // 2
 
        // 表示だけ丸めるなら String.format。ロケールを渡さないと小数点が , になる環境がある
        System.out.println(String.format(Locale.ROOT, "%.2f", 1.005)); // 1.01
    }
}

Math.round は long を返すので、100 倍して丸めてから 100.0 で割り戻すと小数に戻る。
ちょうど 0.5 のときは +∞ の側へ寄せるため、Math.round(-2.5) は -2 になる。
桁数と丸め方を指定したいときは BigDecimal の setScale を使うが、new BigDecimal(1.005) は 2 進数の誤差を含んだ 1.00499… から作られるので、BigDecimal.valueOf か文字列から作る。
String.format はロケールを渡さないと実行環境によって小数点が , になるので、Locale を明示する。

C# 実行確認済み

Console.WriteLine(Math.Round(3.14159, 2)); // 3.14
 
// 既定は偶数の側へ丸める(銀行家の丸め)
Console.WriteLine(Math.Round(2.5)); // 2
Console.WriteLine(Math.Round(3.5)); // 4
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero)); // 3
 
// double の 1.005 は 1.00499… なので、四捨五入を指定しても 1 になる
Console.WriteLine(Math.Round(1.005, 2, MidpointRounding.AwayFromZero)); // 1
// 金額のように 10 進で正確に扱いたい値は decimal を使う
Console.WriteLine(Math.Round(1.005m, 2, MidpointRounding.AwayFromZero)); // 1.01
Console.WriteLine(Math.Round(1.005m, 2)); // 1.00

Math.Round は第 2 引数で桁数を指定でき、既定ではちょうど真ん中の値を偶数の側へ寄せるため Math.Round(2.5) は 2 になる。
四捨五入にしたいときは MidpointRounding.AwayFromZero を渡すが、double の 1.005 は内部で 1.00499… なのでそれでも 1 になる。
金額のように 10 進で正確に丸めたい値は decimal(1.005m)で持つと、指定どおり 1.01 になる。
Console.WriteLine は実行環境のカルチャで書式化するので、de-DE では 3.14 が 3,14 と表示される。

つまずき

ちょうど真ん中の値をどちらへ寄せるかが言語で割れる。
Python の round と C# の Math.Round は既定で偶数の側へ寄せ、2.5 は 2、3.5 は 4 になる。
Go の math.Round と Rust の round は 0 から遠い側へ寄せ、-2.5 は -3 になる。
JavaScript と Java の Math.round は +∞ の側へ寄せるので、-2.5 は -2 になる。
同じ言語の中でも書式化の関数は別の規則で丸めることがあり、JavaScript の (-2.5).toFixed(0) は -3、Rust の {:.0} で書式化した 2.5 は 2 になる。

2 進数で表せない小数は見た目どおりに丸まらない

1.005 や 2.675 のような小数は 2 進数の浮動小数点数では正確に表せず、1.005 は 1.00499999999999989…、2.675 は 2.67499999999999982… として保持される。
そのため 1.005 を小数第 2 位に丸めると、多くの言語で 1.01 ではなく 1.00 になる。
100 倍してから丸める方法では掛け算の結果によって答えが割れ、1.005 × 100 は 100.49999999999999 になって切り捨て側へ落ちるが、2.675 × 100 はちょうど 267.5 になる。
実際に Go の math.Round(y*100)/100 と C# の Math.Round(2.675, 2) は 2.68 を返し、Python の round(2.675, 2) は 2.67 を返した。

金額は 10 進の型か整数で扱う

税込価格や按分のように 1 円単位の四捨五入が結果を左右する計算では、浮動小数点数のまま丸めず、Python の Decimal、Java の BigDecimal、C# の decimal のような 10 進の型を使う。
10 進の型でも double から作ると誤差ごと引き継ぐので、Python では文字列から、Java では BigDecimal.valueOf か文字列から作る。
標準で 10 進の型を持たない言語では、金額を円や銭のような最小単位の整数で持つと、丸めの問題そのものを避けられる。

この記事は役立ちましたか?