C#のMath.Roundは四捨五入じゃない?銀行丸めの原因と小数点を正しく丸める方法

はじめに

C#で数値を丸めるとき、多くの人が最初に使うのがMath.Roundです。しかし、実際に使ってみると「Math.Round(2.5)3にならない」「小数点第1位で丸めたのに期待と違う」「ExcelやJavaScriptのROUNDと結果が違う」と感じることがあります。

原因は、C#のMath.Roundが初期設定で一般的な四捨五入ではなく、銀行丸めと呼ばれる「最近接偶数への丸め」を使うためです。Microsoftのドキュメントでも、Math.Round(Double)Math.Round(Double, Int32)は中間値を最も近い偶数へ丸めると説明されています。

この記事では、C#のround処理でハマりやすい原因、Math.Roundの正しい使い方、一般的な四捨五入をする方法、doubledecimalの違い、切り上げ・切り捨てとの使い分けまで解説します。

1. C#のMath.Roundで「四捨五入されない」と感じる原因

1-1. Math.Round(2.5)が3ではなく2になる代表例

C#で次のコードを実行すると、2.53ではなく2になります。

C#
Console.WriteLine(Math.Round(2.5)); // 2

一般的な感覚では「小数部が0.5なら切り上げて3」と考えがちです。しかし、Math.Roundの既定動作では、ちょうど中間にある値は最も近い偶数に丸められます。

2.523のちょうど中間です。このとき偶数は2なので、結果は2になります。

1-2. Math.Round(1.5)とMath.Round(2.5)で結果が変わる理由

次の例を見ると、より分かりやすくなります。

C#
Console.WriteLine(Math.Round(1.5)); // 2
Console.WriteLine(Math.Round(2.5)); // 2
Console.WriteLine(Math.Round(3.5)); // 4
Console.WriteLine(Math.Round(4.5)); // 4

1.512の中間なので偶数の2へ、2.523の中間なので偶数の2へ丸められます。

つまり、C#のMath.Roundは「0.5以上なら必ず上げる」のではなく、「ちょうど中間なら偶数側に寄せる」というルールで動いています。

1-3. 小数点指定でも「0.15が0.2にならない」ことがある理由

Math.Roundで小数点以下の桁数を指定した場合も、同じように期待と違う結果になることがあります。

C#
double value = 0.15;
Console.WriteLine(Math.Round(value, 1));

このようなケースで混乱しやすい理由は主に2つあります。

1つ目は、Math.Roundの既定が銀行丸めであることです。小数点第1位に丸める場合、0.150.10.2の中間として扱われる可能性があります。その場合、末尾が偶数になる側が選ばれます。

2つ目は、doubleの浮動小数点誤差です。doublefloatは2進数で数値を表現するため、0.1のような10進小数を正確に表せないことがあります。MicrosoftのC#リファレンスでも、0.1decimalでは正確に表現できる一方、doublefloatでは正確に表現できないと説明されています。

金額や税率のように10進小数の正確さが重要な処理では、doubleではなくdecimalを使うのが基本です。

1-4. 結論:C#のRoundは初期設定では一般的な四捨五入ではない

C#のMath.Roundは、初期設定では一般的な「5以上なら切り上げ」の四捨五入ではありません。

既定の丸め方式はMidpointRounding.ToEvenです。これは銀行丸め、または最近接偶数への丸めと呼ばれます。一般的な四捨五入に近い動作をさせたい場合は、MidpointRounding.AwayFromZeroを明示する必要があります。

2. Math.Roundの基本的な使い方

2-1. Math.Roundとは何をするメソッドか

Math.Roundは、数値を指定したルールに従って丸めるメソッドです。整数へ丸めることも、小数点以下の桁数を指定して丸めることもできます。

主に次のような場面で使います。

  • 平均値を小数点以下2桁にする

  • 税込金額を整数にする

  • パーセント表示用に小数点第1位へ丸める

  • 計算結果を保存前に指定桁数へそろえる

Math.Rounddoubledecimalの両方に対応しており、丸め方式を指定できるオーバーロードも用意されています。

2-2. 整数に丸める基本構文

