Scala gra podwójna i precyzja

118

Czy istnieje funkcja, która może obciąć lub zaokrąglić Double? W pewnym momencie mojego kodu chciałbym, aby liczba taka jak: 1.23456789została zaokrąglona do1.23

richsoni
źródło
9
Po przejrzeniu wszystkich odpowiedzi myślę, że krótka odpowiedź brzmi: nie? :)
Marsellus Wallace
4
@Gevorg Rozśmieszyłeś mnie. Jestem nowy w Scali z innych języków z licznymi liczbami i moja szczęka prawie uderzyła o podłogę czytając ten wątek. To jest szalony stan rzeczy dla języka programowania.
ely

Odpowiedzi:

150

Możesz użyć scala.math.BigDecimal:

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

Istnieje wiele innych trybów zaokrąglania , które niestety nie są obecnie dobrze udokumentowane (chociaż ich odpowiedniki w Javie są ).

Travis Brown
źródło
5
Powiedziałbym, że całkiem prawdopodobne. Wszystko, co dotyczy siatek lub finansów, może wymagać zaokrąglenia, a także wydajności.
Rex Kerr
28
Przypuszczam, że są ludzie, dla których długie wywołanie niezgrabnej biblioteki jest bardziej zrozumiałe niż prosta matematyka. Poleciłbym "%.2f".format(x).toDoublew takim przypadku. Tylko 2x wolniej i musisz korzystać tylko z biblioteki, którą już znasz.
Rex Kerr
6
@RexKerr, w tym przypadku nie zaokrąglasz ... po prostu skracasz.
José Leal
16
@ JoséLeal - co? scala> "%.2f".format(0.714999999999).toDouble res13: Double = 0.71ale scala> "%.2f".format(0.715).toDouble res14: Double = 0.72.
Rex Kerr
5
@RexKerr Wolę twój string.format sposób, ale w ustawieniach regionalnych sa moje (fińskie), należy zachować ostrożność, aby naprawić ustawienie regionalne ROOT. Np. „% .2f” .formatLocal (java.util.Locale.ROOT, x) .toDouble. Wygląda na to, że format używa „,” ze względu na ustawienia regionalne, podczas gdy toDouble nie jest w stanie go przyjąć i zgłasza wyjątek NumberFormatException. To oczywiście zależy od tego, gdzie jest uruchamiany twój kod , a nie gdzie jest rozwijany.
akauppi
77

Oto inne rozwiązanie bez BigDecimals

Ścięty:

(math floor 1.23456789 * 100) / 100

Okrągły:

(math rint 1.23456789 * 100) / 100

Lub dla dowolnego podwójnego n i precyzji p:

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

Podobnie można zrobić z funkcją zaokrąglania, tym razem używając curry:

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

który jest bardziej przydatny do ponownego wykorzystania, np. przy zaokrąglaniu kwot pieniędzy można zastosować:

def roundAt2(n: Double) = roundAt(2)(n)
Kaito
źródło
8
roundAt2 powinno być def roundAt2 (n: Double) = roundAt (2) (n) no?
C4stor,
wydaje się, że zwraca nieprawidłowy wynik dla NaN, prawda?
jangorecki
Problem floorpolega na tym, truncateAt(1.23456789, 8)że zwróci, 1.23456788a roundAt(1.23456789, 8)zwróci prawidłową wartość1.23456789
Todor Kolev
35

Ponieważ nikt jeszcze nie wspomniał o %operatorze, nadchodzi. Wykonuje tylko obcinanie i nie można polegać na zwracanej wartości, aby nie mieć niedokładności zmiennoprzecinkowych, ale czasami jest to przydatne:

scala> 1.23456789 - (1.23456789 % 0.01)
res4: Double = 1.23
akauppi
źródło
2
Nie polecałbym tego, chociaż to moja własna odpowiedź: te same problemy z niedokładnością, o których wspomniał @ryryguy w komentarzu innej odpowiedzi, mają również wpływ tutaj. Użyj string.format z ustawieniami regionalnymi Java ROOT (skomentuję to tam).
akauppi
jest to idealne rozwiązanie, jeśli potrzebujesz tylko wyrenderować wartość i nigdy nie używać jej w kolejnych operacjach. dzięki
Alexander Arendar
3
tu jest coś zabawnego: 26.257391515826225 - 0.057391515826223094 = 26.200000000000003
kubudi
12

Co powiesz na :

 val value = 1.4142135623730951

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

//4 decimal places
println((value * 10000).round / 10000.toDouble)
niebieskie niebo
źródło
całkiem czyste rozwiązanie. Tutaj jest moje do obcinania: ((1.949 * 1000).toInt - ((1.949 * 1000).toInt % 10)) / 1000.toDoublenie testowałem tego za dużo. Ten kod zrobiłby 2 miejsca po przecinku.
robert
7

