Scala Doubles и точность


118

Есть ли функция, которая может обрезать или округлять Double? В какой-то момент в моем коде я хотел бы, чтобы число вроде: 1.23456789было округлено до1.23


9
Посмотрев на все ответы, я предполагаю, что короткий ответ - нет? :)
Марселлус Уоллес

4
@Gevorg Ты меня рассмешил. Я новичок в Scala из других языков с большим количеством чисел, и моя челюсть почти упала на пол, читая эту ветку. Это безумное положение вещей для языка программирования.
ely

Ответы:


150

Вы можете использовать scala.math.BigDecimal:

BigDecimal(1.23456789).setScale(2, BigDecimal.RoundingMode.HALF_UP).toDouble

Существует ряд других режимов округления , которые, к сожалению, в настоящее время не очень хорошо документированы (хотя их эквиваленты в Java есть ).


5
Я бы сказал, что вполне вероятно. Все, что связано с сетями или финансами, может потребовать округления, а также производительности.
Рекс Керр

28
Полагаю, есть люди, для которых долгий вызов неуклюжей библиотеки более понятен, чем простая математика. Я рекомендую "%.2f".format(x).toDoubleв таком случае. Только в 2 раза медленнее, и вам нужно использовать только уже знакомую библиотеку.
Рекс Керр

6
@RexKerr, в этом случае вы не округляете .. просто усекаете.
Хосе Леаль,

16
@ ХосеЛил - А? scala> "%.2f".format(0.714999999999).toDouble res13: Double = 0.71но scala> "%.2f".format(0.715).toDouble res14: Double = 0.72.
Рекс Керр

5
@RexKerr Я предпочитаю использовать ваш вариант string.format, но в других (финских) языках нужно позаботиться о том, чтобы исправить локаль ROOT. Например, "% .2f" .formatLocal (java.util.Locale.ROOT, x) .toDouble. Похоже, что формат использует ',' из-за локали, тогда как toDouble не может принять его и выдает исключение NumberFormatException. Это, конечно , основывается на том, где ваш код в настоящее время работать не там , где это развито.
akauppi

77

Вот еще одно решение без BigDecimals

Обрезание:

(math floor 1.23456789 * 100) / 100

Круглый:

(math rint 1.23456789 * 100) / 100

Или для любого двойного n и точности p:

def truncateAt(n: Double, p: Int): Double = { val s = math pow (10, p); (math floor n * s) / s }

То же самое можно сделать и с функцией округления, на этот раз с использованием каррирования:

def roundAt(p: Int)(n: Double): Double = { val s = math pow (10, p); (math round n * s) / s }

который является более многоразовым, например, при округлении денежных сумм можно использовать следующее:

def roundAt2(n: Double) = roundAt(2)(n)

8
roundAt2 должно быть def roundAt2 (n: Double) = roundAt (2) (n) no?
C4stor 08

похоже, это возвращает неверный результат NaN, не так ли?
jangorecki

проблема в floorтом, что truncateAt(1.23456789, 8)вернет, 1.23456788а roundAt(1.23456789, 8)вернет правильное значение1.23456789
Тодор Колев

35

Так как никто еще не упомянул %оператора, вот оно. Он выполняет только усечение, и вы не можете полагаться на возвращаемое значение, чтобы не иметь неточностей с плавающей запятой, но иногда это удобно:

scala> 1.23456789 - (1.23456789 % 0.01)
res4: Double = 1.23

2
Не рекомендую это, хотя это мой собственный ответ: здесь также сказываются те же проблемы с неточностью, которые упоминались @ryryguy в другом комментарии ответа. Используйте string.format с локалью Java ROOT (я прокомментирую это там).
akauppi

это идеально, если вам просто нужно отобразить значение и никогда не использовать его в последующих операциях. спасибо
Александр Арендар 03

3
вот что-то смешное: 26.257391515826225 - 0.057391515826223094 = 26.200000000000003
kubudi 09