整数へ丸める基本構文は次のとおりです。

C#
double result = Math.Round(123.456);
Console.WriteLine(result); // 123

ただし、.5のようにちょうど中間の値では銀行丸めが適用されます。

C#
Console.WriteLine(Math.Round(10.5)); // 10
Console.WriteLine(Math.Round(11.5)); // 12

10.51011の中間なので偶数の10へ、11.51112の中間なので偶数の12へ丸められます。

2-3. 小数点以下の桁数を指定して丸める方法

小数点以下の桁数を指定するには、第2引数に桁数を渡します。

C#
double value = 123.4567;

Console.WriteLine(Math.Round(value, 0)); // 123
Console.WriteLine(Math.Round(value, 1)); // 123.5
Console.WriteLine(Math.Round(value, 2)); // 123.46
Console.WriteLine(Math.Round(value, 3)); // 123.457

第2引数のdigitsは「小数点以下を何桁残すか」を表します。

C#
Math.Round(value, 2)

この場合は、小数点以下2桁に丸めます。

2-4. doubleとdecimalで使うMath.Roundの違い

doubledecimalは、どちらも小数を扱えますが、性質が異なります。

doubleは科学技術計算や座標計算など、広い範囲の値を高速に扱いたい場合に向いています。一方で、10進小数を正確に表せないことがあります。

decimalは10進小数を扱うのに向いており、金額計算や税計算などでよく使われます。特に0.1mのような値を正確に扱いたい場合はdecimalが適しています。

C#
double d = 0.1;
decimal m = 0.1m;

Console.WriteLine(d);
Console.WriteLine(m);

金額計算では、基本的に次のようにdecimalを使います。

C#
decimal price = 1234.567m;
decimal rounded = Math.Round(price, 2, MidpointRounding.AwayFromZero);

Console.WriteLine(rounded); // 1234.57

2-5. Math.Roundの主なオーバーロード一覧

Math.Roundには複数のオーバーロードがあります。よく使うものを整理すると、次のようになります。

書き方内容
Math.Round(double value)doubleを整数に丸める
Math.Round(decimal value)decimalを整数に丸める
Math.Round(double value, int digits)doubleを指定桁数に丸める
Math.Round(decimal value, int digits)decimalを指定桁数に丸める
Math.Round(double value, MidpointRounding mode)doubleを指定方式で整数に丸める
Math.Round(decimal value, MidpointRounding mode)decimalを指定方式で整数に丸める
Math.Round(double value, int digits, MidpointRounding mode)doubleを指定桁数・指定方式で丸める
Math.Round(decimal value, int digits, MidpointRounding mode)decimalを指定桁数・指定方式で丸める

一般的な四捨五入を明示したい場合は、MidpointRoundingを指定できるオーバーロードを使いましょう。

3. 銀行丸めとは何か

3-1. 銀行丸めは「最近接偶数への丸め」

銀行丸めとは、端数がちょうど中間にあるとき、最も近い偶数に丸める方式です。英語ではBanker's rounding、またはRound half to evenと呼ばれます。

C#では、この方式がMidpointRounding.ToEvenに対応します。ToEvenは、値が2つの数のちょうど中間にある場合、最も近い偶数へ丸める方式です。

3-2. 端数が5のとき偶数側に丸められる仕組み

たとえば、整数に丸める場合を考えます。

C#
Math.Round(2.5) // 2
Math.Round(3.5) // 4
Math.Round(4.5) // 4
Math.Round(5.5) // 6

ポイントは「必ず切り上げる」のではなく、「偶数になる側へ丸める」ことです。

2.5なら候補は23です。偶数は2なので2になります。
3.5なら候補は34です。偶数は4なので4になります。

3-3. 四捨五入と銀行丸めの結果比較

一般的な四捨五入銀行丸め
1.522
2.532
3.544
4.554
5.566

このように、すべての値で違いが出るわけではありません。違いが出るのは、主に.5のようにちょうど中間にある値です。

3-4. なぜC#のMath.Roundは銀行丸めを採用しているのか

銀行丸めは、大量の数値を丸めるときに偏りを減らす目的で使われます。

