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
对于Double
,1.20
和1.2
是完全一样的,所以你不能把它们转换成不同的BigDecimal
。对于 String
,您不会失去精度;您可以看到,因为 res3: scala.math.BigDecimal = 1.20
而不是 ... = 1.2
!但是 scala.math.BigDecimal
上的 equals
恰好被定义为在数值上相等的 BigDecimal
是相等的,即使它们是可区分的。
如果你想避免这种情况,你可以使用 java.math.BigDecimal
s
对于你的情况,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= 的真实消息样本].
如何在将 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
对于Double
,1.20
和1.2
是完全一样的,所以你不能把它们转换成不同的BigDecimal
。对于 String
,您不会失去精度;您可以看到,因为 res3: scala.math.BigDecimal = 1.20
而不是 ... = 1.2
!但是 scala.math.BigDecimal
上的 equals
恰好被定义为在数值上相等的 BigDecimal
是相等的,即使它们是可区分的。
如果你想避免这种情况,你可以使用 java.math.BigDecimal
s
对于你的情况,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= 的真实消息样本].