3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Java/Kotlin:BigDecimalの割り算(除算)で割り切れない場合に有効数字の桁数を指定して商を算出

Last updated at Posted at 2018-07-07

割り切れる場合は、割り切れるところまで計算してほしい。
でも割り切れない場合は、常に同じ有効数字桁数で商を求めてほしい。
電卓のように割り算の計算結果を表示するアプリで必要になると思う。
ネットで良い方法を見つけられなかったので、自分で考えました(`・ω・´)

(2018/07/16 追記)
実現させたいことを明示するため、下に使用例をGIFで追加しました。
ちなみに、この画像は私が開発したCalcLeafという電卓アプリで撮ったものです。

このように答えが無限小数であっても表示可能な範囲内で適切に表示させることが目的です。

Java の場合

出力例もいくつか載せておきます。
ここでは省略しますが、得られた答えをDecimalFormatでフォーマットしてやると表示が良い感じになります。

Division.java
import java.math.BigDecimal;
import java.math.RoundingMode;

public class Division {
	
	public static final int SIGNIFICANT_DIGITS = 3; // 有効数字桁数
	
	public static void main(String[] args) {
		try {
			System.out.println(divided("10000", "3")); // 3330
			System.out.println(divided("1000", "3"));  // 333
			System.out.println(divided("100", "3"));   // 33.3
			System.out.println(divided("10", "3"));    // 3.33
			System.out.println(divided("1", "3"));     // 0.333
			System.out.println(divided("1", "30"));    // 0.0333
			System.out.println(divided("1", "300"));   // 0.00333
			System.out.println(divided("1", "3000"));  // 0.000333
			System.out.println(divided("1", "30000")); // 0.0000333
		} catch(NumberFormatException nfe) {
			System.out.println("Error: Not a number.");
		} catch(ArithmeticException ae) {
			System.out.println("Error: Impossible to calculate.");
		} catch(Exception e) {
			System.out.println("Error: Unexpected.");
		}
	}

	public static String divided(String dividend, String divisor) throws Exception {
		BigDecimal bdDividend = new BigDecimal(dividend);
		BigDecimal bdDivisor = new BigDecimal(divisor);
		BigDecimal answer = null;
		
		try {
			// 割り切れたらそのまま割る
			answer = bdDividend.divide(bdDivisor);
		} catch(ArithmeticException arithmeticException) {
			// 割り切れない場合は有効なscaleを算出して計算(この例では丸め方は四捨五入とする)
			answer = bdDividend.divide(bdDivisor, getSignificantScale(bdDividend, bdDivisor), RoundingMode.HALF_UP);
		}
		return answer.stripTrailingZeros().toPlainString();
	}
	
	public static int getSignificantScale(BigDecimal dividend, BigDecimal divisor) {
		// 絶対値にして小数点以下の末尾のゼロを除く
		BigDecimal bd1 = dividend.abs().stripTrailingZeros();
		BigDecimal bd2 = divisor.abs().stripTrailingZeros();
		// 自然数に揃えてプレイン文字列化する
		int decimalDigits = bd1.scale() > bd2.scale() ? bd1.scale() : bd2.scale();
		if (decimalDigits < 0) decimalDigits = 0;
		String s1 = bd1.scaleByPowerOfTen(decimalDigits).toPlainString();
		String s2 = bd2.scaleByPowerOfTen(decimalDigits).toPlainString();
		// 有効なscaleを算出して返す
		return SIGNIFICANT_DIGITS - (s1.length() - s2.length()) - (s1.compareTo(s2) >= 0 ? 1 : 0);
	}
}

Kotlin の場合

Try kotlinにそのままコピペして試せます。
decimalDigitsを求めるところはletを使って1行にしてvalにもできるけど、なんか微妙なのでやめました。

Division.kt
import java.math.BigDecimal
import java.math.RoundingMode

const val SIGNIFICANT_DIGITS: Int = 3 // 有効数字桁数

fun main(args: Array<String>) {
    try {
    	println(divided("10000", "3")) // 3330
    	println(divided("1000", "3"))  // 333
    	println(divided("100", "3"))   // 33.3
    	println(divided("10", "3"))    // 3.33
    	println(divided("1", "3"))     // 0.333
    	println(divided("1", "30"))    // 0.0333
    	println(divided("1", "300"))   // 0.00333
    	println(divided("1", "3000"))  // 0.000333
    	println(divided("1", "30000")) // 0.0000333
    } catch(nfe: NumberFormatException) {
        println("Error: Not a number.")
    } catch(ae: ArithmeticException) {
        println("Error: Impossible to calculate.")
    } catch(e: Exception) {
        println("Error: Unexpected.")
    }
}

fun divided(dividend: String, divisor: String): String {
    val bdDividend = BigDecimal(dividend)
    val bdDivisor = BigDecimal(divisor)
    val answer = try {
        // 割り切れたらそのまま割る
        bdDividend.divide(bdDivisor)
    } catch(ae: ArithmeticException) {
        // 割り切れない場合は有効なscaleを算出して計算(この例では丸め方は四捨五入とする)
        bdDividend.divide(bdDivisor, getSignificantScale(bdDividend, bdDivisor), RoundingMode.HALF_UP)
    }
    return answer.stripTrailingZeros().toPlainString()
}

fun getSignificantScale(dividend: BigDecimal, divisor: BigDecimal): Int {
    // 絶対値にして小数点以下の末尾のゼロを除く
    val bd1 = dividend.abs().stripTrailingZeros()
    val bd2 = divisor.abs().stripTrailingZeros()
    // 自然数に揃えてプレイン文字列化する
    var decimalDigits = if (bd1.scale() > bd2.scale()) bd1.scale() else bd2.scale()
    if (decimalDigits < 0) decimalDigits = 0
    val s1 = bd1.scaleByPowerOfTen(decimalDigits).toPlainString()
    val s2 = bd2.scaleByPowerOfTen(decimalDigits).toPlainString()
    // 有効なscaleを算出して返す
    return SIGNIFICANT_DIGITS - (s1.length - s2.length) - (if (s1 >= s2) 1 else 0)
}

自己責任でご利用ください

上記のコードは、ご自身でテストをした上でお好きなように使っていただいて構いません。
私が実施したテストコードは割愛しますgithubにアップしています(kaizen_nagoyaさんからのコメントを受けて公開しました)が、割る数と割られる数の大小関係・小数点の位置・正負・指数表記か否かなどで50パターンくらい検証して要件をクリアしました。
ただし、私1人でやっていたことなので客観性に欠けるという点は否めません(´・ω・`)
というわけで、レビュー・再検証してくださる方は大歓迎です!
また、同じことをやっているライブラリや参考サイトなどの情報をお持ちの方がいましたら、教えていただけましたら大変嬉しいです!
(今思ったけど数学辞典に載ってるかも。持ってないけど。)

計算結果の表示を考えて調整してもいい

getSignificantScaleは、私の開発した電卓アプリCalcLeafで実際に使われていますが、実は少しだけ実装が違う点があります。
それは以下のように「有効なscaleの算出結果がマイナスだったら0を返す」という点です。

Sample.java
int significantScale = SIGNIFICANT_DIGITS - (s1.length() - s2.length()) - (s1.compareTo(s2) >= 0 ? 1 : 0);
return significantScale < 0 ? 0 : significantScale;

電卓の場合、例えば1,000,000,000,000÷3の答えが333,333,333,330になると気持ち悪いので、CalcLeafでは仕様として調整を入れています。

感想

JavaのAPIで標準的にできればいいのにと思った。
Javaの中の人が見てくれたら検討してくれないかな。

3
4
5

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
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?