scala.math.BigDecimal : 1.2 和 1.20 相等

scala.math.BigDecimal : 1.2 and 1.20 are equal

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

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

我通常不做数字,但是:

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

对于Double1.201.2是完全一样的,所以你不能把它们转换成不同的BigDecimal。对于 String,您不会失去精度;您可以看到,因为 res3: scala.math.BigDecimal = 1.20 而不是 ... = 1.2!但是 scala.math.BigDecimal 上的 equals 恰好被定义为在数值上相等的 BigDecimal 是相等的,即使它们是可区分的。

如果你想避免这种情况,你可以使用 java.math.BigDecimals

Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method).

对于你的情况,res2.underlying == res3.underlying 将是错误的。

当然,它的文档也说明了

Note: care should be exercised if BigDecimal objects are used as keys in a SortedMap or elements in a SortedSet since BigDecimal's natural ordering is inconsistent with equals. See Comparable, SortedMap or SortedSet for more information.

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

@Saurabh 多么好的问题!分享用例至关重要!

我认为我的回答可以以最安全有效的方式解决它...简而言之:

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

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

将依赖项添加到您的 build.sbt:

libraryDependencies ++= Seq(
  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core"   % "2.0.1",
  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.0.1" % 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"}   

为什么建议的方法是安全的?

嗯... Parsing of JSON is a minefield,尤其是当您之后要获得精确的 BigDecimal 值时。 Scala 的大多数 JSON 解析器使用 Java 的构造函数来表示具有 O(n^2) 复杂性的字符串表示(其中 n 是尾数中的数字)并且不将结果舍入到 MathContext 的安全选项(默认情况下,MathContext.DECIMAL128 值用于 Scala 的 BigDecimal 构造函数和操作)。

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

...
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 攻击。最后一个表达式演示了如果后续 +- 操作与 Scala 2.12.8 一起使用,如何使用具有 13 字节数字的消息将 CPU 核心燃烧约 1.5 分钟。

而且,jsoniter-scala 会处理 Scala 2.11.x、2.12.x 和 2.13.x.

的所有这些情况

为什么效率最高?

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

The parsing routine for BigDecimal 在 jsoniter-scala 中:

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

  • 对 37 到 284 位数字的中位数使用更高效的热循环

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

此外,jsoniter-scala 直接从 UTF-8 字节解析和序列化 JSON 到您的数据结构并返回,并且在不使用 运行 时间反射、中间 AST 的情况下速度非常快、字符串或哈希映射,具有最少的分配和复制。请查看 here 115 个不同数据类型的基准测试结果和 GeoJSON、Google 地图 API、OpenRTB 和 Twitter [=97= 的真实消息样本].