一般的な四捨五入では、.5が常に大きい方向へ寄るため、データ全体として丸め結果が上振れしやすくなります。一方、銀行丸めでは中間値を偶数側に分散させるため、丸め誤差の偏りを抑えやすくなります。

そのため、統計処理や集計処理のように大量の値を扱う場面では、銀行丸めが適していることがあります。

3-5. 銀行丸めが向いているケースと向いていないケース

銀行丸めが向いているのは、次のようなケースです。

  • 大量データの平均や集計

  • 統計処理

  • 丸め誤差の偏りをなるべく減らしたい計算

  • 仕様として最近接偶数への丸めが決まっている処理

一方で、次のようなケースでは注意が必要です。

  • ユーザーが一般的な四捨五入を期待している画面表示

  • 請求金額や税額など、業務ルールで端数処理が決まっている処理

  • Excelや他システムと同じ結果に合わせる必要がある処理

  • 「2.5は必ず3にしたい」という仕様がある処理

このような場合は、MidpointRounding.AwayFromZeroMath.FloorMath.Ceilingなど、目的に合った丸め方法を明示することが重要です。

4. C#で一般的な四捨五入をする方法

4-1. MidpointRounding.AwayFromZeroを指定する

C#で一般的な四捨五入に近い丸めを行いたい場合は、MidpointRounding.AwayFromZeroを指定します。

C#
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero)); // 3
Console.WriteLine(Math.Round(3.5, MidpointRounding.AwayFromZero)); // 4
Console.WriteLine(Math.Round(4.5, MidpointRounding.AwayFromZero)); // 5

AwayFromZeroは、ちょうど中間の値を0から遠ざかる方向へ丸めます。正の数では.5が上がり、負の数では-2.5-3になります。

4-2. 小数点第1位・第2位・第n位で四捨五入するコード例

小数点以下の桁数を指定して一般的な四捨五入をしたい場合は、次のように書きます。

C#
decimal value = 123.4567m;

Console.WriteLine(Math.Round(value, 0, MidpointRounding.AwayFromZero)); // 123
Console.WriteLine(Math.Round(value, 1, MidpointRounding.AwayFromZero)); // 123.5
Console.WriteLine(Math.Round(value, 2, MidpointRounding.AwayFromZero)); // 123.46
Console.WriteLine(Math.Round(value, 3, MidpointRounding.AwayFromZero)); // 123.457

小数点第1位まで残すなら1、小数点第2位まで残すなら2を指定します。

C#
decimal rounded = Math.Round(value, 2, MidpointRounding.AwayFromZero);

「小数点第2位で四捨五入」という表現は、人によって「第2位を残す」場合と「第2位を見て第1位へ丸める」場合で解釈が分かれることがあります。C#のdigitsは「残す小数桁数」と覚えると安全です。

4-3. 正の数を四捨五入する場合の実装例

正の数だけを扱う場合、AwayFromZeroは多くの人が想像する四捨五入に近い結果になります。

C#
decimal score = 89.5m;
decimal roundedScore = Math.Round(score, 0, MidpointRounding.AwayFromZero);

Console.WriteLine(roundedScore); // 90

割合やスコアなど、正の数だけを表示用に丸めるなら、この書き方で分かりやすく実装できます。

4-4. 負の数を四捨五入する場合の注意点

負の数を扱う場合は、「四捨五入」という言葉だけでは仕様があいまいになることがあります。

C#
Console.WriteLine(Math.Round(-2.5, MidpointRounding.AwayFromZero)); // -3
Console.WriteLine(Math.Round(-2.5, MidpointRounding.ToEven)); // -2

AwayFromZeroは0から遠ざかる方向へ丸めるため、-2.5-3になります。

一方で、業務仕様によっては「負数でも小数部を見て絶対値を小さくしたい」「常に正の無限大方向へ丸めたい」「常に0方向へ丸めたい」というケースもあります。その場合は、AwayFromZeroではなくToZeroToPositiveInfinityToNegativeInfinityなどを検討します。

4-5. 金額計算で使うならdecimal型を優先する

