intra-martのIM-BISで文字列アイテムを数値アイテムのように使用する際の注意点

2020年入社したM.Sと申します。
本年度からプログラミングを始めたため 若輩者ですが、私の記事が皆さんのお役にたてればと思います。

今回は私が業務でミスしてしまった箇所を紹介します。

なぜ文字列アイテムを使うのか

IM-BISで数値アイテムを使用すると.00が表示されずに省略されてしまいます。 .00を表示させる仕様であったため、数値アイテムを使用せず、文字列アイテムを使って数値表示をすることになりました。

何が起きたか

IM-BISのアクション設定において、文字列アイテムの入力イベントで小数点の付与と指定以下の小数点の切り捨てを行う以下のスクリプトを実装しました。

let comma = (Math.floor(100 * str) / 100).toFixed(2);

ところが、例として72445.40と入力すると72445.39になってしまい、入力内容が変わってしてしまう問題が発生しました。

デバッグしてみたところ、Math.floor(100 * str)の計算結果が7244539.999999999になってしまうことが分かりました。

何故こうなるかを調べたところ以下の記事で挙げられている通り、浮動小数点数の計算時に丸め誤差が発生していたことが原因でした。
https://qiita.com/Chinats/items/e2647ca7900dfe7835a8

どう対処したか

intra-martのクライアントサイドAPI(ImDecimalFormatter)を使用してフォーマット処理を行うこととしました。

前準備としてFormaアイテムの入力値を取得する関数と反映する関数を実装しておきます。 アイテムの値取得・値反映については以下に記載があります。
https://www.intra-mart.jp/document/library/bis/public/bis_specification/texts/spec/csjs_script.html

// 文字列 入力値を取得する関数
function getItemValue(type, name) {
    let data = {}, _type = type || 'product_72_textbox', item = formaItems[_type];
    return item.getItemData[name]();
};

// 文字列 入力値を反映する関数
function setItemValue(type, name, value) {
    let data = {}, _type = type || 'product_72_textbox', item = formaItems[_type];
    data[name] = value;
    item.setItemData[name]({
        data : data
    });
};

次に、ImDecimalFormatterを用いて数値文字列のフォーマットする処理を実装します。
以下はImDecimalFormatterのAPIドキュメントです。
https://www.intra-mart.jp/apidoc/iap/jsdoc/symbols/ImDecimalFormatter.html

function format(name){
    let formatter = ImDecimalFormatter.getAccountInstance();
    // 小数部分の最大桁数を設定
    formatter.setMaximumFractionDigits(2);
    // 小数部分の最小桁数を設定
    formatter.setMinimumFractionDigits(2);

     let str = getItemValue('product_72_textbox', name);

    // ImBigDecimal(任意精度のスケールなしの整数値)に変換
    formatter.parseToBigDecimal(str,function(data, textStatus, jqXHR){
        if(data.error) {
            console.log(data);
            setItemValue('product_72_textbox', name, '');  
            return;
        }    

        // 小数点以下の桁数を丸める
        let bd = data.data;
        // 一つ目めの引数は、求める小数点以下の桁数を指定
        // 二つ目の引数は、列挙型定数のRoundingMode.DOWNを指定し切り捨て
        bd = bd.setScale(2,ImRoundingMode.DOWN);

        // 区切り文字付きでフォーマット
        formatter.format(bd, function(data, textStatus, jqXHR){
            // フォーマットエラーの場合はブランクを設定
            if(data.error) {
                console.log(data);
                setItemValue('product_72_textbox', name, '');
                return;
            }
            setItemValue('product_72_textbox', name, data.data);
        });
    }); 
}

これで小数を入力した場合でも、誤差なく入力した通りの値を表示することができました。

おわりに

JavaScriptを学んでいく中で計算と日付周りは罠が多いとよく目にしましたが、 実際に望まない挙動が起こるとその意味を実感しました。 まだまだ理解が浅いため学習を続け、罠に注意して開発を行っていきたいと思います。