Scala Doubles et précision

118

Existe-t-il une fonction qui peut tronquer ou arrondir un double? À un moment donné dans mon code, je voudrais un nombre comme: 1.23456789être arrondi à1.23

Richsoni
la source
9
Après avoir examiné toutes les réponses, je suppose que la réponse courte est non? :)
Marsellus Wallace
4
@Gevorg Vous m'avez fait rire. Je suis nouveau sur Scala à partir d'autres langages à forte densité numérique, et ma mâchoire a presque touché le sol en lisant ce fil. C'est un état de choses insensé pour un langage de programmation.
ely

Réponses:

150

Vous pouvez utiliser scala.math.BigDecimal:

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

Il existe un certain nombre d'autres modes d'arrondi , qui ne sont malheureusement pas très bien documentés à l'heure actuelle (bien que leurs équivalents Java le soient ).

Travis Brown
la source
5
Très probable, je dirais. Tout ce qui concerne les grilles ou la finance peut nécessiter des arrondis et également des performances.
Rex Kerr
28
Je suppose qu'il y a des gens pour qui un long appel à une bibliothèque maladroite est plus compréhensible que de simples mathématiques. Je recommanderais "%.2f".format(x).toDoubledans ce cas. Seulement 2x plus lentement, et vous n'avez qu'à utiliser une bibliothèque que vous connaissez déjà.
Rex Kerr
6
@RexKerr, vous n'êtes pas arrondi dans ce cas ... simplement tronqué.
José Leal
16
@ JoséLeal - Hein? scala> "%.2f".format(0.714999999999).toDouble res13: Double = 0.71mais scala> "%.2f".format(0.715).toDouble res14: Double = 0.72.
Rex Kerr
5
@RexKerr Je préfère votre manière string.format, mais dans les locales sa mienne (finnois), il faut prendre soin de corriger la locale ROOT. Par exemple, "% .2f" .formatLocal (java.util.Locale.ROOT, x) .toDouble. Il semble que le format utilise ',' à cause de la locale alors que toDouble n'est pas capable de le prendre et lève une NumberFormatException. Cela dépend bien sûr de l'endroit où votre code est exécuté , pas de l'endroit où il a été développé.
akauppi
77

Voici une autre solution sans BigDecimals

Tronquer:

(math floor 1.23456789 * 100) / 100

Rond:

(math rint 1.23456789 * 100) / 100

Ou pour tout double n et précision p:

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

La même chose peut être faite pour la fonction d'arrondi, cette fois en utilisant le curry:

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

qui est plus réutilisable, par exemple pour arrondir des montants d'argent, les éléments suivants peuvent être utilisés:

def roundAt2(n: Double) = roundAt(2)(n)
Kaito
la source
8
roundAt2 doit être def roundAt2 (n: Double) = roundAt (2) (n) no?
C4stor
cela semble retourner un résultat incorrect pour NaN, n'est-ce pas?
jangorecki
le problème floorest quetruncateAt(1.23456789, 8) retournera 1.23456788tandis que roundAt(1.23456789, 8)retournera la valeur correcte de1.23456789
Todor Kolev
35

Puisque personne n'a encore mentionné l' %opérateur, voici. Cela ne fait que la troncature et vous ne pouvez pas vous fier à la valeur de retour pour ne pas avoir d'inexactitudes en virgule flottante, mais parfois c'est pratique:

scala> 1.23456789 - (1.23456789 % 0.01)
res4: Double = 1.23
Akauppi
la source
2
Je ne le recommanderais pas, même si c'est ma propre réponse: les mêmes problèmes d'inexactitude que ceux mentionnés par @ryryguy dans le commentaire d'une autre réponse affectent également ici. Utilisez string.format avec la locale Java ROOT (je vais commenter là-dessus).
akauppi
c'est parfait si vous avez juste besoin de rendre la valeur et de ne jamais l'utiliser dans les opérations suivantes. merci
Alexander Arendar
3
voici quelque chose de drôle: 26.257391515826225 - 0.057391515826223094 = 26.200000000000003
kubudi
12

Que diriez-vous :

 val value = 1.4142135623730951

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

//4 decimal places
println((value * 10000).round / 10000.toDouble)
ciel bleu
la source
solution assez propre. Voici le mien pour la troncature: ((1.949 * 1000).toInt - ((1.949 * 1000).toInt % 10)) / 1000.toDoubleje ne l'ai pas trop testé. Ce code ferait 2 décimales.
robert
7

