使用Scala BigDecimal 乘法和除法的倒数



我们遇到了一个问题,我们将一个大十进制除以另一个大十进制,并试图通过取除数的倒数并乘以除数来验证它。

例如,基础数学会告诉我们:a / b = a * (1 / b)

更具体地说,这是我们正在努力做的事情:6.0075 / 0.89 = 6.0075 * (1 / 0.89) = 6.75

使用 BigDecimal 来增加精度,我们可以得到第一部分:

scala> BigDecimal(6.0075) / BigDecimal(0.89)
res1: scala.math.BigDecimal = 6.75

但不是第二个:

scala> BigDecimal(6.0075) * (BigDecimal(1.00) / BigDecimal(0.89))
res2: scala.math.BigDecimal = 6.749999999999999999999999999999999

进一步挖掘,我们看到BigDecimal(1.00) / BigDecimal(0.89)是一个不精确的结果,并且Scala的BigDecimal 包装器围绕Java的数学。BigDecimal 指定精度为 35 位小数,舍入模式为 HALF_EVEN。不幸的是,1.00 / 0.89有无限个小数位,所以直接使用java的BigDecimal 也无济于事。

还有其他人遇到过这个问题吗?除了尝试重写表达式1.00/0.89之外,在 Scala 或 Java 中是否有办法处理这个问题?

谢谢!

无法回避这样一个事实,即无限回归的有限表示将具有舍入误差。(另请参阅:浮点数学坏了吗?

我看到了两种可能的解决方案,它们都不是微不足道的。

  1. 使用有理数类来表示这些值。 每个值在整个积分中都有一个分子和一个分母。您需要跟踪 GCD,但您实际需要潜水两个数字的唯一时间是在向屏幕(或页面)发送值时。
  2. 检查每个值是否存在舍入误差(这可能最好通过值的String表示形式的正则表达式模式匹配来完成)并相应地进行调整。

不幸的是,没有 100% 准确的方法来表示具有无限十进制扩展的数字,例如1.00/0.89(或1/3)使用浮点数。这是试图模拟无限属性的有限机器的限制。

但是,如果您只关心一定位数的准确性(如果您使用货币,则为 2),那么您可以使用 Java 的MathContextsetScale方法:

scala> import java.math.{RoundingMode => RM, MathContext => MC}
import java.math.{RoundingMode=>RM, MathContext=>MC}
scala> val mc = new MC(100, RM.HALF_UP)
mc: java.math.MathContext = precision=100 roundingMode=HALF_UP
scala> val a = BigDecimal("6.0075")
a: scala.math.BigDecimal = 6.0075
scala> val b = BigDecimal("1")
b: scala.math.BigDecimal = 1
scala> val c = BigDecimal("0.89")
c: scala.math.BigDecimal = 0.89
scala> val d = (a * (b(mc) / c)).setScale(2)
d: scala.math.BigDecimal = 6.75

请注意,我使用的是双精度的字符串版本,因为它们是更精确的表示(因为即使0.89也无法完美地用浮点数表示)。

编辑

受到@jwvh对有理数的评论的启发,我把这个基本的Rational课放在一起:

scala> :pa
// Entering paste mode (ctrl-D to finish)
object Rational
{
def apply(n: BigInt, d: BigInt): Rational =
{
val neg_mod = if (d < BigInt(0)) BigInt(-1) else BigInt(1)
val (n_mod, d_mod) = (neg_mod * n, neg_mod * d)
val gcd_val = gcd(n_mod, d_mod)
new Rational(n_mod / gcd_val, d_mod / gcd_val)
}
def gcd(a: BigInt, b: BigInt): BigInt = if (b == BigInt(0)) a else gcd(b, a % b)
}
class Rational(val n: BigInt, val d: BigInt)
{
override def toString: String = if (n == BigInt(0)) "0" else if (d == BigInt(1)) s"$n" else s"$n/$d"
def toDouble: Double = n.toDouble / d.toDouble
def *(that: Rational): Rational = Rational(n * that.n, d * that.d)
def /(that: Rational): Rational = Rational(n * that.d, d * that.n)
def +(that: Rational): Rational = Rational(n * that.d + that.n * d, d * that.d)
def -(that: Rational): Rational = this + (-that)
def unary_- = Rational(-n, d)
}
// Exiting paste mode, now interpreting.
defined object Rational
defined class Rational
scala> val a = Rational(60075, 10000)
a: Rational = 2403/400
scala> val b = Rational(1, 1)
b: Rational = 1
scala> val c = Rational(89, 100)
c: Rational = 89/100
scala> a * (b / c)
res0: Rational = 27/4
scala> (a * (b / c)).toDouble
res1: Double = 6.75

您要验证a / b是否等于a * (1 / b)

问题:(1 / b)的值可能不准确。

我的解决方法:
u = 0.00000000000000000000000000000000001(你说它适用于小数点后 35 位)
然后验证a / b是否至少是a * ((1 / b) - u)并且最多是a * ((1 / b) + u)
如果a为负数,那么您将需要交换这些比较的符号。 这不是一个完美的解决方法,但我希望它会做到。

最新更新