scala.math.BigDecimal : 1.2 和 1.20 相等

scala.math.BigDecimal : 1.2 and 1.20 are equal

提问人:Saurabh 提问时间:11/8/2019 最后编辑:Saurabh 更新时间:9/21/2022 访问量:1957

问:

如何在将 Double 或 String 转换为 scala.math.BigDecimal 时保持精度和尾随零?

用例 - 在 JSON 消息中,属性的类型为 String,值为“1.20”。但是在 Scala 中读取此属性并将其转换为 BigDecimal 时,我丢失了精度并将其转换为 1.2

Scala REPL screenshot

scala 精度 bigdecimal

评论

3赞 Pedro Correia Luís 11/8/2019
您不会失去精度,但 1.2 与 1.20 相同,如果您有 1.21,它将保持精度。
6赞 Jörg W Mittag 11/8/2019
请将代码作为代码发布,而不是作为代码的照片。这是一个程序员的网站,摄影就在那边。我们希望复制、粘贴、读取、运行和调试您的代码,而不是批评它对颜色和透视的使用。
3赞 som-snytt 11/8/2019
此外,代码照片未着色。
2赞 Alexey Romanov 11/8/2019
@jwvh “如果要在转换回 String 表示形式时保持精度,则必须单独保存该信息。”不,你没有,它是 .BigDecimal

答:

2赞 som-snytt 11/8/2019 #1

我通常不做数字,但是:

scala> import java.math.MathContext
import java.math.MathContext

scala> val mc = new MathContext(2)
mc: java.math.MathContext = precision=2 roundingMode=HALF_UP

scala> BigDecimal("1.20", mc)
res0: scala.math.BigDecimal = 1.2

scala> BigDecimal("1.2345", mc)
res1: scala.math.BigDecimal = 1.2

scala> val mc = new MathContext(3)
mc: java.math.MathContext = precision=3 roundingMode=HALF_UP

scala> BigDecimal("1.2345", mc)
res2: scala.math.BigDecimal = 1.23

scala> BigDecimal("1.20", mc)
res3: scala.math.BigDecimal = 1.20

编辑:也,https://github.com/scala/scala/pull/6884

scala> res3 + BigDecimal("0.003")
res4: scala.math.BigDecimal = 1.20

scala> BigDecimal("1.2345", new MathContext(5)) + BigDecimal("0.003")
res5: scala.math.BigDecimal = 1.2375
5赞 Alexey Romanov 11/8/2019 #2

对于 ,与 完全相同,因此无法将它们转换为不同的 s。因为,您不会失去精度;你可以看到,因为而不是!但是 on 恰好被定义,因此数值相等的 s 是相等的,即使它们是可区分的。Double1.201.2BigDecimalStringres3: scala.math.BigDecimal = 1.20... = 1.2equalsscala.math.BigDecimalBigDecimal

如果你想避免这种情况,你可以使用 s for whichjava.math.BigDecimal

与 compareTo 不同,此方法认为两个 BigDecimal 对象仅在值和小数位数相等时才相等(因此,使用此方法进行比较时,2.0 不等于 2.00)。

就您的情况而言,将是错误的。res2.underlying == res3.underlying

当然,它的文档也指出

注意:如果将 BigDecimal 对象用作 SortedMap 中的键或 SortedSet 中的元素,则应小心,因为 BigDecimal 的自然顺序与 equals 不一致。有关详细信息,请参阅 Comparable、SortedMap 或 SortedSet。

这可能是 Scala 设计者决定采取不同行为的部分原因。

6赞 Andriy Plokhotnyuk 11/9/2019 #3

@Saurabh 真是个好问题!分享用例至关重要!

我想我的答案可以以最安全、最有效的方式解决它......简而言之,它是:

使用 jsoniter-scala 精确解析 BigDecimal 值。

任何数值类型的 JSON 字符串的编码/解码都可以按编解码器或按类字段定义。请看下面的代码:

将依赖项添加到:build.sbt

libraryDependencies ++= Seq(
  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core"   % "2.17.4",
  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.17.4" % Provided // required only in compile-time
)

