3
votes

New to SML, trying to round up a real number to nth decimal, by declaring a function round(n,L), where L is a list of real numbers and n decide the nth decimal that can round up to.

My approach is to convert the real number to a string first, and then get the substring to the nth decimal and then parse the substring back to real number, this works fine if I only want to get the real number to nth decimal, but if I have a number like 0.3456 which I want to round to 0.35, my method won't really achieve that.

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)

The expected result is like this:

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

But the actual output I got is

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

If I want to round up the number, is there any good approach?

I've also tried this:

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;

but this solution will give me an uncaught exception overflow...

1
The Basis library has rounding functions, you can use one of those to do what you need :-)Yawar
I tried that but it keeps giving me an overflow exception...FlashWolves
Why do you expect String.substring("~0.3456"),0,5) to produce "~0.35"?molbdnilo
@FishWolves that's happening because of the recursion, not because of the rounding. I recommend writing a decimal place rounding function that works on a single number first. Then just use List.map to apply it to a list of reals.Yawar

1 Answers

1
votes

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

Judging by your use of Math.pow(10.0,real(a)) in your second attempted solution, you seem to be on track. I don't understand where a list comes in; as Yawar points out, try and solve this for rounding a single real, and then apply that recursively (using map) to a list of reals.

So a function

fun roundN (x, n) = ...

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

Start by making some examples and encode them as tests. Since you can't compare real for equality in those tests, start by making (or copying) a custom equality operator.

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

You also have some edge cases that you eventually want to deal with:

  • When n is zero or negative, or when n is greater than the number of digits in the fraction.
  • When x is close to a rounding point, e.g. roundN (3.1451, 2) ~> 3.15.
  • When x·10ⁿ has a magnitude that exceeds the size of an int.
  • When n is so large that a magnitude change may affect the precision of a real.

For a better testing library, check out testlib.sml (and its use in test.sml) in this exercism exercise.

Extracting your second solution into a function, and giving Math.pow (10.0, real n) a temporary binding, you get the solution:

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

On what input, I might ask.

One source could be that round : real -> int is a partial function: There are real values that cannot be expressed as int, such as Real.posInf, Real.negInf, 1e10 (on 32-bit SML) and 1e19 (on 64-bit SML). To avoid this, consider using Real.realRound : real -> real to avoid the int conversion.

One way to avoid errors related to x * Math.pow(10.0, real n) causing imprecision because the number grows too big, could be to strip the integer part before multiplying, and adding the integer part back after dividing.