12

Как насчет :

 val value = 1.4142135623730951

//3 decimal places
println((value * 1000).round / 1000.toDouble)

//4 decimal places
println((value * 10000).round / 10000.toDouble)

довольно чистое решение. Вот мой для усечения: ((1.949 * 1000).toInt - ((1.949 * 1000).toInt % 10)) / 1000.toDoubleхотя и не слишком много тестировал. В этом коде будет два десятичных знака.
Роберт

7

Изменить: исправлена ​​проблема, указанная @ryryguy. (Спасибо!)

Если вы хотите, чтобы все было быстро, у Кайто есть правильная идея. math.powхотя медленный. Для любого стандартного использования вам лучше использовать рекурсивную функцию:

def trunc(x: Double, n: Int) = {
  def p10(n: Int, pow: Long = 10): Long = if (n==0) pow else p10(n-1,pow*10)
  if (n < 0) {
    val m = p10(-n).toDouble
    math.round(x/m) * m
  }
  else {
    val m = p10(n).toDouble
    math.round(x*m) / m
  }
}

Это примерно в 10 раз быстрее, если вы находитесь в пределах диапазона Long(т.е. 18 цифр), поэтому вы можете округлить до любого значения от 10 ^ 18 до 10 ^ -18.


3
Будьте осторожны, умножение на обратное не работает надежно, потому что оно не может быть надежно представлено как двойное: scala> def r5(x:Double) = math.round(x*100000)*0.000001; r5(0.23515)==> res12: Double = 0.023514999999999998. Вместо этого разделите по значимости:math.round(x*100000)/100000.0
ryguy

Также может быть полезно заменить рекурсивную p10функцию поиском по массиву: массив увеличит потребление памяти примерно на 200 байт, но, вероятно, сэкономит несколько итераций на вызов.
Леви Рэмси

4

Вы можете использовать неявные классы:

import scala.math._

object ExtNumber extends App {
  implicit class ExtendedDouble(n: Double) {
    def rounded(x: Int) = {
      val w = pow(10, x)
      (n * w).toLong.toDouble / w
    }
  }

  // usage
  val a = 1.23456789
  println(a.rounded(2))
}

1
Пожалуйста, укажите, что этот метод предназначен только для усечения, а не для правильного округления.
bobo32

4

Для тех, кому интересно, вот несколько вариантов предлагаемых решений ...

Rounding
Java Formatter: Elapsed Time: 105
Scala Formatter: Elapsed Time: 167
BigDecimal Formatter: Elapsed Time: 27

Truncation
Scala custom Formatter: Elapsed Time: 3 

Усечение является самым быстрым, за ним следует BigDecimal. Имейте в виду, что этот тест был выполнен при выполнении norma scala, без использования каких-либо инструментов тестирования.

object TestFormatters {

  val r = scala.util.Random

  def textFormatter(x: Double) = new java.text.DecimalFormat("0.##").format(x)

  def scalaFormatter(x: Double) = "$pi%1.2f".format(x)

  def bigDecimalFormatter(x: Double) = BigDecimal(x).setScale(2, BigDecimal.RoundingMode.HALF_UP).toDouble

  def scalaCustom(x: Double) = {
    val roundBy = 2
    val w = math.pow(10, roundBy)
    (x * w).toLong.toDouble / w
  }

  def timed(f: => Unit) = {
    val start = System.currentTimeMillis()
    f
    val end = System.currentTimeMillis()
    println("Elapsed Time: " + (end - start))
  }

  def main(args: Array[String]): Unit = {

    print("Java Formatter: ")
    val iters = 10000
    timed {
      (0 until iters) foreach { _ =>
        textFormatter(r.nextDouble())
      }
    }

    print("Scala Formatter: ")
    timed {
      (0 until iters) foreach { _ =>
        scalaFormatter(r.nextDouble())
      }
    }

    print("BigDecimal Formatter: ")
    timed {
      (0 until iters) foreach { _ =>
        bigDecimalFormatter(r.nextDouble())
      }
    }

    print("Scala custom Formatter (truncation): ")
    timed {
      (0 until iters) foreach { _ =>
        scalaCustom(r.nextDouble())
      }
    }
  }

}

