1

Is there a function which rounds numbers (even decimal numbers) like round() in Excel?

Example

Round 1,45 to one decimal: 1,5

Round 2,45 to one decimal: 2,5

There is a similar question but they use a different algorithm.

Sr. Schneider
  • 647
  • 10
  • 20

2 Answers2

2

OK, here's an attempt to reimplement Excel =ROUND function in Maxima. Some notes. (1) Values are rounded to 15 significant digits before applying the user's rounding. This is an attempt to work around problems caused by inexact representation of decimals as floating point numbers. (2) I've implemented excel_round and integer_log10 as so-called simplifying functions. That means that the calculation isn't carried out until the arguments are something that can be evaluated (in this case, when the arguments are numbers). (3) I didn't check to see what Excel =ROUND does with negative numbers -- does it round 5 upward (i.e., towards zero in this case), or away from zero? I dunno.

I've posted this solution as the little package excel_round.mac on Github. See: https://github.com/maxima-project-on-github/maxima-packages and navigate to robert-dodier/excel_round. In the interest of completeness, I've pasted the code here as well.

Here are a few examples.

(%i1) excel_round (1.15, 1);
(%o1)                                 1.2
(%i2) excel_round (1.25, 1);
(%o2)                                 1.3
(%i3) excel_round (12.455, 2);
(%o3)                                12.46
(%i4) excel_round (x, 2);
(%o4)                          excel_round(x, 2)
(%i5) ev (%, x = 9.865);
(%o5)                                9.87

Here is the code. This is the content of excel_round.mac.

/* excel_round -- round to specified number of decimal places,
 * rounding termminal 5 upwards, as in MS Excel, apparently.
 * Inspired by: https://stackoverflow.com/q/62533742/871096
 *
 * copyright 2020 by Robert Dodier
 * I release this work under terms of the GNU General Public License.
 */

matchdeclare (xx, numberp);
matchdeclare (nn, integerp);
tellsimpafter (excel_round (xx, nn), excel_round_numerical (xx, nn));

matchdeclare (xx, lambda ([e], block ([v: ev (e, numer)], numberp(v))));
tellsimpafter (excel_round (xx, nn), excel_round_numerical (ev (xx, numer), nn));

excel_round_numerical (x, n) :=
  block ([r, r1, r2, l],
         /* rationalize returns exact rational equivalent of float */
         r: rationalize (x),
         /* First round to 15 significant decimal places.
          * This is a heuristic to recover what a user "meant"
          * to type in, since many decimal numbers are not
          * exactly representable as floats.
          */
         l: integer_log10 (abs (r)),
         r1: round (r*10^(15 - l)),
         /* Now begin rounding to n places. */
         r2: r1/10^((15 - l) - n),
         /* If terminal digit is 5, then r2 is integer + 1/2.
          * If that's the case, round upwards and rescale,
          * otherwise, terminal digit is something other than 5,
          * round to nearest integer and rescale.
          */
         if equal (r2 - floor(r2), 1/2)
           then ceiling(r2)/10.0^n
           else round(r2)/10.0^n);

matchdeclare (xx, lambda ([e], numberp(e) and e > 0));
tellsimpafter (integer_log10 (xx), integer_log10_numerical (xx));

matchdeclare (xx, lambda ([e], block ([v: ev (e, numer)], numberp(v) and v > 0)));
tellsimpafter (integer_log10 (xx), integer_log10_numerical (ev (xx, numer)));

matchdeclare (xx, lambda ([e], not atom(e) and op(e) = "/" and numberp (denom (e)) and pow10p (denom (e))));
pow10p (e) := integerp(e) and  e > 1 and (e = 10 or pow10p (e/10));
tellsimpafter (integer_log10 (xx), integer_log10 (num (xx)) - integer_log10_numerical (denom (xx)));

integer_log10_numerical (x) :=
  if x >= 10
    then (for i from 0 do
              if x >= 10 then x:x/10 else return(i))
  elseif x < 1
    then (for i from 0 do
              if x < 1 then x:x*10 else return(-i))
  else 0;
Robert Dodier
  • 16,905
  • 2
  • 31
  • 48
0

The problem of rounding numbers is actually pretty subtle, but here is a simple approach which I think gives workable results. Here I define a new function myround which has the behavior described for Excel =ROUND. [1]

(%i4) myround (x, n) := round(x*10^n)/10.0^n;
                                                    n
                                         'round(x 10 )
(%o4)                   myround(x, n) := -------------
                                                 n
                                             10.0
(%i5) myround (2.15, 1);
(%o5)                                 2.2
(%i6) myround (2.149, 1);
(%o6)                                 2.1
(%i7) myround (-1.475, 2);
(%o7)                               - 1.48
(%i8) myround (21.5, -1);
(%o8)                                20.0
(%i9) myround (626.3, -3);
(%o9)                               1000.0
(%i10) myround (1.98, -1);
(%o10)                                0.0
(%i11) myround (-50.55, -2);
(%o11)                              - 100.0

[1] https://support.microsoft.com/en-us/office/round-function-c018c5d8-40fb-4053-90b1-b3e7f61a213c

Robert Dodier
  • 16,905
  • 2
  • 31
  • 48
  • `myround (1.45, 1);` gives `1.4`. `myround (1.55, 1);` gives `1.6`. Why? Well, the problem is the implementation of the `round()` function in `Maxima`: The manual says that `Multiples of 1/2 are rounded to the nearest EVEN(!) integer.` But isn't there a workaround? – Sr. Schneider Jun 24 '20 at 06:44
  • The documentation for =ROUND doesn't say so, but from trying some examples in Excel, it looks like =ROUND always rounds up. For integer + 1/2, Maxima implements what's called "banker's rounding" or "round to even", however, note that nnn.nnn are floating point numbers, not exact decimals, so it's the float nearest to nnn.nnn which gets rounded, not nnn.nnn itself; this is the subtlety I mentioned. It's probably possible to reimplement "always round up" but once we get into that, it becomes a rather tricky problem. Maybe you can say more about what you need to do. – Robert Dodier Jun 24 '20 at 17:03
  • `myround(1.41,1);` to `myround(1.44,1);` should round to `1.4` and `myround(1.45,1);` to `myround(1.49,1);` to `1.5`. I think this is was Excel does. – Sr. Schneider Jun 24 '20 at 17:13
  • Right, the difference in behavior is only in how to handle decimals which end in 5. I wasn't careful enough in my comments above. – Robert Dodier Jun 24 '20 at 17:28
  • So how to implement this with `Maxima`? – Sr. Schneider Jun 24 '20 at 18:21
  • Let me think about it, I'll post another answer. The tricky part is inferring what the user meant to say when the actual float value is not exactly nnn/10^n (e.g., 1.224999999999 or something like that -- did they actually type in 1.225?). The rest is not yet clear to me but it feels doable. – Robert Dodier Jun 24 '20 at 20:19
  • No, 1.225 can also be the result of another calculation. – Sr. Schneider Jun 25 '20 at 06:21