Edycja: naprawiono problem wskazany przez @ryryguy. (Dzięki!)

Jeśli chcesz, żeby było szybko, Kaito ma dobry pomysł. math.powjest jednak powolny. W przypadku każdego standardowego zastosowania lepiej jest skorzystać z funkcji rekurencyjnej:

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

Jest to około 10 razy szybsze, jeśli znajdujesz się w zakresie Long(tj. 18 cyfr), więc możesz zaokrąglić w dowolnym miejscu między 10 ^ 18 a 10 ^ -18.

Rex Kerr
źródło
3
Uważaj, mnożenie przez odwrotność nie działa niezawodnie, ponieważ może nie być wiarygodnie reprezentowane jako podwójne: scala> def r5(x:Double) = math.round(x*100000)*0.000001; r5(0.23515)==> res12: Double = 0.023514999999999998. Zamiast tego podziel według znaczenia:math.round(x*100000)/100000.0
ryryguy
Przydatne może być również zastąpienie p10funkcji rekurencyjnej wyszukiwaniem tablicy: tablica zwiększy zużycie pamięci o około 200 bajtów, ale prawdopodobnie zapisze kilka iteracji na wywołanie.
Levi Ramsey
4

Możesz używać niejawnych klas:

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))
}
Mitrakov Artem
źródło
1
Określ, że ta metoda służy tylko do obcinania, a nie do prawidłowego zaokrąglania.
bobo32
4

Dla zainteresowanych kilka razy proponowane rozwiązania ...

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

Truncation
Scala custom Formatter: Elapsed Time: 3 

Obcięcie jest najszybsze, a po nim następuje BigDecimal. Należy pamiętać, że te testy zostały przeprowadzone z uruchomieniem norma scala, bez użycia narzędzi do testów porównawczych.

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())
      }
    }
  }

}
cevaris
źródło
1
Drogi Scala, Custom nie kończy się, to tylko skracanie
Ravinder Payal
hmm, OP nie był specyficzny dla zaokrąglania lub obcięcia; ...truncate or round a Double.
cevaris
Jednak moim zdaniem porównywanie szybkości / czasu wykonywania funkcji obcinania z funkcjami zaokrąglającymi jest niewystarczające. Dlatego poprosiłem cię o wyjaśnienie czytelnikowi, że funkcja niestandardowa jest tylko obcięta. Wspomniana przez Ciebie funkcja obcinania / niestandardowa może zostać jeszcze bardziej uproszczona. val doubleParts = double. toString.split (".") Teraz pobierz pierwsze dwa znaki doubleParts.taili połącz je z łańcuchami "." i doubleParts. headprzeanalizuj, aby podwoić.
Ravinder Payal
1
zaktualizowane, lepiej wyglądać? również twoja sugestia toString.split(".")i doubleParts.head/tailsugestia mogą ucierpieć z powodu dodatkowej alokacji tablicy i konkatenacji ciągów. musiałby jednak przetestować, aby mieć pewność.
cevaris
3

Niedawno stanąłem przed podobnym problemem i rozwiązałem go stosując następujące podejście

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)

Użyłem tego problemu SO. Mam kilka przeciążonych funkcji zarówno dla opcji Float \ Double, jak i niejawnych \ jawnych. Zauważ, że w przypadku przeciążonych funkcji musisz wyraźnie wspomnieć o zwracanym typie.

Khalid Saifullah
źródło
Możesz także użyć podejścia @ rex-kerr dla władzy zamiast Math.pow
Khalid
1

Nie użyłbym BigDecimal, jeśli zależy ci na wydajności. BigDecimal konwertuje liczby na ciąg, a następnie analizuje je ponownie:

  /** 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)

Mam zamiar trzymać się manipulacji matematycznych, jak zasugerował Kaito .

bigonazzi
źródło
0

Trochę dziwne, ale miłe. Używam String, a nie 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
}
jafed
źródło
0

Możesz to zrobić: Math.round(<double precision value> * 100.0) / 100.0 Ale Math.round jest najszybsza, ale źle się psuje w przypadkach narożnych z bardzo dużą liczbą miejsc dziesiętnych (np. Zaokrąglenie (1000.0d, 17)) lub dużą częścią całkowitą (np. Round (90080070060.1d, 9) ).

Użyj Bigdecimal, jest to nieco nieefektywne, ponieważ konwertuje wartości na łańcuchy, ale bardziej przypomina: BigDecimal(<value>).setScale(<places>, RoundingMode.HALF_UP).doubleValue() użyj preferencji trybu zaokrąglania.

Jeśli jesteś ciekawy i chcesz dowiedzieć się więcej szczegółów, dlaczego tak się dzieje, możesz przeczytać to: wprowadź opis obrazu tutaj

frostcs
źródło