如何在 SML 中将实数四舍五入到第 n 位小数?

How to round up a real number to nth decimal in SML?

SML 新手,试图通过声明函数 round(n,L) 将实数四舍五入到第 n 位小数,其中 L 是实数列表,n 决定可以四舍五入到的第 n 位小数.

我的做法是先把实数转成字符串,然后得到第n位小数的子串,再把子串解析回实数,如果我只想得到实数第 n 个小数,但如果我有一个像 0.3456 这样的数字,我想四舍五入到 0.35,我的方法将无法真正实现。

fun rd(_,[]) = []
|rd(a:int, x::y:real list) =
if x>0.0
then Option.getOpt(Real.fromString(String.substring(Real.toString(x),0,a+2)),0.0) :: rd(a,y)
else Option.getOpt(Real.fromString(String.substring(Real.toString(x),0,a+3)),0.0) :: rd(a,y)

预期的结果是这样的:

- rd (2, [0.1234, 0.2345, ~0.3456]);
val it = [0.12,0.23,~0.35] : real list`

但是我得到的实际输出是

val it = [0.12,0.23,~0.34] : real list

如果我想把数字四舍五入,有什么好的办法吗?

我也试过这个:

fun rd(_,[]) = []
|rd(a:int, x::y:real list) =
let
val n = real(round(x*Math.pow(10.0,real(a)))) / Math.pow(10.0,real(a))
in n::rd(a,y)
end;

但是这个解决方案会给我一个未捕获的异常溢出...

trying to round up a real number to nth decimal

declaring a function round(n,L), where L is a list of real numbers and n decide the nth decimal

从您在第二次尝试的解决方案中使用 Math.pow(10.0,real(a)) 判断,您似乎走上了正轨。我不明白清单从何而来;正如 Yawar 指出的那样,尝试解决这个问题以舍入单个实数,然后递归地(使用 map)将其应用于实数列表。

所以一个函数

fun roundN (x, n) = ...

fun roundManyN (xs, n) = map (fn x => roundN (x, n)) xs

首先制作一些示例并将它们编码为测试。由于您 在这些测试中,请先制作(或复制)自定义相等运算符。

fun nearlyEqual (a, b, eps) =
    let val absA = Real.abs a
        val absB = Real.abs b
        val diff = Real.abs (a - b)
    in Real.== (a, b) orelse
     ( if Real.== (a, 0.0) orelse
          Real.== (b, 0.0) orelse
          diff < Real.minNormalPos
       then diff < eps * Real.minNormalPos
       else diff / Real.min (absA + absB, Real.maxFinite) < eps )
    end

val test_roundN_1 =
  let val got = roundN (3.14159, 1)
      val expected = 3.1
  in nearlyEqual (got, expected, 0.1) end

val test_roundN_2 =
  let val got = roundN (3.14159, 2)
      val expected = 3.14
  in nearlyEqual (got, expected, 0.01) end

(* rounding point *)
val test_roundN_3 =
  let val got = roundN (3.14159, 3)
      val expected = 3.142
  in nearlyEqual (got, expected, 0.001) end

(* rounding point *)
val test_roundN_4 =
  let val got = roundN (3.14159, 4)
      val expected = 3.1416
  in nearlyEqual (got, expected, 0.0001) end

val test_roundN_5 =
  let val got = roundN (3.14159, 5)
      val expected = 3.14159
  in nearlyEqual (got, expected, 0.00001) end

您还有一些最终想要处理的边缘情况:

  • n为零或负数,或n大于分数位数时。
  • x接近圆点时,例如roundN (3.1451, 2) ~> 3.15.
  • x·10ⁿ 的大小超过 int 的大小时。
  • n 太大以至于幅度变化可能会影响实数的精度时。

要获得更好的测试库,请查看 this exercism exercise 中的 testlib.sml(及其在 test.sml 中的使用)。

将你的第二个解决方案提取到一个函数中,并给 Math.pow (10.0, real n) 一个临时绑定,你得到解决方案:

fun roundN (x, n) =
    let val m = Math.pow(10.0, real n)
    in real (round (x * m)) / m end

this solution will give me an uncaught exception overflow

关于什么输入,我可能会问。

一个来源可能是 round : real -> int 是偏函数:有些实数不能表示为 int,例如 Real.posInfReal.negInf1e10 (在 32 位 SML 上)和 1e19(在 64 位 SML 上)。为避免这种情况,请考虑使用 Real.realRound : real -> real 来避免 int 转换。

避免与 x * Math.pow(10.0, real n) 相关的错误导致不精确的一种方法是在乘法之前去除整数部分,然后在除法之后将整数部分加回去。