- 技術 💻
金額計算の端数処理設計:消費税の丸め仕様とDECIMAL型の選び方
金額計算の端数処理設計とは、1円未満の端数を「どの項目で・どの時点で・どの方式で・どの単位に」丸めるかを決め、仕様書に書くことです。 決めずに実装すると、画面と帳票、アプリケーションとデータベースで1円違う金額が出てきます。
この記事では、明細ごとに丸めた場合と税率ごとに丸めた場合で合計が1円ずれる例を実際に計算し、金額を入れる型の選び方、丸め方式の違い、言語ごとの丸め関数の動きを、すべて実行結果つきで示します。そのまま使える端数処理仕様のテンプレートも載せています。
なお、制度に関する記述は国税庁の公開ページで確認できた範囲の紹介であり、税務上の助言ではありません。自社の取り扱いは経理担当や税理士の確認に従ってください。
この記事の結論
- 金額は浮動小数点型(float・double)を使わず、DECIMAL か整数で持ちます。 0.1+0.2 は浮動小数点では 0.30000000000000004 になります。
- 消費税の端数処理は、税率ごとの合計に対して1回だけ行います。 明細ごとに丸めて足すと、同じ請求書でも合計が1円ずれます。
- 丸め方式(切り捨て・切り上げ・四捨五入)は仕様書に明記します。 言語や関数によって既定の動きが違い、「round を呼ぶ」だけでは決まりません。
- 負の数(返品・値引き)をどちらへ丸めるかも、あわせて決めておきます。
確認環境:Python 3.11.15(decimal)、OpenJDK 21、Node.js 22.22.2、PostgreSQL 16.15、MySQL 8.0.46(Ubuntu 24.04)。確認日は 2026-10-05。掲載したコードと実行結果はすべてこの環境で動かしたものです。
金額計算の端数処理 早見表
| 決めること | 選択肢 | 迷ったときの目安 |
|---|---|---|
| 金額の型 | DECIMAL(numeric)/整数(円)/浮動小数点 | 円だけなら整数か DECIMAL(12,0)、小数の単価があるなら DECIMAL(12,2) |
| 丸める位置 | 明細ごと/税率ごとの合計/請求書の合計 | 消費税は税率ごとの合計に1回 |
| 丸め方式 | 切り捨て/切り上げ/四捨五入/偶数丸め | 取引先との取り決めに合わせ、仕様書に書く |
| 丸める単位 | 1円未満/10円未満/1,000円未満 | 項目ごとに書く |
| 負の数の扱い | 0に近づける/絶対値で丸めて符号を戻す/小さい方へ | 「正の数と同じ絶対値になる」方式がそろえやすい |
| 途中計算の精度 | 丸めずに持つ/小数第n位まで持つ | 最後の1回まで丸めない |
| 余りの寄せ先(按分) | 最後の行/金額が最大の行 | どちらかを仕様書に書く |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「金額計算の端数処理設計:消費税の丸め仕様とDECIMAL型の選び方」
浮動小数点で金額を持つと起きること:0.1+0.2 の再現
浮動小数点型は数値を2進数で持つため、0.1 のような10進数の小数を正確に表せません。Python で確かめます。
from decimal import Decimal
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
print(sum([0.1] * 10), sum([0.1] * 10) == 1.0)
print(4.35 * 100, int(4.35 * 100))
print(Decimal(0.1))
print(Decimal("0.1") + Decimal("0.2"), Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
print(Decimal("4.35") * 100)
0.30000000000000004 False
0.9999999999999999 False
434.99999999999994 434
0.1000000000000000055511151231257827021181583404541015625
0.3 True
435.00
4.35 * 100 が 434.99999999999994 になり、整数に直すと 434 になりました。4.35円の単価を100個で計算したつもりが、1円少なくなります。Decimal(0.1) の出力は、浮動小数点の 0.1 が実際に持っている値です。
Decimal には文字列を渡します。 Decimal(0.1) のように浮動小数点の値を渡すと、誤差を含んだ値がそのまま入ります。
データベースでも同じです。PostgreSQL と MySQL で、0.1 を多数回足した合計を比べます。
-- PostgreSQL:0.1 を100万行
CREATE TABLE amt (f real, d double precision, n numeric(12,2));
INSERT INTO amt SELECT 0.1, 0.1, 0.1 FROM generate_series(1, 1000000);
SELECT sum(f) AS sum_real, sum(d) AS sum_double, sum(n) AS sum_numeric FROM amt;
sum_real | sum_double | sum_numeric
-----------+-------------------+-------------
99851.664 | 99999.99999938629 | 100000.00
real(単精度)は約148のずれ、double precision(倍精度)でも小数の末尾にずれが出て、numeric だけが正確に 100000.00 になりました。MySQL でも 0.1 を10万行足すと、FLOAT は 10000.000149011612、DOUBLE は 10000.000000018848、DECIMAL(12,2) は 10000.00 でした。
金額を入れる型の選び方
| 型 | 正確さ | 向いている場面 | 気を付ける点 |
|---|---|---|---|
| DECIMAL(PostgreSQL では numeric) | 10進数で正確 | 金額全般、小数のある単価、税率 | 桁数(p,s)を決める必要がある |
| 整数(INT・BIGINT、単位は円) | 正確 | 円未満を扱わない金額 | 単位(円か、銭か)を列名か定義書に書く |
| 浮動小数点(FLOAT・DOUBLE・real) | 近似値 | 統計値、測定値 | 金額には使わない |
| こういうとき | 選ぶ型 |
|---|---|
| 円単位の金額だけを持つ | DECIMAL(12,0) か BIGINT |
| 単価に小数がある(1個 4.35円 など) | DECIMAL(12,2) など、小数桁を持つ DECIMAL |
| 税率・割引率を持つ | DECIMAL(3,2)(0.10、0.08)か、%の整数 |
| JavaScript で金額を計算する | 円の整数で持ち、桁が大きいなら BigInt |
| Python・Java で金額を計算する | Decimal・BigDecimal(文字列から作る) |
DECIMAL(p,s) の桁数の決め方 早見表
DECIMAL(p,s) の p は全体の桁数、s はそのうち小数点以下の桁数です。整数部に使えるのは p-s 桁です。
| 定義 | 整数部の桁 | 入る最大値 | 使いどころの例 |
|---|---|---|---|
| DECIMAL(8,0) | 8 | 99,999,999 | 1億円未満と決まっている金額 |
| DECIMAL(10,0) | 10 | 9,999,999,999 | 明細の単価(円) |
| DECIMAL(12,0) | 12 | 999,999,999,999 | 請求金額、月次の集計 |
| DECIMAL(15,0) | 15 | 999,999,999,999,999 | 年次や全体の集計 |
| DECIMAL(12,2) | 10 | 9,999,999,999.99 | 小数第2位まである単価 |
| DECIMAL(9,3) | 6 | 999,999.999 | 小数のある数量(kg、時間) |
| DECIMAL(3,2) | 1 | 9.99 | 税率(0.10、0.08) |
| DECIMAL(5,4) | 1 | 9.9999 | 割引率、按分の比率 |
| INT | — | 2,147,483,647 | 約21億円までの金額(円) |
| BIGINT | — | 9,223,372,036,854,775,807 | 桁あふれを気にしたくない金額(円) |
桁数は次の順で決めます。
- 1件の最大値を決める(例:単価は9,999万円まで)。
- 集計したときの最大値を決める(例:月次合計は1件の最大値×件数)。集計結果を入れる列は、明細の列より桁を多く取ります。
- 小数桁を決める(円だけなら0、単価に小数があるなら必要な桁)。
- 途中計算の桁を確かめる。PostgreSQL では、掛け算の結果の小数桁は両者の小数桁の和になりました(190 × 0.10 = 19.00)。
桁を超えた値や、小数桁を超えた値を入れたときの動きも確かめました。
CREATE TABLE price (amount numeric(8,0), unit_price numeric(10,2));
INSERT INTO price VALUES (99999999, 1.005);
SELECT * FROM price;
INSERT INTO price VALUES (100000000, 1);
INSERT 0 1
amount | unit_price
----------+------------
99999999 | 1.01
ERROR: numeric field overflow
DETAIL: A field with precision 8, scale 0 must round to an absolute value less than 10^8.
整数部の桁あふれはエラーになりますが、小数桁を超えた分は、エラーにならずに四捨五入されて入ります(1.005 が 1.01)。MySQL でも同じく 1.01 で登録され、Data truncated for column 'unit_price' という Note が出るだけでした。「列の小数桁に合わせてデータベースが丸める」という動きに任せず、アプリケーション側で仕様どおりに丸めてから登録します。
整数型は、範囲を超えるとエラーになります。
CREATE TABLE price_int (amount integer);
INSERT INTO price_int VALUES (2147483648);
SELECT 2000000000 + 2000000000;
ERROR: integer out of range
ERROR: integer out of range
2つ目は、列に入れる前の足し算の時点で範囲を超えた例です。INT の列どうしを足す集計は、結果が約21億を超えるとエラーになるので、集計に使う金額列は BIGINT か DECIMAL にしておくと安心です。
明細ごとに丸める場合と税率ごとに丸める場合の違い
消費税の端数処理について、国税庁の「消費税の仕入税額控除制度における適格請求書等保存方式に関するQ&A」問57(令和6年4月改訂)には、次の内容が書かれています。
- 適格請求書に記載する消費税額等に1円未満の端数が生じる場合は、一の適格請求書につき、税率ごとに1回の端数処理を行う。
- 切上げ、切捨て、四捨五入などの端数処理の方法は任意。
- 個々の商品ごとに消費税額等を計算して端数処理を行い、その合計額を消費税額等として記載することは認められない。
つまり、「どこで丸めるか」は決まっていて、「どう丸めるか」は事業者が決めて仕様にすることになります。丸める位置によって金額が変わることを、Python の decimal で検算します。
from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP, ROUND_UP
ONE = Decimal("1")
# (品名, 税抜単価, 数量, 税率)
lines = [
("ノート", 95, 1, Decimal("0.10")),
("ボールペン", 95, 1, Decimal("0.10")),
("お茶", 100, 1, Decimal("0.08")),
("菓子", 150, 1, Decimal("0.08")),
]
def tax_per_line(lines, mode):
"""明細ごとに丸めてから合計する"""
return sum((Decimal(p) * q * r).quantize(ONE, rounding=mode) for _, p, q, r in lines)
def tax_per_rate(lines, mode):
"""税率ごとに税抜金額を合計し、1回だけ丸める"""
result = {}
for rate in sorted({r for *_, r in lines}, reverse=True):
base = sum(Decimal(p) * q for _, p, q, r in lines if r == rate)
result[rate] = (base, (base * rate).quantize(ONE, rounding=mode))
return result
for name, mode in [("切り捨て", ROUND_DOWN), ("四捨五入", ROUND_HALF_UP), ("切り上げ", ROUND_UP)]:
a = tax_per_line(lines, mode)
by_rate = tax_per_rate(lines, mode)
b = sum(tax for _, tax in by_rate.values())
print(f"{name}: 明細ごと {a}円 / 税率ごと {b}円 / 差 {a - b}円")
for rate, (base, tax) in by_rate.items():
print(f" {int(rate * 100)}%対象 {base}円 × {rate} = {base * rate} → {tax}円")
切り捨て: 明細ごと 38円 / 税率ごと 39円 / 差 -1円
10%対象 190円 × 0.10 = 19.00 → 19円
8%対象 250円 × 0.08 = 20.00 → 20円
四捨五入: 明細ごと 40円 / 税率ごと 39円 / 差 1円
10%対象 190円 × 0.10 = 19.00 → 19円
8%対象 250円 × 0.08 = 20.00 → 20円
切り上げ: 明細ごと 40円 / 税率ごと 39円 / 差 1円
10%対象 190円 × 0.10 = 19.00 → 19円
8%対象 250円 × 0.08 = 20.00 → 20円
95円の品物は、1つずつ計算すると税が 9.5円です。明細ごとに切り捨てると 9円+9円=18円、税率ごとに合計してから計算すると 190円×10%=19円で、1円の差が出ます。四捨五入と切り上げでは、明細ごとのほうが1円多くなりました。
| 丸める位置 | 計算の順番 | この例の消費税(切り捨て) | 特徴 |
|---|---|---|---|
| 明細ごと | 明細の税を丸める→足す | 38円 | 明細の数だけ端数が積み重なる |
| 税率ごとの合計 | 税率ごとに税抜金額を足す→税を計算→1回丸める | 39円 | 端数処理は税率の数だけ |
明細行に「参考の税額」を表示したい場合も、請求書の消費税額として使うのは税率ごとに計算した値にして、明細の税額の合計とは一致しないことがあると仕様書に書いておきます。
税込金額から消費税額を求める場合
税込の合計から消費税額を取り出すときは、税込金額×税率/(100+税率)で計算します。国税庁の問57に載っている例を検算しました。
| 税込金額 | 税率 | 計算式 | 丸める前(小数第2位まで) | 切り捨て | 四捨五入 | 切り上げ |
|---|---|---|---|---|---|---|
| 60,000円 | 10% | 60,000×10/110 | 5454.55 | 5,454円 | 5,455円 | 5,455円 |
| 40,000円 | 8% | 40,000×8/108 | 2962.96 | 2,962円 | 2,963円 | 2,963円 |
問57の例は 5,454円・2,962円で、切り捨てた値と一致しました。同じ金額でも方式によって1円変わるので、方式を決めずに実装すると、担当者ごとに違う結果になります。
同じ計算を SQL・Java・JavaScript で書く
SQL(PostgreSQL):GROUP BY で税率ごとに合計してから丸めます。
CREATE TABLE invoice_lines (
line_no integer PRIMARY KEY,
item_name varchar(40) NOT NULL,
unit_price numeric(10,0) NOT NULL,
quantity integer NOT NULL,
tax_rate numeric(3,2) NOT NULL CHECK (tax_rate IN (0.10, 0.08))
);
INSERT INTO invoice_lines VALUES
(1, 'ノート', 95, 1, 0.10),
(2, 'ボールペン', 95, 1, 0.10),
(3, 'お茶', 100, 1, 0.08),
(4, '菓子', 150, 1, 0.08);
-- 明細ごとに切り捨てて合計
SELECT sum(trunc(unit_price * quantity * tax_rate)) AS tax_per_line FROM invoice_lines;
-- 税率ごとに合計してから切り捨て
SELECT tax_rate,
sum(unit_price * quantity) AS base_amount,
sum(unit_price * quantity) * tax_rate AS raw_tax,
trunc(sum(unit_price * quantity) * tax_rate) AS tax
FROM invoice_lines GROUP BY tax_rate ORDER BY tax_rate DESC;
tax_per_line
--------------
38
tax_rate | base_amount | raw_tax | tax
----------+-------------+---------+-----
0.10 | 190 | 19.00 | 19
0.08 | 250 | 20.00 | 20
Java:BigDecimal を文字列から作り、setScale で丸め方式を明示します(java -Dstdout.encoding=UTF-8 Tax.java で実行)。
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Tax {
record Line(String name, BigDecimal unitPrice, int quantity, BigDecimal taxRate) {}
public static void main(String[] args) {
List<Line> lines = List.of(
new Line("ノート", new BigDecimal("95"), 1, new BigDecimal("0.10")),
new Line("ボールペン", new BigDecimal("95"), 1, new BigDecimal("0.10")),
new Line("お茶", new BigDecimal("100"), 1, new BigDecimal("0.08")),
new Line("菓子", new BigDecimal("150"), 1, new BigDecimal("0.08")));
// 税率ごとに税抜金額を合計する
Map<BigDecimal, BigDecimal> baseByRate = new TreeMap<>();
for (Line l : lines) {
BigDecimal amount = l.unitPrice().multiply(BigDecimal.valueOf(l.quantity()));
baseByRate.merge(l.taxRate(), amount, BigDecimal::add);
}
// 税率ごとの合計に対して1回だけ丸める(ここでは切り捨て)
BigDecimal totalTax = BigDecimal.ZERO;
for (var e : baseByRate.entrySet()) {
BigDecimal tax = e.getValue().multiply(e.getKey()).setScale(0, RoundingMode.DOWN);
System.out.println(e.getKey() + " 対象 " + e.getValue() + "円 → 税 " + tax + "円");
totalTax = totalTax.add(tax);
}
System.out.println("消費税額の合計 " + totalTax + "円");
}
}
0.08 対象 250円 → 税 20円
0.10 対象 190円 → 税 19円
消費税額の合計 39円
JavaScript:Number は浮動小数点なので、円と%を整数で持ち、BigInt で計算します。
// 金額は「円」の整数、税率は「%」の整数で持ち、BigInt で計算する
const lines = [
{ name: "ノート", unitPrice: 95n, quantity: 1n, taxRate: 10n },
{ name: "ボールペン", unitPrice: 95n, quantity: 1n, taxRate: 10n },
{ name: "お茶", unitPrice: 100n, quantity: 1n, taxRate: 8n },
{ name: "菓子", unitPrice: 150n, quantity: 1n, taxRate: 8n },
];
// 税率ごとに税抜金額を合計する
const baseByRate = new Map();
for (const l of lines) {
baseByRate.set(l.taxRate, (baseByRate.get(l.taxRate) ?? 0n) + l.unitPrice * l.quantity);
}
// 税率ごとの合計に対して1回だけ丸める(BigInt の割り算は 0 方向への切り捨て)
let totalTax = 0n;
for (const [rate, base] of baseByRate) {
const tax = (base * rate) / 100n;
console.log(`${rate}% 対象 ${base}円 → 税 ${tax}円`);
totalTax += tax;
}
console.log(`消費税額の合計 ${totalTax}円`);
10% 対象 190円 → 税 19円
8% 対象 250円 → 税 20円
消費税額の合計 39円
BigInt で正の数を四捨五入・切り上げにするときは、割る前に数を足します。195円×10%(19.5円)で確かめると、195n * 10n / 100n は 19、(195n * 10n + 50n) / 100n は 20(四捨五入)、(195n * 10n + 99n) / 100n は 20(切り上げ)でした。
切り捨て・切り上げ・四捨五入・偶数丸めの違い
同じ入力値を、Python の decimal が持つ7つの丸め方式で整数に丸めた結果です。
| 丸め方式(decimal の定数) | 0.5 | 1.5 | 2.5 | 2.4 | 2.6 | -0.5 | -1.5 | -2.5 | -2.4 | -2.6 |
|---|---|---|---|---|---|---|---|---|---|---|
| 切り捨て・0に近づける(ROUND_DOWN) | 0 | 1 | 2 | 2 | 2 | -0 | -1 | -2 | -2 | -2 |
| 切り上げ・0から遠ざける(ROUND_UP) | 1 | 2 | 3 | 3 | 3 | -1 | -2 | -3 | -3 | -3 |
| 小さい方へ(ROUND_FLOOR) | 0 | 1 | 2 | 2 | 2 | -1 | -2 | -3 | -3 | -3 |
| 大きい方へ(ROUND_CEILING) | 1 | 2 | 3 | 3 | 3 | -0 | -1 | -2 | -2 | -2 |
| 四捨五入(ROUND_HALF_UP) | 1 | 2 | 3 | 2 | 3 | -1 | -2 | -3 | -2 | -3 |
| 五捨六入(ROUND_HALF_DOWN) | 0 | 1 | 2 | 2 | 3 | -0 | -1 | -2 | -2 | -3 |
| 偶数丸め(ROUND_HALF_EVEN) | 0 | 2 | 2 | 2 | 3 | -0 | -2 | -2 | -2 | -3 |
表から読み取れることは3つです。
- 方式で差が出るのは、ちょうど 0.5 のときと負の数のときです。 2.4 と 2.6 は、切り捨て・切り上げ以外ではどれも同じ結果になります。
- 「切り捨て」には2種類あります。 0に近づける方式(ROUND_DOWN)は -2.5 を -2 に、小さい方へ寄せる方式(ROUND_FLOOR)は -2.5 を -3 にします。正の数では同じなので、負の数を試すまで違いに気づきません。
- 偶数丸めは、ちょうど 0.5 のときに偶数側へ寄せます。 0.5 は 0、1.5 は 2、2.5 は 2 です。
結果が 0 になる負の数は -0 と表示されました。Decimal("-0") == 0 は True で、0 を足すと 0 になりますが、文字列にして出力すると「-0」のままです。帳票に出す前に + 0 するなどの扱いを決めておきます。
偶数丸めはどんなときに使うか
四捨五入は、ちょうど 0.5 の値をいつも大きい方へ寄せるため、多数の値を丸めて合計すると、合計が少しずつ大きい方へ寄ります。0.5、1.5、…、999.5 の1,000個で確かめました。
| 項目 | 合計 |
|---|---|
| 丸める前の合計 | 500,000 |
| 四捨五入してから合計 | 500,500 |
| 偶数丸めしてから合計 | 500,000 |
四捨五入では 500 大きくなり、偶数丸めでは丸める前の合計と一致しました。多数の値を丸めて集計する統計的な処理には偶数丸めが合います。 一方、請求書の消費税のように「取引先と同じ金額になること」が大事な場面では、取り決めた方式(切り捨てや四捨五入)をそのまま使います。
| こういうとき | 選ぶ方式 |
|---|---|
| 取引先・社内規程で方式が決まっている | その方式(仕様書に出典を書く) |
| 消費税額で、特に取り決めがない | 経理担当に確認して決める(方式は任意とされている) |
| 多数の値を丸めて集計し、偏りを抑えたい | 偶数丸め |
| 返品・値引きで負の数が出る | 正の数と同じ絶対値になる方式(ROUND_DOWN・ROUND_UP・ROUND_HALF_UP) |
言語ごとの丸め関数:負の数と 0.5 でどう動くか
「round を呼べば四捨五入」とは限りません。同じ6つの値を、各言語の関数で丸めた結果です。
| 言語 | 書き方 | 0.5 | 1.5 | 2.5 | -0.5 | -1.5 | -2.5 | 動き |
|---|---|---|---|---|---|---|---|---|
| Python | round(x) |
0 | 2 | 2 | 0 | -2 | -2 | 偶数丸め |
| Python | math.floor(x) |
0 | 1 | 2 | -1 | -2 | -3 | 小さい方へ |
| Python | math.ceil(x) |
1 | 2 | 3 | 0 | -1 | -2 | 大きい方へ |
| Python | int(x) math.trunc(x) |
0 | 1 | 2 | 0 | -1 | -2 | 0に近づける |
| Python | Decimal(str(x)).quantize(Decimal("1"), ROUND_HALF_UP) |
1 | 2 | 3 | -1 | -2 | -3 | 四捨五入 |
| Java | Math.round(x) |
1 | 2 | 3 | 0 | -1 | -2 | 0.5 は大きい方へ |
| Java | Math.rint(x) |
0.0 | 2.0 | 2.0 | -0.0 | -2.0 | -2.0 | 偶数丸め |
| Java | Math.floor(x) |
0.0 | 1.0 | 2.0 | -1.0 | -2.0 | -3.0 | 小さい方へ |
| Java | (long) x |
0 | 1 | 2 | 0 | -1 | -2 | 0に近づける |
| Java | BigDecimal.valueOf(x).setScale(0, RoundingMode.HALF_UP) |
1 | 2 | 3 | -1 | -2 | -3 | 四捨五入 |
| Java | …setScale(0, RoundingMode.HALF_EVEN) |
0 | 2 | 2 | 0 | -2 | -2 | 偶数丸め |
| JavaScript | Math.round(x) |
1 | 2 | 3 | -0 | -1 | -2 | 0.5 は大きい方へ |
| JavaScript | Math.floor(x) |
0 | 1 | 2 | -1 | -2 | -3 | 小さい方へ |
| JavaScript | Math.trunc(x) |
0 | 1 | 2 | -0 | -1 | -2 | 0に近づける |
| JavaScript | x.toFixed(0) |
1 | 2 | 3 | -1 | -2 | -3 | 四捨五入(文字列を返す) |
| PostgreSQL | round(x::numeric) |
1 | 2 | 3 | -1 | -2 | -3 | 四捨五入 |
| PostgreSQL | round(x::float8) |
0 | 2 | 2 | -0 | -2 | -2 | 偶数丸め |
| PostgreSQL | trunc(x::numeric) |
0 | 1 | 2 | 0 | -1 | -2 | 0に近づける |
| PostgreSQL | floor(x::numeric) |
0 | 1 | 2 | -1 | -2 | -3 | 小さい方へ |
| MySQL | ROUND(x)(DECIMAL の値) |
1 | 2 | 3 | -1 | -2 | -3 | 四捨五入 |
| MySQL | ROUND(x)(DOUBLE の値) |
0 | 2 | 2 | -0 | -2 | -2 | 偶数丸め |
| MySQL | TRUNCATE(x, 0) |
0 | 1 | 2 | 0 | -1 | -2 | 0に近づける |
| MySQL | FLOOR(x) |
0 | 1 | 2 | -1 | -2 | -3 | 小さい方へ |
-2.5 の列を見ると、-2・-3 の両方が並んでいます。 Python の round は -2、Java と JavaScript の Math.round も -2、PostgreSQL と MySQL の DECIMAL に対する ROUND は -3 です。同じ「round」という名前でも、アプリケーションで計算した値とSQLで計算した値が食い違います。
PostgreSQL と MySQL の ROUND は、渡した値の型で動きが変わります。 DECIMAL(numeric)なら四捨五入、浮動小数点なら偶数丸めでした。金額列を DECIMAL にしておくことは、丸めの動きをそろえる意味でも大切です。
小数第2位で丸めるときは、浮動小数点の誤差も加わります。
| 書き方 | 結果 | 理由 |
|---|---|---|
Python round(2.675, 2) |
2.67 | 2.675 が内部で 2.67499… になっている |
Python Decimal("2.675").quantize(Decimal("0.01"), ROUND_HALF_UP) |
2.68 | 10進数のまま丸める |
JavaScript 1.005 * 100 |
100.49999999999999 | 掛け算の時点で誤差が出る |
JavaScript Math.round(1.005 * 100) / 100 |
1 | 誤差のある値を丸めている |
JavaScript (1.005).toFixed(2) |
"1.00" | 同上 |
割り算にも言語ごとの違いがあります。
| 書き方 | 結果 |
|---|---|
PostgreSQL 1000 / 3(整数どうし) |
333(小数は切り捨て) |
PostgreSQL 1000::numeric / 3 |
333.3333333333333333 |
MySQL 1000 / 3 |
333.3333 |
MySQL 1000 DIV 3 |
333 |
Java new BigDecimal("1000").divide(new BigDecimal("3")) |
ArithmeticException(Non-terminating decimal expansion; no exact representable decimal result.) |
Java …divide(new BigDecimal("3"), 0, RoundingMode.DOWN) |
333 |
Java の BigDecimal は、割り切れない割り算で丸め方式を指定しないと例外になります。 方式を書かせる仕組みになっているので、仕様書の丸め方式をそのままコードに写せます。
按分で出る余りの寄せ方
金額を分けるときも端数が出ます。1,000円を3つに等分して切り捨てると 333円×3=999円で、1円が余ります。 この1円をどこに寄せるかも仕様です。
| 寄せ方 | 内容 | 向いている場面 |
|---|---|---|
| 最後の行に寄せる | 333・333・334 | 実装が単純。並び順が決まっているとき |
| 金額が最大の行に寄せる | 割合への影響が小さい行に載せる | 金額の差が大きい明細を按分するとき |
| 余りを別の項目にする | 「端数調整」の行を立てる | 明細の金額を変えたくないとき |
どの方式でも、「分けた金額の合計が元の金額と一致すること」をテストで確かめます。
端数処理の仕様を書くテンプレート
端数処理は「対象項目・丸め位置・丸め方式・丸める単位」の4つを項目ごとに書きます。架空の備品販売システムを題材にした記入例です。
共通の決まり
| 項目 | 記入例 |
|---|---|
| 金額の型(データベース) | DECIMAL(12,0)、単位は円。小数のある単価のみ DECIMAL(12,2) |
| 金額の型(アプリケーション) | Java は BigDecimal(文字列から生成)。浮動小数点は使わない |
| 途中計算 | 丸めずに持ち、下表の「丸め位置」で1回だけ丸める |
| 負の数(返品・値引き) | 絶対値で丸めて符号を戻す(-2.5 の切り捨ては -2) |
| 方式を決めた根拠 | 経理担当の確認日と確認者、取引先との取り決めの文書名 |
項目ごとの端数処理
| No | 対象項目 | 計算式 | 丸め位置 | 丸め方式 | 丸める単位 | 実装での指定 | 備考 |
|---|---|---|---|---|---|---|---|
| 1 | 明細金額 | 単価×数量 | 明細ごと | 切り捨て | 1円未満 | RoundingMode.DOWN | 単価に小数がある品目のみ端数が出る |
| 2 | 値引き額 | 明細金額×割引率 | 明細ごと | 切り捨て | 1円未満 | RoundingMode.DOWN | 値引き後金額=明細金額-値引き額 |
| 3 | 消費税額(税抜の請求書) | 税率ごとの税抜合計×税率 | 請求書1枚につき税率ごとに1回 | 切り捨て | 1円未満 | RoundingMode.DOWN | 明細ごとには丸めない |
| 4 | 消費税額(税込の請求書) | 税率ごとの税込合計×税率/(100+税率) | 請求書1枚につき税率ごとに1回 | 切り捨て | 1円未満 | RoundingMode.DOWN | No.3 と同じ方式にそろえる |
| 5 | 明細の参考税額(表示のみ) | 明細金額×税率 | 明細ごと | 切り捨て | 1円未満 | RoundingMode.DOWN | 合計は No.3 と一致しないことがある旨を帳票に注記 |
| 6 | 送料の按分 | 送料×明細金額/合計金額 | 明細ごと | 切り捨て | 1円未満 | RoundingMode.DOWN | 余りは金額が最大の明細に加える |
| 7 | 月次の平均単価(集計表) | 合計金額/数量 | 表示時 | 四捨五入 | 小数第2位未満 | RoundingMode.HALF_UP | 保存はせず表示のみ |
表計算ソフトに貼れるタブ区切りです。
No 対象項目 計算式 丸め位置 丸め方式 丸める単位 実装での指定 備考
1 明細金額 単価×数量 明細ごと 切り捨て 1円未満 RoundingMode.DOWN 単価に小数がある品目のみ端数が出る
2 値引き額 明細金額×割引率 明細ごと 切り捨て 1円未満 RoundingMode.DOWN 値引き後金額=明細金額-値引き額
3 消費税額(税抜の請求書) 税率ごとの税抜合計×税率 請求書1枚につき税率ごとに1回 切り捨て 1円未満 RoundingMode.DOWN 明細ごとには丸めない
4 消費税額(税込の請求書) 税率ごとの税込合計×税率/(100+税率) 請求書1枚につき税率ごとに1回 切り捨て 1円未満 RoundingMode.DOWN No.3 と同じ方式にそろえる
5 明細の参考税額(表示のみ) 明細金額×税率 明細ごと 切り捨て 1円未満 RoundingMode.DOWN 合計は No.3 と一致しないことがある旨を帳票に注記
6 送料の按分 送料×明細金額/合計金額 明細ごと 切り捨て 1円未満 RoundingMode.DOWN 余りは金額が最大の明細に加える
7 月次の平均単価(集計表) 合計金額/数量 表示時 四捨五入 小数第2位未満 RoundingMode.HALF_UP 保存はせず表示のみ
書くときのポイントです。
- 「実装での指定」の列に、使う定数や関数名まで書きます。 「切り捨て」という言葉だけでは、0に近づけるのか小さい方へ寄せるのかが決まりません。
- テストケースに、ちょうど 0.5 になる値と負の数を入れます。 方式の違いが表に出るのはこの2つだけです。
- 同じ金額を2か所で計算しないようにします。アプリケーションで計算して保存するのか、SQL で集計するのかを項目ごとに決めます。
申告で使う端数処理は別に決まっている
請求書に書く消費税額の端数処理と、申告のときの端数処理は別のものです。国税庁のタックスアンサー No.6371「端数計算」(令和8年4月1日現在法令等)には、課税標準額に1,000円未満の端数があるときは切り捨てること、納付する税額に100円未満の端数があるときは切り捨てることが書かれています。申告に関わる集計機能を設計するときは、経理担当や税理士に確認したうえで、テンプレートに「丸める単位:1,000円未満」のような行を足します。
確かめて分かったこと
4.35 * 100を整数に直すと 434 になりました。 0.1+0.2 のような分かりやすい例だけでなく、単価×数量という普通の計算でも1円のずれが起きます。- PostgreSQL と MySQL の ROUND は、DECIMAL の値には四捨五入、浮動小数点の値には偶数丸めでした。 2.5 が、型によって 3 にも 2 にもなります。
- -2.5 を丸めた結果は、Python の round が -2、Java と JavaScript の Math.round が -2、SQL の ROUND(DECIMAL)が -3 でした。 理由はそれぞれ違い、Python は偶数丸め、Java と JavaScript は 0.5 を大きい方へ寄せる動きです。
- DECIMAL(10,2) の列に 1.005 を入れると、エラーにならず 1.01 で登録されました。 整数部の桁あふれだけがエラーになります。
- Python の decimal は、0 になる負の数を「-0」と表示しました。 値としては 0 と等しいものの、そのまま文字列にすると帳票に「-0」が出ます。
- Java の BigDecimal は、割り切れない割り算で丸め方式を指定しないと例外になりました。
よくある質問
Q. 消費税の端数処理は切り捨てと四捨五入のどちらですか? A. 国税庁のQ&A(問57)では、切上げ・切捨て・四捨五入などの方法は任意とされています。決まっているのは、1枚の適格請求書につき税率ごとに1回という丸める位置です。どの方式にするかは経理担当や税理士に確認して決め、仕様書に書きます。
Q. 金額を float や double で持ってはいけないのはなぜですか? A. 0.1 のような小数を正確に表せず、計算のたびに誤差が出るためです。0.1 を100万回足すと、倍精度でも 99999.99999938629 になりました。金額は DECIMAL か、円単位の整数で持ちます。
Q. DECIMAL と整数型は、金額にはどちらがよいですか? A. 円未満を扱わないならどちらでも正確に持てます。小数のある単価や税率を同じ計算に混ぜるなら DECIMAL が扱いやすく、JavaScript のように10進数の型が標準に無い言語では整数(必要なら BigInt)が向いています。
Q. 偶数丸めとは何ですか? A. ちょうど 0.5 のときに、結果が偶数になる側へ丸める方式です。0.5 は 0、1.5 は 2、2.5 は 2 になります。多数の値を丸めて合計したときの偏りが小さく、Python の round や、浮動小数点に対する SQL の ROUND がこの動きでした。
Q. 明細の消費税を足しても請求書の消費税額と合わないときは? A. 丸める位置が違うことが原因です。明細ごとに丸めた税額の合計と、税率ごとの合計に対して1回丸めた税額は、この記事の例で1円ずれました。請求書の消費税額には税率ごとに計算した値を使い、明細の税額は参考表示として扱います。
まとめ
- 金額は浮動小数点型を使わず、DECIMAL か整数で持ちます。
4.35 * 100を整数にすると 434 になりました。 - 消費税の端数処理は、1枚の請求書につき税率ごとに1回です。 明細ごとに丸めて足すと、同じ内容で1円ずれました。
- 丸め方式は任意とされているので、仕様書に明記します。 「切り捨て」と書くだけでなく、使う定数(RoundingMode.DOWN など)まで書きます。
- DECIMAL(p,s) は、1件の最大値・集計の最大値・小数桁の順に決めます。 小数桁を超えた値は、エラーにならず丸めて登録されます。
- 方式の違いが表に出るのは、ちょうど 0.5 の値と負の数です。 テストケースに必ず入れます。
- 同じ round という名前でも、言語と型で動きが違います。 Python の round と浮動小数点の SQL ROUND は偶数丸め、DECIMAL の SQL ROUND は四捨五入でした。
- 按分の余りの寄せ先も仕様です。 分けた金額の合計が元の金額と一致することを確かめます。
参考・出典
- 国税庁「消費税の仕入税額控除制度における適格請求書等保存方式に関するQ&A」問57(適格請求書に記載する消費税額等の端数処理、令和6年4月改訂) https://www.nta.go.jp/taxes/shiraberu/zeimokubetsu/shohi/keigenzeiritsu/pdf/qa/57.pdf
- 国税庁 タックスアンサー No.6371「端数計算」(令和8年4月1日現在法令等) https://www.nta.go.jp/taxes/shiraberu/taxanswer/shohi/6371.htm
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「金額計算の端数処理設計:消費税の丸め仕様とDECIMAL型の選び方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。