【问题标题】:Scala Doubles, and PrecisionScala 双倍和精度
【发布时间】:2012-06-19 18:14:55
【问题描述】:

是否有可以截断或舍入 Double 的函数?在我的代码中的某一时刻,我想要一个像:1.23456789 这样的数字四舍五入到 1.23

【问题讨论】:

  • 查看所有答案后,我猜简短的答案是否定的? :)
  • @Gevorg 你让我发笑。我是 Scala 的新手,从其他数字繁重的语言开始,读到这个帖子我的下巴差点撞到地板上。对于编程语言来说,这是一种疯狂的状态。

标签: scala double truncate rounding


【解决方案1】:

你可以使用scala.math.BigDecimal:

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

还有许多其他rounding modes,遗憾的是目前没有很好的记录(尽管their Java equivalents are)。

【讨论】:

  • 很有可能,我会说。任何涉及网格或财务的事情都可能需要四舍五入和性能。
  • 我想有些人长时间调用笨重的库比简单的数学更容易理解。在这种情况下,我会推荐"%.2f".format(x).toDouble。只慢 2 倍,而且你只需要使用你已经知道的库。
  • @RexKerr,在这种情况下你没有四舍五入..只是截断。
  • @JoséLeal - 嗯? scala> "%.2f".format(0.714999999999).toDouble res13: Double = 0.71scala> "%.2f".format(0.715).toDouble res14: Double = 0.72.
  • @RexKerr 我更喜欢你的 string.format 方式,但在 locales s.a.我的(芬兰语),必须注意修复到 ROOT 语言环境。例如。 "%.2f".formatLocal(java.util.Locale.ROOT,x).toDouble 。由于语言环境,格式似乎使用“,”,而 toDouble 无法接受它并抛出 NumberFormatException。这当然是基于您的代码在哪里运行,而不是在哪里开发。
【解决方案2】:

这是另一个没有 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)

【讨论】:

  • roundAt2 应该是 def roundAt2(n: Double) = roundAt(2)(n) 否?
  • 这似乎为NaN 返回了不正确的结果,不是吗?
  • floor 的问题是truncateAt(1.23456789, 8) 将返回1.23456788roundAt(1.23456789, 8) 将返回1.23456789 的正确值
【解决方案3】:

由于还没有人提到% 运算符,所以来了。它只是截断,你不能依靠返回值来避免浮点数不准确,但有时它很方便:

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

【讨论】:

  • 虽然这是我自己的答案,但不建议这样做:@ryryguy 在另一个答案的评论中提到的同样的不准确问题也会影响这里。将 string.format 与 Java ROOT 语言环境一起使用(我会在那里评论)。
  • 如果您只需要渲染该值并且从不在后续操作中使用它,这是完美的。谢谢
  • 这里有件有趣的事:26.257391515826225 - 0.057391515826223094 = 26.200000000000003
【解决方案4】:

怎么样:

 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 不过并没有对其进行太多测试。此代码将保留 2 位小数。
【解决方案5】:

编辑:修复了@ryryguy 指出的问题。 (谢谢!)

如果您希望它更快,Kaito 有正确的想法。不过,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
  }
}

如果您在 Long 范围内(即 18 位数字),这大约快 10 倍,因此您可以在 10^18 和 10^-18 之间的任何地方舍入。

【讨论】:

  • 注意,乘以倒数并不可靠,因为它可能无法可靠地表示为双精度数:scala&gt; def r5(x:Double) = math.round(x*100000)*0.000001; r5(0.23515) ==> res12: Double = 0.023514999999999998。改为除以意义:math.round(x*100000)/100000.0
  • 用数组查找替换递归的p10 函数也可能很有用:数组会增加大约 200 字节的内存消耗,但每次调用可能会节省多次迭代。
【解决方案6】:

对于那些有兴趣的人,这里有一些建议的解决方案......

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

}

【讨论】:

  • 亲爱的 scalaCustom 没有四舍五入,它只是截断
  • hmm,OP 并非特定于舍入或截断; ...truncate or round a Double.
  • 但在我看来,比较截断函数与舍入函数的速度/执行时间是不够的。这就是为什么我要求您向读者澄清自定义功能只会截断的原因。您提到的截断/自定义功能可以进一步简化。 val doubleParts = 双倍。 toString.split(".") 现在获取 doubleParts.tail 的前两个字符并与字符串 "." 连接。和doubleParts. head 并解析为加倍。
  • 更新了,更好看?您的建议toString.split(".")doubleParts.head/tail 的建议也可能会受到额外的数组分配和字符串连接的影响。不过需要测试才能确定。
  • @OldGaurd01 您对数字截断/舍入函数“简化”的想法是使用String???在哪个宇宙中这是一个简化? Double -> String 和 String -> Double 的至少 2 次额外转换(每个部分可能 2 倍......)。
