2024/10/06

意外と難しかった四捨五入の作成

前回 は Round という関数は四捨五入ではなかったという話をしました。アプリ開発では必要となるシーンがありますので、ないのなら作ってしまいましょう。

ということで、今回は四捨五入する処理の作成を行います。そのサンプルを紹介するだけのつもりだったのですが、想定外の障害に出くわし、思いのほか難航しまた。そこで、その問題点も含めてまとめておきます。


LotusScript の場合

まずは、簡単に解決できる LotusScript での処理です。Round という関数が存在するので RoundOff 関数としました。

引数は Round 関数と同じで、丸めたい値 vdNum と 丸める桁 viPlaces です。

Function RoundOff(Byval vdNum As Double, Byval viPlaces As Integer) As Double
   Dim dDigit As Double
   Dim d As Double

   '丸めたい桁を1の位にする係数を作成
   dDigit = 10 ^ viPlaces

   '丸めたい桁を1の位にする
   d = vdNum * dDigit

   '四捨五入(0.5 を加算して切り捨て)
   d = Int(d + 0.5)

   '桁を戻す
   RoundOff = d / dDigit
End Function

コメントを記述しているので解説は不要かと思います。ポイントは四捨五入の代わりに 0.5 を足して、少数を切り捨てている点です。


式言語で問題発生

続いては@関数の四捨五入を作成してみます。@Round に倣って、丸めたい値 xNumber と 丸める桁 xFactor を用意して演算させます。

式の構成は上記 LotusScript の関数と揃えています。ただ、丸める桁の値の持たせ方が違いますので、乗除記号が逆転している点に注意してください。

   xNumber := 0.25;
   xFactor := 0.1;

   REM {丸めたい桁を1の位にする};
   xTmp := xNumber / xFactor;

   REM {四捨五入(0.5 を加算して切り捨て)};
   xTmp := @Integer(xTmp + 0.5);

   REM {桁を戻す};
   xTmp * xFactor

式ができ上がって検証しているときに事件が発生しました。なぜか、0.350 の場合に切り捨てが発生してしまいました !?

xNumber の値 0.249 0.250 0.251 0.349 0.350 0.351
実行結果 0.2 0.3 0.3 0.3 0.3 0.4

式をいくら見直しても原因がわかりません。そこで、式の途中経過を順に確認しました(数値フィールドの計算結果として確認)。

検証した式 結果(0.250) 結果(0.350)
xNumber / xFactor 2.5 3.5
xNumber / xFactor + 0.5 3 4
@Integer(xNumber / xFactor + 0.5) 3 3

@Integer に原因があることが明白になりました。そこで、Google 先生に聞いてみたところ以下のリンクが見つかりました。

@Integer 関数で小数を扱った場合の動作について

リンク内の事例でも @Integer の結果が 1 少なくなる場合があることが示されています。今回もそれが原因なのでしょう。

回避策を探して Workaround を確認すると、@Round を使えと書いてあります。@Round の四捨五入が使えないから @Integer を頼ったのに...。頭が混乱してきました。


このページの一番最後にこの現象の原因解説のリンクがありました。

浮動小数点演算と丸め誤差について

細かな話は分かりませんが、少数の値は 2 進数で表すと有限桁数で表せない場合があり、それに起因する誤差によるものだそうです。


式言語の四捨五入

少数の値とその誤差が問題原因なのであれば、できる限り少数を使わなければ抑制できるかもしれません。今回の事例では、丸めたい値が少数なのは仕方がないとして、それ以外の少数利用を避けてみようと考えました。

具体的には次の部分、誤差が混入しうる値(要は小数値)を演算に使用している点に着目しました。

   xTmp := xNumber / xFactor;


対策は次の通りです。

丸める桁の値の持たせ方を LotusScript の Round と合わせて、次のように記述しました。これで、元の値以外は整数(もしくは整数部)となります。

   xNumber := 0.350;
   xPlaces := 1;

   REM {丸めたい桁を1の位にする係数を作成};
   xDigit := @Power(10; xPlaces);

   REM {丸めたい桁を1の位にする};
   xTmp := xNumber * xDigit;

   REM {四捨五入(0.5 を加算して切り捨て)};
   xTmp := @Integer(xTmp + 0.5);

   REM {桁を戻す};
   xTmp / xDigit

サポート情報にある Workaround では誤差を @Round でごまかす方法を取っていましたが、誤差が混入しにくくすることで回避できないか挑戦してみるという方法ですね。


結果は 0.250 であっても 0.350 であっても正しく四捨五入できました。念のため、さまざまな値や桁数を変えてテストしましたが、問題はなさそうでした。


まとめ

今回は、四捨五入の処理を@関数と LotusScript で作成してみました。@関数では想定外のトラブルに見舞われ、思いのほか時間がかかってしまいました。ただ、問題の原因がわかれば、症状が出にくいように対策すれば回避できる事例になればと思い、紹介しました。


原因となった『@Integer 関数で小数を扱った場合の動作について』なのですが、この現象がいつから発生しているのかは記載がありません。原因からするとはるか昔から延々と受け継がれているような気がします。そして、対応のステータスは Deferred となっています。要は対応する気がないのだと理解しました。

ノーツは 30 年以上前のプログラムがそのまま動作する互換性が高い製品です。これがノーツをビジネスで利用する上で、重要な利点のひとつだと思っています。

だからと言って、バグまで互換性維持する必要はないですよね...。せっかくの利点が悪しき文化にならないように祈ります。


0 件のコメント:

コメントを投稿