やりたいこと
kintone の見積アプリで、テーブルの「数量 × 単価」を JavaScript で計算して金額の欄に入れたい。ところが、そのまま掛けるとこうなります。
0.1 * 3 // 0.30000000000000004
1.15 * 100 // 114.99999999999999
Math.round(1.005 * 100) // 100(1.005 * 100 が 100.49999999999999 になる)
数量 1.005 × 単価 100円 は 100.5円なので、四捨五入すれば 101円 のはずです。ところが Math.round の結果は 100円 になります。JavaScript の数は2進数の浮動小数点数で、1.005 をちょうどは表せないためです。
この記事では、建設向けの見積書プラグインで使っている、ライブラリーなしで誤差を出さない書き方を紹介します。
考え方:小数を「整数 × 10のマイナスs乗」で持つ
"1.005" を {i: 1005, s: 3}(1005 × 10⁻³)として持てば、掛け算は整数どうしになり、誤差は出ません。kintone の数値フィールドの値は 文字列 で届くので、Number() に通す前の文字列のまま分けられるのが好都合です。
// 小数を「整数 × 10のマイナスs乗」で持ち、掛け算の誤差を出さない
function parse(value) {
var text = (value === null || value === undefined) ? '' : String(value).trim();
if (text === '') return {empty: true, i: 0, s: 0};
var m = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(text);
if (!m) return {bad: true, i: 0, s: 0};
var frac = m[3] || '';
var i = Number(m[2] + frac);
if (!Number.isSafeInteger(i) || frac.length > 6) return {bad: true, i: 0, s: 0};
return {i: m[1] === '-' ? -i : i, s: frac.length};
}
(kintone/estimate/src/js/desktop.js)
-
"1.005"→ 整数部1と小数部005をつないで1005、小数の桁数s = 3。 - 空欄は
empty、数でないものはbadとして返し、呼ぶ側で「○行目の数量か単価が数ではありません」と知らせます。 -
Number.isSafeIntegerで、整数として正確に扱える範囲(2⁵³−1 まで)を超えたらbadにします。小数は6桁までにしています。
割り算と四捨五入も整数で
最後に円にするとき、整数の積を 10^s で割って四捨五入します。ここで Math.round(積 / 10^s) と書くと、割り算の結果の小数を Math.round に渡すことになります。そこで、割り算と四捨五入を1つの整数の式にします。
// n ÷ d を1円未満で四捨五入する(.5 は大きいほうへ。/tools/mitsumori/ の Math.round と同じ)
function roundDiv(n, d) {
return Math.floor((2 * n + d) / (2 * d));
}
n ÷ d を四捨五入した値は floor(n/d + 1/2) です。分母をそろえると floor((2n + d) / 2d) になります。ちょうど .5 の境目では 2n + d が 2d で割り切れるので、割り算の結果はちょうどの整数になり、floor で下にずれません。小数を掛けた途中の値(100.49999...)を経由しないのがポイントです。
使う側はこうなります。
var q = parse(valueOf(row.value, config.qty));
var p = parse(valueOf(row.value, config.price));
if (q.bad || p.bad) notes.push((index + 1) + '行目の数量か単価が数ではありません');
if (q.empty && p.empty) return {row: row, amount: null};
var product = q.i * p.i;
if (!Number.isSafeInteger(product)) {
notes.push((index + 1) + '行目の金額が大きすぎて計算できません');
return {row: row, amount: null};
}
var amount = roundDiv(product, pow10(q.s + p.s));
数量 1.005 × 単価 100 は 1005 × 100 = 100500、s = 3 + 0 なので roundDiv(100500, 1000) = 101円。掛けた結果も isSafeInteger で確かめ、大きすぎるときは黙って間違えずに知らせます。
率の計算と、消費税の切り捨て
諸経費率(%)も同じ形で計算します。小計 × 率 ÷ 100 を、率の小数の桁まで含めて1回で割ります。
var rate = parse(rateText);
if (rate.bad) notes.push('諸経費率が数ではありません');
var exp = roundDiv(sub * rate.i, 100 * pow10(rate.s));
12.5 % なら rate = {i: 125, s: 1} で、sub × 125 ÷ 1000 を四捨五入します。
消費税は10%・1円未満切り捨てにしています。円の整数を10で割って Math.floor するだけなので、ここは誤差の心配がありません。
// 消費税は10%、1円未満切り捨て(/tools/mitsumori/ と同じ Math.floor)
var tax = Math.floor(base / 10);
端数の扱い(四捨五入か切り捨てか)は会社や発注者で決まりが違うことがあります。コードの中でどちらにしているかを、コメントに必ず書いておくと、あとで読む人が迷いません。
消化率(%)を小数1桁で出す
原価管理のプラグインでは、実績 ÷ 予算 の消化率を小数1桁で出しています。ここも roundDiv を使い、× 1000 してから整数で割り、最後に10で割ります(kintone/genka/src/js/desktop.js)。
// 消化率(%)を小数1桁で。予算が0以下なら出さない
function rateOf(actual, budget) {
if (budget <= 0) return null;
var n = actual * 1000;
if (!Number.isSafeInteger(n)) return null;
return roundDiv(n, budget) / 10;
}
予算が0のときに Infinity や NaN を出さず、null(空欄)にしているのも、帳票にそのまま載せるための工夫です。
なお原価のほうの parse は、String(value).replace(/,/g, '') で桁区切りのカンマも取り除いてから読んでいます。設定画面で手入力する「注意の線(%)」などに、カンマ付きの数が入っても読めるようにするためです。
まとめ
- kintone の数値は文字列で届く。
Number()に通す前に、整数部と小数部に分けて整数で持つ。 - 掛け算は整数どうし、割り算と四捨五入は
floor((2n + d) / 2d)で。 -
Number.isSafeIntegerで範囲を確かめ、超えたら黙らずに知らせる。 - 端数の決まり(四捨五入・切り捨て)はコメントに書く。
ソースと配布先
この記事のコードは、建設向けの無料の kintone プラグイン「見積書作成プラグイン」の一部です。
- ソース(MIT License):https://github.com/Genbacoe/kintone-plugins/tree/main/estimate/src
- 入れ方と設定の画面:https://genbakoe.com/kintone/estimate/
※ kintone はサイボウズ株式会社の登録商標です。このプラグインと記事は、サイボウズ株式会社の公式のものではありません。