Edit: correction du problème signalé par @ryryguy. (Merci!)

Si vous voulez que ce soit rapide, Kaito a la bonne idée. math.powest lent, cependant. Pour toute utilisation standard, il vaut mieux utiliser une fonction récursive:

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

C'est environ 10 fois plus rapide si vous êtes dans la plage de Long (c'est-à-dire 18 chiffres), vous pouvez donc arrondir n'importe où entre 10 ^ 18 et 10 ^ -18.

Rex Kerr
la source
3
Attention, la multiplication par la réciproque ne fonctionne pas de manière fiable, car elle peut ne pas être représentable de manière fiable comme un double: scala> def r5(x:Double) = math.round(x*100000)*0.000001; r5(0.23515)==> res12: Double = 0.023514999999999998. Divisez plutôt par la signification:math.round(x*100000)/100000.0
ryryguy
Il peut également être utile de remplacer la p10fonction récursive par une recherche de tableau: le tableau augmentera la consommation de mémoire d'environ 200 octets mais économisera probablement plusieurs itérations par appel.
Levi Ramsey
4

Vous pouvez utiliser des classes implicites:

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))
}
Artem de Mitrakov
la source
1
Veuillez spécifier que cette méthode sert uniquement à tronquer et non à arrondir correctement.
bobo32
4

Pour ceux qui sont intéressés, voici quelques fois les solutions proposées ...

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

Truncation
Scala custom Formatter: Elapsed Time: 3 

La troncature est la plus rapide, suivie de BigDecimal. Gardez à l'esprit que ces tests ont été effectués en exécutant une exécution norma scala, sans utiliser d'outils d'analyse comparative.

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
la source
1
Cher scalaCustom n'est pas d'arrondi, il est juste tronqué
Ravinder Payal
hmm, OP n'était pas spécifique à l'arrondi ou à la troncature; ...truncate or round a Double.
cevaris
Mais à mon avis, comparer la vitesse / le temps d'exécution de la fonction de troncature avec les fonctions d'arrondi est inadéquat. C'est pourquoi je vous ai demandé de préciser au lecteur que la fonctionnalité personnalisée ne fait que tronquer. Et la fonction tronquée / personnalisée mentionnée par vous peut être simplifiée davantage. val doubleParts = double. toString.split (".") Maintenant, récupérez les deux premiers caractères de doubleParts.tailet concattez avec les chaînes "." et doubleParts. headet analyser pour doubler.
Ravinder Payal
1
mis à jour, une meilleure apparence? votre suggestion toString.split(".")et doubleParts.head/tailsuggestion peuvent également souffrir d'une allocation supplémentaire de tableau et d'une concaténation de chaînes. aurait besoin de tester pour en être sûr.
cevaris
3

Récemment, j'ai rencontré un problème similaire et je l'ai résolu en utilisant l'approche suivante

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)

J'ai utilisé ce problème SO. J'ai quelques fonctions surchargées pour les options Float \ Double et implicites \ explicites. Notez que vous devez mentionner explicitement le type de retour en cas de fonctions surchargées.

Khalid Saifullah
la source
Aussi, vous pouvez utiliser l'approche de @ rex-kerr pour le pouvoir au lieu de Math.pow
Khalid Saifullah
1

Je n'utiliserais pas BigDecimal si vous vous souciez des performances. BigDecimal convertit les nombres en chaîne, puis les analyse à nouveau:

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

Je vais m'en tenir aux manipulations mathématiques comme Kaito l'a suggéré.

bigonazzi
la source
0

Un peu étrange mais sympa. J'utilise String et non 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
la source
0

Vous pouvez faire: Math.round(<double precision value> * 100.0) / 100.0 Mais Math.round est le plus rapide mais il se décompose mal dans les cas d'angle avec un nombre très élevé de décimales (par exemple, round (1000.0d, 17)) ou une grande partie entière (par exemple, round (90080070060.1d, 9) ).

Utilisez Bigdecimal, c'est un peu inefficace car il convertit les valeurs en chaîne mais plus fiable: BigDecimal(<value>).setScale(<places>, RoundingMode.HALF_UP).doubleValue() utilisez votre préférence pour le mode Arrondi.

Si vous êtes curieux et que vous voulez savoir plus en détail pourquoi cela se produit, vous pouvez lire ceci: entrez la description de l'image ici

gelées
la source