在将Double或String转换为scala.math.BigDecimal时,如何保持精度和尾随零?
用例-在JSON消息中,属性的类型为String,且值为“ 1.20”。但是在Scala中读取此属性并将其转换为BigDecimal时,我失去了精度,并将其转换为1.2
@Saurabh真是个好问题!共享用例至关重要!
我认为我的答案可以用最安全,最有效的方式来解决...简而言之,它是:
使用jsoniter-scala来精确解析BigDecimal
值。
可以通过每个编解码器或每个类字段定义对任何数字类型的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
)
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")))
3)将结果打印到控制台并进行验证:
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
是尾数中的数字)并且不会将结果四舍五入到{{1 }}(默认情况下,MathContext
值用于Scala的MathContext.DECIMAL128
构造函数和操作中的值)。
它为接受不受信任的输入的系统引入了低带宽DoS / DoW攻击下的漏洞。下面是一个简单的示例,如何在Scala REPL中使用类路径中针对Scala的最受欢迎的JSON解析器的最新版本进行复制:
BigDecimal
对于当代的1Gbit网络,在10ms内收到1M数字的恶意消息可能会在单个内核上产生29秒的100%CPU负载。在全带宽速率下,可以有效地对256个以上的内核进行DoS处理。最后一个表达式演示了如何在Scala 2.12.8中使用后续的...
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...
或+
操作时,使用带有13字节数字的消息将CPU内核刻录约1.5分钟。
而且,jsoniter-scala会处理Scala 2.11.x,2.12.x和2.13.x的所有这些情况。
为什么效率最高?
以下是在解析128个小(最多34位尾数)值和一个中号(带有一个小数)的数组时,不同JVM上Scala的JSON解析器的吞吐量(每秒的操作数,越大越好)的图表。 -
的128位尾数):
The parsing routine for BigDecimal
(在jsoniter-scala中):
使用BigDecimal
值进行紧凑表示,以表示最多36位的小数字
对具有37到284位数字的中号使用更有效的热循环
切换到递归算法,该算法对于具有超过285位数字的值具有BigDecimal
复杂度
此外,jsoniter-scala直接将JSON从UTF-8字节解析并序列化到您的数据结构,然后再返回,并疯狂地快速执行,而无需使用运行时反射,中间AST,字符串或哈希映射,只需最少的分配和复制。请查看here的针对不同数据类型的115个基准测试的结果以及GeoJSON,Google Maps API,OpenRTB和Twitter API的真实消息示例。
,对于Double
,1.20
与1.2
完全相同,因此不能将它们转换为不同的BigDecimal
。对于String
,您不会失去精度;您会看到这是因为res3: scala.math.BigDecimal = 1.20
而不是... = 1.2
!但是equals
上的scala.math.BigDecimal
恰好被定义为即使它们是可区分的,数值上相等的BigDecimal
也相等。
如果要避免这种情况,可以使用java.math.BigDecimal
个
对于您而言,res2.underlying == res3.underlying
将为假。
当然,其文档中也注明
注意:如果将BigDecimal对象用作SortedMap的键或SortedSet中的元素,则应格外小心,因为BigDecimal的自然顺序与不一致。有关更多信息,请参见Comparable,SortedMap或SortedSet。
这可能是Scala设计师决定不同行为的原因的一部分。
,我通常不做数字,但是:
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