金額計算でMath.Roundを使うなら、基本的にはdecimal型を優先しましょう。

C#
decimal unitPrice = 19.99m;
decimal quantity = 3m;
decimal total = unitPrice * quantity;

decimal rounded = Math.Round(total, 2, MidpointRounding.AwayFromZero);

Console.WriteLine(rounded);

doubleは2進浮動小数点であり、10進小数を正確に表現できない場合があります。金額や税額のように10進小数の正確さが重要な場面では、decimalを使うことで予期しない丸め誤差を避けやすくなります。

5. MidpointRoundingの種類と使い分け

5-1. ToEven:銀行丸め

MidpointRounding.ToEvenは、C#のMath.Roundで既定として使われる丸め方式です。

C#
Console.WriteLine(Math.Round(2.5, MidpointRounding.ToEven)); // 2
Console.WriteLine(Math.Round(3.5, MidpointRounding.ToEven)); // 4

中間値は偶数側に丸められます。大量のデータを扱う集計処理など、丸めの偏りを抑えたい場合に向いています。

5-2. AwayFromZero:一般的な四捨五入に近い丸め

MidpointRounding.AwayFromZeroは、中間値を0から遠ざかる方向へ丸めます。

C#
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));  // 3
Console.WriteLine(Math.Round(-2.5, MidpointRounding.AwayFromZero)); // -3

正の数では、一般的な「0.5以上なら切り上げ」に近い結果になります。

5-3. ToZero:0方向への丸め

MidpointRounding.ToZeroは、0に近づく方向へ丸めます。

C#
Console.WriteLine(Math.Round(2.9, 0, MidpointRounding.ToZero));   // 2
Console.WriteLine(Math.Round(-2.9, 0, MidpointRounding.ToZero)); // -2

小数部を削るような動きに近く、正の数では切り捨て、負の数では切り上げのように見えます。

5-4. ToNegativeInfinity:負の無限大方向への丸め

MidpointRounding.ToNegativeInfinityは、負の無限大方向へ丸めます。

C#
Console.WriteLine(Math.Round(2.1, 0, MidpointRounding.ToNegativeInfinity));  // 2
Console.WriteLine(Math.Round(-2.1, 0, MidpointRounding.ToNegativeInfinity)); // -3

Math.Floorに近い考え方です。常に小さい方向へ寄せたい場合に使います。

5-5. ToPositiveInfinity:正の無限大方向への丸め

MidpointRounding.ToPositiveInfinityは、正の無限大方向へ丸めます。

C#
Console.WriteLine(Math.Round(2.1, 0, MidpointRounding.ToPositiveInfinity));  // 3
Console.WriteLine(Math.Round(-2.1, 0, MidpointRounding.ToPositiveInfinity)); // -2

Math.Ceilingに近い考え方です。常に大きい方向へ寄せたい場合に使います。

5-6. 用途別に選ぶべきMidpointRounding

MidpointRoundingの主な値は、ToEvenAwayFromZeroToZeroToNegativeInfinityToPositiveInfinityです。Microsoftのドキュメントでは、これらは最近接丸めと方向付き丸めの戦略として整理されています。

用途選び方
既定のC#らしい丸めでよいToEven
正の数を一般的な四捨五入にしたいAwayFromZero
.5を必ず0から遠ざけたいAwayFromZero
小数部を削る方向にしたいToZero
常に小さい方向へ寄せたいToNegativeInfinity
常に大きい方向へ寄せたいToPositiveInfinity
金額・税額の端数処理業務ルールに合わせて明示する

大切なのは、Math.Roundを何となく使うのではなく、仕様に合う丸め方式を明示することです。

6. Math.Roundで小数点を正しく丸める実践パターン

6-1. 小数点以下2桁に丸める

小数点以下2桁に丸める場合は、digits2を指定します。

C#
decimal value = 12.345m;

decimal result = Math.Round(value, 2, MidpointRounding.AwayFromZero);

Console.WriteLine(result); // 12.35

一般的な四捨五入を期待するなら、MidpointRounding.AwayFromZeroを付けておくと意図が明確です。

