Scala浮点数和精度

144

是否有可以截断或四舍五入Double类型的函数? 在我的代码的某个地方,我希望像 1.23456789 这样的数字被舍入为 1.23


12
看完所有的回答,我猜短答案是不行? :) - Marsellus Wallace
5
@Gevorg让我笑了。我是从其他数值密集型语言转入Scala的新手,阅读这个线程时,我的下巴几乎掉到地上了。对于一门编程语言来说,这是一个疯狂的情况。 - ely
1
如果我们从末尾开始,不应该是1.24吗? - Sergey Bushmanov
15个回答

185

5
我觉得很有可能。任何涉及栅格或金融的事情都可能需要取整并且需要高效执行。 - Rex Kerr
31
我猜有些人觉得冗长的库函数比简单的数学公式更易于理解。在这种情况下,我建议使用"%.2f".format(x).toDouble。它只比原来慢两倍,并且你只需要使用一个你已经知道的库。 - Rex Kerr
7
@RexKerr,在这种情况下,你没有四舍五入..只是简单的截断。 - José Leal
19
@JoséLeal - 嗯?scala> "%.2f".format(0.714999999999).toDouble的结果是res13: Double = 0.71,但scala> "%.2f".format(0.715).toDouble的结果是res14: Double = 0.72。请问您需要解释什么吗? - Rex Kerr
5
我更喜欢您的string.format方法,但在像我的语言环境(芬兰语)这样的地方,必须小心地固定为ROOT语言环境。例如:"%.2f".formatLocal(java.util.Locale.ROOT,x).toDouble。似乎format使用“,”是因为语言环境不同,而toDouble无法接受它并抛出NumberFormatException。当然,这取决于代码运行的位置,而不是开发的位置。 - akauppi
显示剩余25条评论

86

这里是另一种不使用BigDecimal的解决方案

截断:

(math floor 1.23456789 * 100) / 100

四舍五入(参见rint):

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

同样的方法也可以用于 rounding 函数,这次使用柯里化:

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),对吧? - C4stor
这似乎对于 NaN 返回了错误的结果,是吗? - jangorecki
floor 的问题在于 truncateAt(1.23456789, 8) 返回的是 1.23456788,而 roundAt(1.23456789, 8) 则会返回正确的值 1.23456789 - Todor Kolev

39

由于没有人提及%运算符,所以在这里介绍一下。它只进行截断,并且您不能指望返回值不具有浮点不准确性,但有时它很方便:

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

2
不过我并不推荐这种做法,因为正如@ryryguy在另一个答案的评论中提到的那样,同样存在精度问题。建议使用Java ROOT语言环境下的string.format(我会在那里进行评论)。 - akauppi
如果您只需要渲染数值而不需要在后续操作中使用它,那么这是完美的选择。谢谢。 - Alexander Arendar
3
这里有一个有趣的事情: 26.257391515826225 - 0.057391515826223094 = 26.200000000000003 - kubudi

15

怎么样:

 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位小数。 - robert
这个解决方案可行,但如果我需要在小数位中保留零,例如保留4位小数,它就无法实现。 但是,在这种情况下,格式化操作是正确的:"%.4f".format(myDoubleNumber)。示例:"%.4f".format(1.99999) 将返回 2.0000 "%.4f".format(1.23499) 将返回 1.2350当然,结果是字符串,仅适用于渲染。 - NKM

8

1
如果X位小数不是预先确定的,而是一个变量呢?我尝试过 f"$y$xf" 但不行。 - Jason Politis

7

编辑:修复了@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之间任意舍入。


3
小心,通过乘以倒数不可靠,因为它可能无法在double类型中可靠表示:scala> def r5(x:Double) = math.round(x*100000)*0.000001; r5(0.23515) ==> res12: Double = 0.023514999999999998。相反,应该通过除以有效数字来处理:math.round(x*100000)/100000.0 - ryryguy
将递归的 p10 函数替换为数组查找可能也很有用:该数组会增加约 200 字节的内存消耗,但可能每次调用可以节省多个迭代。 - Levi Ramsey

5
对于那些感兴趣的人,这里提供了一些建议解决方案的时间...
Rounding
Java Formatter: Elapsed Time: 105
Scala Formatter: Elapsed Time: 167
BigDecimal Formatter: Elapsed Time: 27

Truncation
Scala custom Formatter: Elapsed Time: 3 

截断(Truncation)是最快的,其次是BigDecimal。请记住,这些测试是在正常的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没有具体说明是四舍五入还是直接截断;...截断或四舍五入一个Double值 - cevaris
但是在我看来,仅仅比较截断函数和四舍五入函数的速度/执行时间是不够的。这就是为什么我要求您向读者澄清自定义功能仅截断的原因。而您提到的截断/自定义函数可以进一步简化。val doubleParts = double.toString.split(".") 现在获取 doubleParts.tail 的前两个字符并与字符串 "." 和 doubleParts.head 连接,然后解析为双精度浮点数。 - Ravinder Payal
1
更新了,看起来更好了吗?另外你提到的toString.split(".")doubleParts.head/tail建议可能会导致额外的数组分配和字符串连接。不过还需要测试以确保。 - cevaris
@OldGaurd01 你认为对于数字截断/四舍五入函数的“简化”方法是使用String类型???在哪个世界里这是一种简化方式???至少需要进行2次额外的强制类型转换,即Double -> String 和 String -> Double(每部分潜在地需要2次...)。 - Randomness Slayer
你怎么能说速度/执行时间“不足”,而你的建议实际上是将精确数字转换为字符串近似值(取任何重复/无理数)?这不仅可能不够精确,而且从一开始就引入了四舍五入/截断。因此,你在评论中的建议不仅比其他建议慢,而且也不够精确。@OldGaurd01 - Randomness Slayer

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

3

这个帖子里的回答都很好。为了更好地展示区别,这里只是一个例子。我把它放在这里的原因是因为在我的工作中需要确保数字不是半数上调:

    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(其中必须显式定义舍入模式))。


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)

我使用了这个Stack Overflow问题的解决方法。我有几个重载函数,包括Float\Double和implicit\explicit选项。请注意,在重载函数的情况下,需要明确指定返回类型。


此外,您可以使用 @rex-kerr 的方法来计算幂,而不是使用 Math.pow。 - Khalid Saifullah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接