IT

愛すべき「バグ」1: 平均値計算の悲劇

07_001_01
システムインテグレーション部のNです。
 
業務で出会った愛すべき「バグ」を紹介します。
お客様より「帳票の平均値部分におかしな値が出る」と相談されて事象が発生している帳票を眺めてみると、平均が「100000000」ぐらいの値のはずが「1.04」と表示されていた。確かに変だ。
 
Javaソースの該当部分を眺めてみると、下記メソッドで平均を算出して文字列化していたが
コメントがない… orz
 

// 平均算出(小数第三位四捨五入)
public static String calcAverage(long sumOfValue, long numOfValue) {
    double average = ((double)sumOfValue) / ((double)numOfValue);
    String averageStr = "" + average;
    
    String[] number = averageStr.split("\\.");
    
    if (number.length == 1) {
        return number[0];
    }
    
    if (number[1].length() < 3) {
        return number[0] + "." + number[1];
    }
    
    if (Integer.parseInt("" + (number[1].charAt(3))) < 5) {
        return number[0] + "." + number[1].substring(0, 2);
    }
    
    double avg = Double.parseDouble(number[0] + "." + number[1].substring(0, 2));
    avg += 0.01;
    return "" + avg;
}

 
コメントを付けてみると、

// 平均算出(小数第三位四捨五入)
public static String calcAverage(long sumOfValue, long numOfValue) {
    // 平均計算(浮動小数点に変換後、データ値の和÷データの数)
    double average = ((double)sumOfValue) / ((double)numOfValue);
    // 文字列に変換
    String averageStr = "" + average;
    
    // 平均値を整数部分と小数部分に分割
    String[] number = averageStr.split("\\.");
    
    // 小数部分の文字列が無い場合は整数部分を返却値とする
    if (number.length == 1) {
        return number[0];
    }
    
    // 小数値部分の文字列長が3未満の場合は整数部分と小数部分を連結して返却値とする
    if (number[1].length() < 3) {
        return number[0] + "." + number[1];
    }
    
    // 小数値部分の文字列の3文字目を整数と解釈した時に5未満の場合は
    // 整数部分と小数部分の先頭2文字を連結して返却値とする
    if (Integer.parseInt("" + (number[1].charAt(2))) < 5) {
        return number[0] + "." + number[1].substring(0, 2);
    }
    
    // その他の場合は、整数部分と小数部分の先頭2文字を連結した文字列を数値に変換
    double avg = Double.parseDouble(number[0] + "." + number[1].substring(0, 2));
    // 同値に0.01加算
    avg += 0.01;
    // 同値を文字列に変換し返却値とする
    return "" + avg;
}

浮動小数点型(double)を使うことによる誤差に目をつぶれば(苦笑1)
さらに文字列に変換して四捨五入していることに目をつぶれば(苦笑2)
「まぁ、動くっちゃ動くはずだが…」
 
と思い、ためしに動かしてみると

System.out.println(calcAverage(1001 * 7, 7));
System.out.println(calcAverage(100000001 * 7, 7));

 
出力値は、

1001.0
1.00

となり事象が再現した。
 
コードに少しデバッグ用の出力を追加し実行してみると、

// 平均算出(小数第三位四捨五入)
public static String calcAverage(long sumOfValue, long numOfValue) {
    double average = ((double)sumOfValue) / ((double)numOfValue);
    String averageStr = "" + average;
    System.out.println("averageStr = " + averageStr);                  // ←追加
    
    String[] number = averageStr.split("\\.");
    
    if (number.length == 1) {
        return number[0];
    }
    
    if (number[1].length() < 3) {
        return number[0] + "." + number[1];
    }
    
    if (Integer.parseInt("" + (number[1].charAt(3))) < 5) {
        return number[0] + "." + number[1].substring(0, 2);
    }
    
    double avg = Double.parseDouble(number[0] + "." + number[1].substring(0, 2));
    avg += 0.01;
    return "" + avg;
}

System.out.println(calcAverage(1001 * 7, 7));
System.out.println(calcAverage(100000001 * 7, 7));

 
出力値は、

averageStr = 1001.0
1001.0
averageStr = 1.00000001E8
1.00

あーーーーー!!!

averageStr = 1.00000001E8 ←ここ

平均値が大きな値となり文字列化すると指数表現となることが原因でした。
したがってその後の処理は文字列化すると整数部が”1″、小数部が”00000001E8″
小数部が3文字以上で3文字目が”0″のため、繰上げ処理は行われず
 
“1” + “.” + “00” → “1.00”
 
となっていました。
 
ひとしきり盛り上がったあと、賢者モードとなり、
誤差発生も抑止できる”java.math”パッケージを使い修正しました。