6-2. 小数点以下を0桁にして整数へ丸める

整数に丸めたい場合は、digits0を指定するか、digitsなしのオーバーロードを使います。

C#
decimal value = 12.5m;

Console.WriteLine(Math.Round(value, 0, MidpointRounding.AwayFromZero)); // 13

ただし、Math.Round(value)のように丸め方式を省略すると、既定のToEvenが使われます。

C#
Console.WriteLine(Math.Round(12.5m)); // 12

6-3. 金額を税込・税抜計算で丸める

金額計算では、decimalを使い、丸め方式を明示しましょう。

C#
decimal price = 1980m;
decimal taxRate = 0.10m;

decimal tax = Math.Round(price * taxRate, 0, MidpointRounding.AwayFromZero);
decimal total = price + tax;

Console.WriteLine(tax); // 198
Console.WriteLine(total); // 2178

ただし、税計算の端数処理は「四捨五入」「切り捨て」「切り上げ」など、業務や契約、システム仕様によって異なります。実装前に、どのタイミングで、どの単位で、どの方式で丸めるのかを明確にすることが重要です。

6-4. パーセント表示用に丸める

割合をパーセント表示にする場合も、計算値と表示値を分けて考えると安全です。

C#
decimal rate = 0.12345m;

decimal percent = rate * 100;
decimal displayPercent = Math.Round(percent, 1, MidpointRounding.AwayFromZero);

Console.WriteLine(displayPercent); // 12.3

画面に12.3%と表示したいだけであれば、ToStringの書式指定を使う方法もあります。

C#
decimal rate = 0.12345m;

Console.WriteLine(rate.ToString("P1")); // 12.3 %

6-5. 表示だけ丸めたい場合と値そのものを丸めたい場合の違い

Math.Roundは、値そのものを丸めます。一方、ToString("F2")などの書式指定は、表示上の桁数を整えるために使います。

C#
decimal value = 12.345m;

decimal rounded = Math.Round(value, 2, MidpointRounding.AwayFromZero);
string text = value.ToString("F2");

Console.WriteLine(rounded); // 12.35
Console.WriteLine(text); // 12.35

結果が同じように見えても、目的は異なります。

保存する値や次の計算に使う値を丸めたいならMath.Round。画面や帳票に表示する見た目だけを整えたいならToStringを使います。

7. Math.Roundでハマりやすい注意点

7-1. doubleの浮動小数点誤差によるズレ

doubleは高速で広い範囲の数値を扱えますが、10進小数を正確に表現できないことがあります。

C#
double value = 2.675;
Console.WriteLine(Math.Round(value, 2));

このようなコードで、期待と異なる丸め結果になることがあります。原因は、2.675という10進小数がdouble内部で完全にその値として保持されているとは限らないためです。

小数点以下の正確さが重要な処理では、decimalを検討しましょう。

C#
decimal value = 2.675m;
Console.WriteLine(Math.Round(value, 2, MidpointRounding.AwayFromZero)); // 2.68

7-2. 0.1や0.15などが正確に表現できない問題

0.10.150.675のような値は、人間にとっては単純な10進小数です。しかし、doublefloatでは2進数で表現するため、正確に保持できない場合があります。

そのため、Math.Roundの問題に見えて、実際には丸め前の値がすでに期待値からわずかにズレていることがあります。C#の数値型に関する公式説明でも、doublefloatでは10進小数を正確に表せないことがあり、10進データでは予期しない丸め誤差が起こる可能性が示されています。

7-3. 丸め処理を何度も行うと誤差が積み重なる

計算の途中で何度も丸めると、誤差が積み重なりやすくなります。

C#
decimal a = Math.Round(10.005m, 2, MidpointRounding.AwayFromZero);
decimal b = Math.Round(a * 3, 2, MidpointRounding.AwayFromZero);
decimal c = Math.Round(b / 7, 2, MidpointRounding.AwayFromZero);

このように途中で何度も丸めると、最終結果が本来の計算結果から離れることがあります。

7-4. 計算途中では丸めず最後に丸めるべき理由

基本的には、計算途中ではなるべく丸めず、最後に必要な桁数へ丸めるのが安全です。

