scala.math.BigDecimal:1.2和1.20相等

Sau*_*abh 4 precision scala bigdecimal

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

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

Scala REPL屏幕截图

And*_*yuk 6

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

我认为我的答案可以以最安全,最有效的方式解决它……简而言之,它是:

使用jsoniter-scalaBigDecimal精确解析值。

可以通过每个编解码器或每个类字段定义对任何数字类型的JSON字符串进行编码/解码。请参见下面的代码:

1)将依赖项添加到您的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
)
Run Code Online (Sandbox Code Playgroud)

2)定义数据结构,为根结构派生一个编解码器,解析响应主体并将其序列化回:

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(false) // 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")))
Run Code Online (Sandbox Code Playgroud)

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

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

Response(1000,1.20)
{"amount":1000,"price":"1.20"}   
Run Code Online (Sandbox Code Playgroud)

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

好吧... JSON解析是一个雷区,尤其是当您要BigDecimal在那之后获得精确值时。大多数用于Scala的JSON解析器都是使用Java的构造函数进行字符串表示的,这种字符串表示法很O(n^2)复杂(其中n尾数是数字),并且不会将结果四舍五入到安全选项MathContext(默认情况下,该MathContext.DECIMAL128值用于Scala的BigDecimal构造函数中,操作)。

它为接受不受信任的输入的系统引入了低带宽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...
Run Code Online (Sandbox Code Playgroud)

对于当代的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的所有这些情况。

为什么它是最有效的?

以下是在解析128个较小(最多34位尾数)值和一个中等(128个位数)值的数组时,不同JVM上Scala的JSON解析器的吞吐量(每秒的操作数,越大越好)的图表。尾数)值BigDecimal

在此处输入图片说明

在此处输入图片说明

BigDecimaljsoniter-scala中的解析例程

  • 使用BigDecimal紧凑表示的值表示不超过36位的小数字

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

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

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


Ale*_*nov 5

对于Double1.20与完全相同1.2,因此您不能将它们转换为不同BigDecimal的。对于String,您不会失去精度;您可以看到,因为res3: scala.math.BigDecimal = 1.20不是... = 1.2!但是equalson scala.math.BigDecimal恰好被定义BigDecimal为即使它们是可区分的,它们在数值上也相等。

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

与compareTo不同,此方法仅在两个BigDecimal对象的值和比例相等时才认为它们相等(由此方法比较时,2.0不等于2.00)。

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

当然,其文档也说明

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

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