0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kintone の JavaScript で金額を計算するとき、小数の誤差を出さない(整数 × 10^-s で持つ)

0
Posted at

やりたいこと

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 プラグイン「見積書作成プラグイン」の一部です。

※ kintone はサイボウズ株式会社の登録商標です。このプラグインと記事は、サイボウズ株式会社の公式のものではありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?