C#
decimal price = 123.456m;
decimal quantity = 7m;

// 途中では丸めない
decimal subtotal = price * quantity;

// 最後に丸める
decimal total = Math.Round(subtotal, 2, MidpointRounding.AwayFromZero);

ただし、税計算や請求計算では「明細ごとに丸める」「合計後に丸める」などの業務ルールが決まっている場合があります。その場合は、数学的な正しさよりも仕様を優先します。

7-5. ToStringの表示丸めとMath.Roundの計算丸めを混同しない

ToString("F2")は、値を文字列として表示するときに小数点以下2桁に整えます。

C#
decimal value = 1.2m;

Console.WriteLine(value.ToString("F2")); // 1.20

一方、Math.Round(value, 2)は値そのものを小数点以下2桁に丸めます。

C#
decimal rounded = Math.Round(value, 2);
Console.WriteLine(rounded); // 1.2

Math.Roundしても1.20のように末尾の0が必ず表示されるわけではありません。末尾の0を表示したい場合は、ToString("F2")などの書式指定を使います。

8. 切り上げ・切り捨てとの違い

8-1. Math.Ceilingで切り上げる方法

Math.Ceilingは、指定した数値以上の最小の整数へ丸めます。つまり、正の数では切り上げになります。

C#
Console.WriteLine(Math.Ceiling(1.1));  // 2
Console.WriteLine(Math.Ceiling(1.9)); // 2
Console.WriteLine(Math.Ceiling(-1.1)); // -1

負の数では、0に近づく方向になる点に注意が必要です。

8-2. Math.Floorで切り捨てる方法

Math.Floorは、指定した数値以下の最大の整数へ丸めます。正の数では切り捨てになります。

C#
Console.WriteLine(Math.Floor(1.1));  // 1
Console.WriteLine(Math.Floor(1.9)); // 1
Console.WriteLine(Math.Floor(-1.1)); // -2

負の数では、より小さい方向、つまり負の無限大方向へ丸められます。

8-3. Math.Truncateで小数部を削る方法

Math.Truncateは、小数部を削って0方向へ丸めます。Microsoftのドキュメントでも、Truncateは0に向かって最も近い整数へ丸めると説明されています。

C#
Console.WriteLine(Math.Truncate(1.9));  // 1
Console.WriteLine(Math.Truncate(-1.9)); // -1

正の数ではFloorと同じように見えますが、負の数では結果が異なります。

8-4. Round・Ceiling・Floor・Truncateの違い一覧

メソッド動作例:1.5例:-1.5
Math.Round最も近い値へ丸める既定では2既定では-2
Math.Ceiling正の無限大方向へ丸める2-1
Math.Floor負の無限大方向へ丸める1-2
Math.Truncate0方向へ丸める1-1

Roundは「近い方へ丸める」、Ceilingは「上方向」、Floorは「下方向」、Truncateは「小数部を削る」と考えると整理しやすくなります。

8-5. 目的別に使うべき丸めメソッド

目的別に選ぶなら、次のようになります。

目的使うメソッド
最も近い整数・小数桁へ丸めたいMath.Round
一般的な四捨五入に近づけたいMath.Round(..., MidpointRounding.AwayFromZero)
常に大きい整数へ寄せたいMath.Ceiling
常に小さい整数へ寄せたいMath.Floor
小数部を単純に削りたいMath.Truncate
金額を指定ルールで丸めたいdecimal + 明示的な丸め方式

特に金額や業務ロジックでは、「何となくRound」ではなく、端数処理の仕様に合わせてメソッドを選ぶことが重要です。

9. C#のRoundに関するよくある質問

9-1. Math.Roundは四捨五入ではないのか

C#のMath.Roundは、既定では一般的な四捨五入ではありません。既定ではMidpointRounding.ToEven、つまり銀行丸めが使われます。

そのため、Math.Round(2.5)3ではなく2になります。

C#
Console.WriteLine(Math.Round(2.5)); // 2

一般的な四捨五入に近い動作にしたい場合は、MidpointRounding.AwayFromZeroを指定します。