1
Уважаемый scalaCustom не округление, это просто усечения
Ravinder Payal

хм, OP не предназначен для округления или усечения; ...truncate or round a Double,
cevaris 08

Но, на мой взгляд, сравнение скорости / времени выполнения функции усечения с функциями округления неадекватно. Вот почему я просил вас объяснить читателю, что пользовательская функция только усекает. Упомянутая вами функция truncate / custom может быть дополнительно упрощена. val doubleParts = двойной. toString.split (".") Теперь получите первые два символа doubleParts.tailи объедините их со строками "." и doubleParts. headи разобрать на удвоение.
Ravinder Payal

1
обновили, лучше посмотрите? также ваше предложение toString.split(".")и doubleParts.head/tailпредложение могут пострадать от дополнительного выделения массива плюс конкатенации строк. для уверенности потребуется тестирование.
cevaris

3

Недавно я столкнулся с подобной проблемой и решил ее, используя следующий подход

def round(value: Either[Double, Float], places: Int) = {
  if (places < 0) 0
  else {
    val factor = Math.pow(10, places)
    value match {
      case Left(d) => (Math.round(d * factor) / factor)
      case Right(f) => (Math.round(f * factor) / factor)
    }
  }
}

def round(value: Double): Double = round(Left(value), 0)
def round(value: Double, places: Int): Double = round(Left(value), places)
def round(value: Float): Double = round(Right(value), 0)
def round(value: Float, places: Int): Double = round(Right(value), places)

Я использовал эту проблему SO. У меня есть несколько перегруженных функций для параметров Float \ Double и implicit \ explicit. Обратите внимание, что вам нужно явно указать тип возвращаемого значения в случае перегруженных функций.


Кроме того, вы можете использовать подход @rex-kerr для мощности вместо Math.pow
Халид Сайфулла

2

На самом деле с этим очень легко справиться с помощью fинтерполятора Scala - https://docs.scala-lang.org/overviews/core/string-interpolation.html

Предположим, мы хотим округлить до двух знаков после запятой:

scala> val sum = 1 + 1/4D + 1/7D + 1/10D + 1/13D
sum: Double = 1.5697802197802198

scala> println(f"$sum%1.2f")
1.57

1

Я бы не стал использовать BigDecimal, если вам важна производительность. BigDecimal преобразует числа в строку, а затем снова анализирует ее:

  /** Constructs a `BigDecimal` using the decimal text representation of `Double` value `d`, rounding if necessary. */
  def decimal(d: Double, mc: MathContext): BigDecimal = new BigDecimal(new BigDec(java.lang.Double.toString(d), mc), mc)

Я собираюсь придерживаться математических манипуляций, как предложил Кайто .


0

Немного странно, но приятно. Я использую String, а не BigDecimal

def round(x: Double)(p: Int): Double = {
    var A = x.toString().split('.')
    (A(0) + "." + A(1).substring(0, if (p > A(1).length()) A(1).length() else p)).toDouble
}

0

Вы можете сделать: Math.round(<double precision value> * 100.0) / 100.0 Но Math.round самый быстрый, но он плохо работает в угловых случаях с очень большим количеством десятичных знаков (например, round (1000.0d, 17)) или большой целой частью (например, round (90080070060.1d, 9) ).

Используйте Bigdecimal, это немного неэффективно, так как преобразует значения в строку, но более средневековое: BigDecimal(<value>).setScale(<places>, RoundingMode.HALF_UP).doubleValue() используйте свой предпочтительный режим округления.

Если вам интересно и вы хотите узнать более подробно, почему это происходит, вы можете прочитать это: введите описание изображения здесь

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.