定义数据结构,派生根结构的编解码器,解析响应正文并将其序列化回来:

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._

case class Response(
  amount: BigDecimal,
  @stringified price: BigDecimal)
    
implicit val codec: JsonValueCodec[Response] = JsonCodecMaker.make {
  CodecMakerConfig
    .withIsStringified(true) // switch it on to stringify all numeric and boolean values in this codec
    .withBigDecimalPrecision(34) // set a precision to round up to decimal128 format: java.math.MathContext.DECIMAL128.getPrecision
    .withBigDecimalScaleLimit(6178) // limit scale to fit the decimal128 format: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1
    .withBigDecimalDigitsLimit(308) // limit a number of mantissa digits to be parsed before rounding with the specified precision
}
  
val response = readFromArray("""{"amount":1000,"price":"1.20"}""".getBytes("UTF-8"))
val json = writeToArray(Response(amount = BigDecimal(1000), price = BigDecimal("1.20")))

将结果打印到控制台并进行验证:

println(response)
println(new String(json, "UTF-8"))

Response(1000,1.20)
{"amount":1000,"price":"1.20"}   

为什么所提出的方法是安全的?

井。。。解析 JSON 是一个雷区,尤其是当您将在此之后获得精确的值时。大多数 Scala 的 JSON 解析器都使用 Java 的构造函数来表示字符串,这具有复杂性(其中尾数中的数字数),并且不会将结果舍入到安全选项(默认情况下,该值用于 Scala 的构造函数和操作)。BigDecimalO(n^2)nMathContextMathContext.DECIMAL128BigDecimal

它为接受不受信任输入的系统引入了低带宽 DoS/DoW 攻击下的漏洞。下面是一个简单的示例,如何在 Scala REPL 中使用类路径中最流行的 Scala JSON 解析器的最新版本重现它:

...
Starting scala interpreter...
Welcome to Scala 2.12.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_222).
Type in expressions for evaluation. Or try :help.

scala> def timed[A](f: => A): A = { val t = System.currentTimeMillis; val r = f; println(s"Elapsed time (ms): ${System.currentTimeMillis - t}"); r } 
timed: [A](f: => A)A

scala> timed(io.circe.parser.decode[BigDecimal]("9" * 1000000))
Elapsed time (ms): 29192
res0: Either[io.circe.Error,BigDecimal] = Right(999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999...

scala> timed(io.circe.parser.decode[BigDecimal]("1e-100000000").right.get + 1)
Elapsed time (ms): 87185
res1: scala.math.BigDecimal = 1.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000...

对于当代 1Gbit 网络,收到 10 毫秒的 1M 位数字的恶意消息可能会在单个内核上产生 29 秒的 100% CPU 负载。超过 256 个内核可以在全带宽速率下有效地进行 DoS ed。最后一个表达式演示了如果 Scala 2.12.8 使用了后续 OR 操作,如何使用带有 13 字节数字的消息将 CPU 内核烧录 ~1.5 分钟。+-

而且,jsoniter-scala 负责处理 Scala 2.11.x、2.12.x、2.13.x 和 3.x 的所有这些情况。

为什么它是最有效的?

下面是图表,在解析 128 个小(最多 34 位尾数)值和中等(128 位尾数)值的数组期间,不同 JVM 上 Scala 的 JSON 解析器的吞吐量(每秒操作数,因此越大越好):BigDecimal

enter image description here

enter image description here

jsoniter-scala 中 BigDecimal 的解析例程

  • 对最多 36 位的小数字使用具有紧凑表示的值BigDecimal

  • 对 37 到 284 位的中型数字使用更高效的热回路

  • 切换到递归算法,该算法对于超过 285 位的值具有复杂性O(n^1.5)

此外,jsoniter-scala 将 JSON 直接从 UTF-8 字节解析和序列化到您的数据结构并返回,并且速度非常快,无需使用运行时反射、中间 AST、字符串或哈希映射,只需最少的分配和复制。请在此处查看 GeoJSON、Google Maps API、OpenRTB 和 Twitter API 针对不同数据类型的 115 个基准测试的结果以及真实消息示例。