「御社の請求書、合計が当方の計算と 1 円合わないのですが」— 経理から、あるいは取引先から、この問い合わせを受けたことのある開発者は少なくないはずです。
金額は、業務アプリでもっとも「ズレてはいけない」データでありながら、もっとも壊れやすいデータでもあります。日本の現場では、同じ事故が形を変えて繰り返し起きます。
- 単価 × 数量を
doubleで計算したら、合計が9999.9999996のような端数になる(浮動小数点の誤差) - 金額を
"¥1,234"のような文字列でセルに入れたら、合計も並べ替えもできなくなった(文字列化) - 消費税の端数処理が取引先と食い違い、請求総額が 1 円ずれる(端数処理の不一致)
- 軽減税率 8% と標準税率 10% が混在する明細で、税額の計算をどこでまとめるか迷う(軽減税率)
これらはどれも「気をつける」で減らせるものではありません。原因は構造的で、対策も構造的です。この記事では ReoGrid を使い、セルのデータは数値のまま保ち、表示だけを通貨書式にするという原則に沿って、通貨表示・消費税・端数処理・税率別集計を C# で組み立てます。WinForms / WPF どちらでも、Excel のインストールは不要です。
ここでつまずく — 金額を「文字列」にしてしまう
いちばん多い失敗は、表示を整えたいあまり、金額そのものを文字列にしてしまうことです。
// よくある実装 — これは壊れます
sheet["E5"] = $"¥{amount:N0}"; // "¥1,234,567" という文字列が入る
見た目は完璧です。¥1,234,567 と表示されます。しかし、このセルに入っているのは数値ではなく文字列です。その瞬間、次のすべてが壊れます。
=SUM(E5:E9)が 0 を返す(文字列は合計されない)- 金額の列で並べ替えると、
¥1,000,000が¥999より前に来る(文字列としての辞書順) - 別のセルでこの値を参照した数式が
#VALUE!になる
double で持つのも別の地雷を踏みます。0.1 + 0.2 != 0.3 の世界なので、税率を掛けて積み上げた合計に微小な誤差が残り、ROUND をかけ忘れた箇所で 1 円ズレます。
正しい原則はひとつです。セルには素の数値を入れ、「¥」や桁区切りは「書式」として後からかぶせる。データと見た目を分離すれば、合計も並べ替えも数式もすべて生き続けます。
通貨書式 — データは数値、表示は「¥1,234,567」
ReoGrid では、セルに数値を入れたうえで SetRangeDataFormat に通貨書式を指定します。
using unvell.ReoGrid;
using unvell.ReoGrid.DataFormat;
var sheet = grid.CurrentWorksheet;
// データはあくまで数値
sheet["E5"] = 1234567;
// 表示だけを通貨書式にする
sheet.SetRangeDataFormat("E5:E20", CellDataFormatFlag.Currency,
new CurrencyDataFormatter.CurrencyFormatArgs
{
CultureEnglishName = "ja-JP",
PrefixSymbol = "¥",
DecimalPlaces = 0, // 円は小数なし
UseSeparator = true, // 3桁区切り
});
これで E5 は ¥1,234,567 と表示されますが、セルが保持しているのは数値 1234567 のままです。=SUM(E5:E20) も並べ替えも、何ごともなく動きます。
円は小数点以下を持たないのが普通なので DecimalPlaces = 0 を指定します。1 円未満を扱う単価(例:ガソリン単価 156.7円/L)がある列だけ DecimalPlaces = 1 にする、といった列ごとの使い分けも自由です。
マイナスを赤字や「▲」で表示する
会計帳票では、マイナスの金額を赤字や三角(▲)で示す慣習があります。NegativeStyle で指定できます。
sheet.SetRangeDataFormat("E5:E20", CellDataFormatFlag.Currency,
new CurrencyDataFormatter.CurrencyFormatArgs
{
CultureEnglishName = "ja-JP",
PrefixSymbol = "¥",
DecimalPlaces = 0,
UseSeparator = true,
// 赤字+括弧: ¥(1,234)
NegativeStyle = NumberDataFormatter.NumberNegativeStyle.RedBrackets,
});
日本式の三角表記が必要なら Prefix_Sankaku を使います。
// ▲ 1,234 という日本式のマイナス表記
NegativeStyle = NumberDataFormatter.NumberNegativeStyle.Prefix_Sankaku,
NumberNegativeStyle はフラグなので、Red | Brackets のように組み合わせることもできます。いずれの場合も、セルのデータは負の数値そのまま — 表示の見え方が変わるだけです。
消費税と端数処理 — 「どこで丸めるか」が 1 円を分ける
金額がズレるいちばんの原因は、消費税の端数処理です。1234 × 0.1 = 123.4 の 0.4 をどう扱うか。ここで二つの判断が必要になります。
- 丸め方 — 切り捨て / 四捨五入 / 切り上げ。これは法律で一律に決まっておらず、事業者が選んで継続適用するものです(多くの BtoC は切り捨て、BtoB は取引先との取り決めによる)。
- どの単位で丸めるか — 明細の行ごとに丸めるのか、税率ごとの合計に対して一度だけ丸めるのか。
特に 2 番目が重要です。インボイス制度(適格請求書)では、消費税額の端数処理は「1 つの請求書につき、税率ごとに 1 回」が原則です。行ごとに丸めて積み上げる旧来のやり方は認められません。つまり、先に税率ごとの税抜金額を合計し、その合計に対して 1 回だけ端数処理をするのが正しい順序です。
ReoGrid は ROUND / ROUNDDOWN / ROUNDUP を内蔵しているので、この丸めをそのまま数式で表現できます。
// 税抜の小計(数値)に対して、消費税を切り捨てで 1 回だけ計算する
sheet["E20"] = "=ROUNDDOWN(E19*0.1, 0)"; // 第2引数 0 → 1円未満を切り捨て
C# 側で計算して結果だけ入れたい場合は、double ではなく decimal を使うのが鉄則です。
// 金額計算は必ず decimal で(double は誤差が出る)
decimal subtotal = 12_345m;
decimal tax = Math.Floor(subtotal * 0.10m); // 切り捨て → 1234
sheet["E20"] = tax; // 数値としてセルへ
Math.Floor が切り捨て(ROUNDDOWN 相当)、Math.Round(x, MidpointRounding.AwayFromZero) が四捨五入(ROUND 相当)です。丸め方を「請求書の中で 1 か所」に集約しておくこと。あちこちで丸めると、合計と内訳が合わなくなります。
軽減税率 — 税率別の集計は SUMIF で
軽減税率 8%(飲食料品など)と標準税率 10% が 1 枚の請求書に混在するのは、いまや珍しくありません。インボイスでは税率ごとに区分して合計・税額表示する必要があります。
明細表をこう作るとします。C 列に税率(10 か 8)、D 列に税抜金額を入れる構成です。
| B(品名) | C(税率%) | D(税抜金額) | |
|---|---|---|---|
| 5 | コピー用紙 | 10 | 3,000 |
| 6 | 会議用 弁当 | 8 | 4,000 |
| 7 | トナー | 10 | 12,000 |
| 8 | 来客用 茶菓子 | 8 | 1,500 |
税率別の集計と税額は、SUMIF と ROUNDDOWN を組み合わせて数式で組めます。
var sheet = grid.CurrentWorksheet;
// --- 税率別の税抜合計 ---
sheet["D11"] = "=SUMIF(C5:C8, 10, D5:D8)"; // 10%対象の税抜計 → 15,000
sheet["D12"] = "=SUMIF(C5:C8, 8, D5:D8)"; // 8%対象の税抜計 → 5,500
// --- 税率ごとに 1 回だけ端数処理(インボイス対応) ---
sheet["D13"] = "=ROUNDDOWN(D11*0.1, 0)"; // 10%消費税 → 1,500
sheet["D14"] = "=ROUNDDOWN(D12*0.08, 0)"; // 8%消費税 → 440
// --- 税込総合計 ---
sheet["D15"] = "=D11+D12+D13+D14"; // 22,440
// 金額列に通貨書式をまとめて適用
sheet.SetRangeDataFormat("D5:D15", CellDataFormatFlag.Currency,
new CurrencyDataFormatter.CurrencyFormatArgs
{
CultureEnglishName = "ja-JP",
PrefixSymbol = "¥",
DecimalPlaces = 0,
UseSeparator = true,
});
ポイントは、税額を「税率ごとの合計(D11 / D12)に対して」計算していることです。各行で丸めてから足すのではなく、SUMIF でまず税率別に積んでから ROUNDDOWN を 1 回かける — これがインボイス制度の求める順序であり、取引先と 1 円もズレない計算です。
数式はセルに入れるだけで ReoGrid の内蔵エンジンが再計算します。D5 の値を書き換えれば D11〜D15 が自動で追従するので、ユーザーが画面上で金額を直しても合計は常に正しいままです。
テンプレート × 数式で「崩れない帳票」にする
ここまでを組み合わせると、見た目は Excel で作ったテンプレートに任せ、コードは数値と数式を流し込むだけ、という構成になります。罫線・ロゴ・ラベルといった固定レイアウトは .xlsx テンプレートが持ち、可変の金額だけをコードが埋めます。
grid.Load("invoice-template.xlsx"); // レイアウトは Excel で用意
var sheet = grid.CurrentWorksheet;
// 明細(税率と税抜金額は数値で入れる)
var items = new[]
{
("コピー用紙", 10, 3000m),
("会議用 弁当", 8, 4000m),
("トナー", 10, 12000m),
("来客用 茶菓子", 8, 1500m),
};
int r = 5;
foreach (var (name, rate, price) in items)
{
sheet[$"B{r}"] = name;
sheet[$"C{r}"] = rate;
sheet[$"D{r}"] = price; // decimal をそのまま数値セルへ
r++;
}
// 集計は数式(テンプレート側にあらかじめ入れておいてもよい)
sheet["D11"] = "=SUMIF(C5:C8, 10, D5:D8)";
sheet["D12"] = "=SUMIF(C5:C8, 8, D5:D8)";
sheet["D13"] = "=ROUNDDOWN(D11*0.1, 0)";
sheet["D14"] = "=ROUNDDOWN(D12*0.08, 0)";
sheet["D15"] = "=D11+D12+D13+D14";
grid.Save($"invoice-{DateTime.Now:yyyyMMdd}.xlsx"); // 控えを .xlsx で保存
テンプレート側に通貨書式と集計式を作り込んでおけば、コードはさらに薄くなります。.xlsx で控えを残せば、相手が Excel で開いても書式と数値の両方がファイルの中に保持されるので、金額が崩れることはありません(テンプレート流し込みの考え方は 約30行で領収書発行アプリを作る で詳しく扱っています)。
まとめ
- 金額が崩れる原因は構造的 — 文字列化(合計・並べ替えが壊れる)と
doubleの誤差 - セルには素の数値を入れ、「¥」と桁区切りは通貨書式でかぶせる。データと表示を分離すれば数式も並べ替えも生き続ける
- 通貨書式は
SetRangeDataFormat(..., CellDataFormatFlag.Currency, new CurrencyFormatArgs { PrefixSymbol = "¥", DecimalPlaces = 0, UseSeparator = true }) - マイナスは
NegativeStyleで赤字・括弧・日本式のPrefix_Sankaku(▲) に - 金額計算は C# なら
decimal、丸めはMath.Floor(切り捨て)/Math.Round(四捨五入)。丸め方は請求書内で 1 か所に集約する - 消費税の端数処理は インボイス制度の原則どおり「税率ごとに 1 回」 —
SUMIFで税率別に合計してからROUNDDOWNを 1 回かける - 軽減税率の混在は
SUMIF(範囲, 税率, 金額範囲)で税率別に区分集計 - レイアウトは
.xlsxテンプレートに任せ、コードは数値と数式だけ流し込む
金額のズレは「検算でなんとかする」ものではありません。データは数値のまま持ち、丸める場所を 1 か所に決める — この 2 つを守れば、合計は最初から合います。
次に読むもの
- データ書式 — Number / Currency / DateTime など書式 API の全体像
- 数式と関数 —
ROUND/ROUNDDOWN/SUMIFなど組み込み関数 - 約30行のC#で領収書発行アプリを作る — Excel テンプレートに値を流し込む
- 和暦(令和)対応の日付セル — 請求日・発行日を和暦で表示する
- 「CSV を Excel で開いたら 0 が消えた」を防ぐ — 金額・コードの型を開発者が握る
