scala.math.BigDecimal: 1,2 и 1,20 равны - PullRequest
4 голосов
/ 08 ноября 2019

Как сохранить точность и конечный ноль при преобразовании Double или String в scala.math.BigDecimal?

Вариант использования - В сообщении JSON атрибут имеет тип String и имеет значение «1.20». Но читая этот атрибут в Scala и преобразуя его в BigDecimal, я теряю точность, и он преобразуется в 1,2

Scala REPL screenshot

Ответы [ 3 ]

6 голосов
/ 09 ноября 2019

@ 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"}   

Почему предложенный подход безопасен?

Ну ... Разбор JSON - это минное поле , особенно если после этого у вас будут точные значения BigDecimal. Большинство анализаторов JSON для Scala делают это, используя конструктор Java для строкового представления, который имеет O(n^2) сложность (где n - это число цифр в мантиссе) и не округляет результаты до безопасного параметра MathContext (по умолчаниюЗначение MathContext.DECIMAL128 используется для этого в BigDecimal конструкторах и операциях Scala).

Внедряет уязвимости при DoS / DoW-атаках с низкой пропускной способностью для систем, которые принимают ненадежный ввод. Ниже приведен простой пример того, как его можно воспроизвести в Scala REPL с последней версией самого популярного парсера JSON для Scala в classpath:

...
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...

Для современных сетей 1 Гбит 10 мс получать вредоносное сообщение с1-значное число может дать 29 секунд 100% загрузки процессора на одно ядро. Более 256 ядер могут быть эффективно DoS-ed на полной скорости полосы пропускания. Последнее выражение демонстрирует, как записать ядро ​​ЦП в течение ~ 1,5 минут, используя сообщение с 13-байтовым номером, если последующие операции + или - были использованы с Scala 2.12.8.

И, jsoniter-scala позаботьтесь обо всех этих случаях для Scala 2.11.x, 2.12.x и 2.13.x.

Почему это наиболее эффективно?

Ниже приведеныдиаграммы с пропускной способностью (операций в секунду, поэтому чем больше, тем лучше) результаты синтаксических анализаторов JSON для Scala для разных JVM при разборе массива из 128 малых (до 34-значных мантисс) значений и среднего (со 128-значной мантиссой)значение BigDecimal соответственно:

enter image description here

enter image description here

Процедура синтаксического анализа дляBigDecimal в jsoniter-scala:

  • использует BigDecimal значения с компактным представлением для небольших чисел до 36 цифр

  • использует более эффективные горячие циклы для средних чисел, которые имеют от 37 до 284 цифр

  • swжаждет рекурсивного алгоритма, который имеет O(n^1.5) сложность для значений, которые имеют более 285 цифр

Более того, jsoniter-scala анализирует и сериализует JSON напрямую из байтов UTF-8 в ваши структуры данныхи обратно, и делает это безумно быстро без использования отражения во время выполнения, промежуточных AST, строк или хэш-карт, с минимальными выделениями и копированием. См. здесь результаты 115 тестов для различных типов данных и реальных примеров сообщений для GeoJSON, Google Maps API, OpenRTB и Twitter API.

5 голосов
/ 08 ноября 2019

Для Double, 1.20 точно так же, как 1.2, поэтому вы не можете конвертировать их в различные BigDecimal с. Для String вы не теряете точность;Вы можете видеть это, потому что res3: scala.math.BigDecimal = 1.20, а не ... = 1.2! Но equals в scala.math.BigDecimal определено так, что численно равные BigDecimal s равны, даже если они различимы.

Если вы хотите избежать этого, вы можете использовать java.math.BigDecimal s, для которых

В отличие от compareTo, этот метод считает два объекта BigDecimal равными только в том случае, если они равны по значению и масштабу (таким образом, 2,0 не равно 2,00 при сравнении этим методом).

Для вашего случая res2.underlying == res3.underlying будет ложным.

Конечно, его документация также гласит:

Примечание: следует соблюдать осторожность, если объекты BigDecimalиспользуются в качестве ключей в SortedMap или элементов в SortedSet, поскольку естественное упорядочение BigDecimal не соответствует равным . Посмотрите Comparable, SortedMap или SortedSet для получения дополнительной информации.

, что, вероятно, является одной из причин, по которой дизайнеры Scala выбрали другое поведение.

2 голосов
/ 08 ноября 2019

Обычно я не делаю цифры, но:

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
...