【解决方案7】:

你可以使用隐式类:

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

【讨论】:

  • 请说明此方法仅用于截断,不能正确舍入。
【解决方案8】:

使用 Scala f interpolator - https://docs.scala-lang.org/overviews/core/string-interpolation.html 实际上很容易处理

假设我们要四舍五入到小数点后 2 位:

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

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

【讨论】:

  • 如果 X 位小数是变量而不是预先确定的怎么办?我试过f"$y$xf"之类的东西,但没有bueno。
【解决方案9】:

最近,我遇到了类似的问题,我使用以下方法解决了它

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)

我使用了this SO 问题。对于 Float\Double 和implicit\explicit 选项,我有几个重载函数。请注意,在重载函数的情况下,您需要明确提及返回类型。

【讨论】:

  • 另外,你可以使用@rex-kerr 的方法来代替Math.pow
【解决方案10】:

在这个线程中这些是很好的答案。为了更好地展示差异,这里只是一个例子。我在工作期间把它放在这里的原因是数字必须不是一半:

    import org.apache.spark.sql.types._
    val values = List(1.2345,2.9998,3.4567,4.0099,5.1231)
    val df = values.toDF
    df.show()
    +------+
    | value|
    +------+
    |1.2345|
    |2.9998|
    |3.4567|
    |4.0099|
    |5.1231|
    +------+

    val df2 = df.withColumn("floor_val", floor(col("value"))).
    withColumn("dec_val", col("value").cast(DecimalType(26,2))).
    withColumn("floor2", (floor(col("value") * 100.0)/100.0).cast(DecimalType(26,2)))

    df2.show()
+------+---------+-------+------+
| value|floor_val|dec_val|floor2|
+------+---------+-------+------+
|1.2345|        1|   1.23|  1.23|
|2.9998|        2|   3.00|  2.99|
|3.4567|        3|   3.46|  3.45|
|4.0099|        4|   4.01|  4.00|
|5.1231|        5|   5.12|  5.12|
+------+---------+-------+------+

floor 函数下限到小于当前值的最大整数。 DecimalType 默认会启用HALF_UP 模式,而不仅仅是切割到您想要的精度。如果您想在不使用HALF_UP 模式的情况下切割到一定的精度,您可以使用上述解决方案(或使用scala.math.BigDecimal(您必须明确定义舍入模式)。

【讨论】:

    【解决方案11】:

    如果您关心性能,我不会使用 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)
    

    我将按照Kaito 的建议坚持数学运算。

    【讨论】:

      【解决方案12】:

      有点奇怪但很好。我使用 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
      }
      

      【讨论】:

        【解决方案13】:

        你可以这样做:Math.round(&lt;double precision value&gt; * 100.0) / 100.0 但是 Math.round 是最快的,但它在小数位数非常多(例如 round(1000.0d, 17))或大整数部分(例如 round(90080070060.1d, 9))的极端情况下会严重崩溃。

        使用 Bigdecimal 效率有点低,因为它将值转换为字符串,但更轻松: BigDecimal(&lt;value&gt;).setScale(&lt;places&gt;, RoundingMode.HALF_UP).doubleValue() 使用您对舍入模式的偏好。

        如果您很好奇并想详细了解为什么会发生这种情况,您可以阅读以下内容:

        【讨论】:

          【解决方案14】:

          由于问题特别指定了双精度数的舍入,这似乎比处理大整数或过多的字符串或数字运算要简单。

          "%.2f".format(0.714999999999).toDouble
          

          【讨论】:

            【解决方案15】:

            我认为以前的答案是:

            • 明显错误:例如,使用 math.floor 不适用于负值..
            • 不必要的复杂。

            这是基于@kaito 回答的建议(我还不能发表评论):

            def truncateAt(x: Double, p: Int): Double = {
                val s = math.pow(10, p)
                (x * s).toInt / s
            }
            

            toInt 适用于正值和负值。

            【讨论】:

              猜你喜欢
              • 2016-12-19
              • 2017-01-23
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-02-23
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多