9-2. C#で2.5を必ず3にするにはどうするのか

2.5を必ず3にしたい場合は、次のように書きます。

C#
double result = Math.Round(2.5, MidpointRounding.AwayFromZero);

Console.WriteLine(result); // 3

小数点以下の桁数も指定する場合は、次のようにします。

C#
decimal result = Math.Round(2.5m, 0, MidpointRounding.AwayFromZero);

Console.WriteLine(result); // 3

9-3. 小数点第2位で四捨五入するにはどう書くのか

「小数点以下2桁を残す」という意味なら、digits2を指定します。

C#
decimal value = 1.235m;

decimal result = Math.Round(value, 2, MidpointRounding.AwayFromZero);

Console.WriteLine(result); // 1.24

Math.Round(value, 2)だけでも丸められますが、その場合は既定の銀行丸めになります。一般的な四捨五入を意図するなら、MidpointRounding.AwayFromZeroを明示しましょう。

9-4. decimalとdoubleはどちらを使うべきか

金額、税額、請求、会計、数量単価など、10進小数の正確さが重要な場合はdecimalを使うべきです。

一方、科学技術計算、物理計算、座標、統計、ゲームの座標処理など、広い範囲の値や処理速度を重視する場合はdoubleが使われることもあります。

C#のround処理で「0.1や0.15の結果がおかしい」と感じた場合は、まずdoubleによる浮動小数点誤差を疑いましょう。

9-5. 銀行丸めを使っても問題ないケースはあるのか

銀行丸めを使っても問題ないケースはあります。

たとえば、大量の数値を集計する処理や、丸め誤差の偏りを抑えたい統計処理では、ToEvenが適している場合があります。また、システム仕様として銀行丸めが定められているなら、むしろToEvenを使うべきです。

問題になるのは、利用者や仕様が一般的な四捨五入を期待しているのに、暗黙的にMath.Roundの既定動作を使ってしまうケースです。

9-6. JavaScriptやExcelのROUNDと結果が違うのはなぜか

言語やツールによって、丸めのルールが異なるためです。

JavaScriptのMath.roundは、.5の中間値を正の無限大方向へ丸める仕様です。そのため、Math.round(-2.5)-2になります。MDNでも、小数部分がちょうど.5の場合は正の無限大方向へ丸められると説明されています。

ExcelのROUND関数も、C#のMath.Round既定動作とは結果が異なる場合があります。MicrosoftのExcelサポートでは、ROUNDは指定した桁数に数値を丸める関数として説明され、常に切り上げたい場合はROUNDUP、常に切り下げたい場合はROUNDDOWNを使うと案内されています。

つまり、C#、JavaScript、Excelで同じ「round」という名前でも、.5の扱いや負数の扱いが同じとは限りません。他システムと結果を合わせる場合は、丸め方式を必ず確認しましょう。

まとめ

C#のMath.Roundで「四捨五入されない」と感じる最大の原因は、既定の丸め方式が一般的な四捨五入ではなく、銀行丸めであることです。

Math.Round(2.5)3ではなく2になるのはバグではありません。2.523の中間にあり、既定のMidpointRounding.ToEvenによって偶数の2へ丸められるためです。

一般的な四捨五入に近い動作をさせたい場合は、次のようにMidpointRounding.AwayFromZeroを指定します。

C#
decimal result = Math.Round(2.5m, 0, MidpointRounding.AwayFromZero);
Console.WriteLine(result); // 3

また、金額計算や税計算ではdoubleではなくdecimalを優先し、丸め方式を明示することが重要です。

C#でround処理を書くときは、次の3点を意識しましょう。

ポイント内容
既定のMath.Round銀行丸めになる
一般的な四捨五入MidpointRounding.AwayFromZeroを指定する
金額計算decimalを使い、業務ルールに合わせて丸める

Math.Roundは便利なメソッドですが、丸め方式を理解せずに使うと、思わぬ計算ミスにつながります。特に.5の扱い、負数の扱い、doubleの誤差、表示丸めと計算丸めの違いを押さえておくと、C#の小数点処理でハマりにくくなります。