Round, With Tie Breakers, A Prototype

In February and March I published three blog posts about an enhancement request for MATLAB concerning tie breakers with the round function, including the ability to round ties to even. Round One, Round Two and Round Three. Since then, a group of us at MathWorks, organized by my colleague (and academic great-great-grand-descendant) Heiko Weichelt, have been considering the request. We had a virtual design review recently. This post describes a prototype incorporating the features that were discussed at that review.

Contents

round

Traditionally, help round had only one line.

  ROUND(X) rounds each element of X to the nearest integer.

A few years ago, two optional arguments were added.

  ROUND(X, N), for positive integers N, rounds to N digits to the right
  of the decimal point. If N is zero, X is rounded to the nearest integer.
  If N is less than zero, X is rounded to the left of the decimal point.
  N must be a scalar integer.
  ROUND(X, N, 'significant') rounds each element to its N most significant
  digits, counting from the most-significant or left side of the number.
  N must be a positive integer scalar.

floor and ceil

The functions floor and ceil have important roles in any discussion of rounding. Every real number, x, that is not an integer, lies between two integers,

  floor(x) < x < ceil(x) = floor(x) + 1

If x is an integer, then

  floor(x) = x = ceil(x)

The function floor(x) defines "rounding x towards minus infinity" and the function ceil(x) is "rounding x towards plus infinity".

The function fix(x), which is also known as "integer part of x", is

  fix(x) = floor(x) if x >= 0,
         = ceil(x) if x < 0.

The "fractional part of x" is

  frac(x) = x - fix(x).

Two examples

  x = 9.64             x = -9.64
  floor(x) = 9         floor(x) = -10
  ceil(x) = 10         ceil(x) = -9
  fix(x) = 9           fix(x) = -9
  frac(x) = 0.64       frac(x) = -0.64

Ties

The traditional code for round(x) is one line.

  round(x) = sign(x).*floor(abs(x) + 0.5)

This implies that

  round(x) = floor(x) if frac(x) < 0.5
           = ceil(x) if frac(x) > 0.5

This unambiguously defines round(x) for almost all x. The only ambiguous situations are the rounding ties. They occur when the fractional part of x is exactly 1/2, or 0.5 in decimal. Ties are relatively rare. Traditionally, MATLAB has rounded ties away from zero.

When there is no scaling specified, the ties are the numbers exactly halfway between two consecutive integers,

  halfs = -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, ...

The "round ties away from zero" rule produces

  round(halfs) = -4, -3, -2, -1, 1, 2, 3, 4, ...

Recently, there has been some interest in alternatives to this behavior. One alternative is "round ties to nearest even". For the halfway numbers, this would be

  halfs_even = -4, -2, -2, 0, 0, 2, 2, 4, ...

Nearest decimal

Here are four examples of rounding to the nearest decimal value.

    format short
    format compact
  • Express intercity mileage distance in whole numbers. The distance from my home office to MathWorks headquarters is 406.3 miles. What is this to the nearest mile?
    miles = Round(406.3)
miles =
   406
  • The patient's body temperature is 98.64 degrees. Express this in nearest tenths of a degree.
    temp = Round(98.64,1)
temp =
   98.6000
  • Express monetary values in hundredths. The computed interest is 13.8327 Euros. Express this in hundredths of a Euro.
    cents = Round(13.8327,2)
cents =
   13.8300
  • The population of Tallahassee in 2019 was reported to be 194,500. What is this to the nearest one-thousand people?
    pop = Round(194500,-3)
pop =
      195000

Only one of these examples involves a tie. With a round-ties-to-even rule, the population of Tallahassee would be listed as 194,000 instead of 195,000.

IEEE 754

The round function is not the same as the round-to-even setting for IEEE 754 floating point arithmetic. The round function is done with software by MATLAB; its input is doubles (or singles) and the output is flints (floating point numbers whose values are integers.) The IEEE 754 setting is done in hardware, its input is extended precision or registers with guard bits and its output is doubles that are rarely flints.

Name-value pairs

Several reviewers preferred name-value pairs. Name-value pairs have been part of MATLAB syntax for a long time and a recent enhancement involves the use of an equals sign to specify them. For example

  plot(x,y,'ko','markersize',10)

can now be written

  plot(x,y,marker='o',color='black',markersize=10)

The left hand portion of a name-value pair is the paramName and the list of possible right hand portions is paramValues.

At first, I was not enthusiastic about using a name-value pair for tie-braking in round. But after working on this prototype and blog post, I am now in favor. The paramName could be

   roundTies

There would be six possible paramValues.

   awayFromZero
   towardZero
   toEven
   toOdd
   towardPlusInfinity
   towardMinusInfinity

For example

    x = (0.1:0.2:0.9)'
    r_from = Round(x,roundTies='awayFromZero')
    r_even = Round(x,roundTies='toEven')
x =
    0.1000
    0.3000
    0.5000
    0.7000
    0.9000
r_from =
     0
     0
     1
     1
     1
r_even =
     0
     0
     0
     1
     1

Not-a-Number

This occurred to me while writing this post. How about a seventh choice for handling ties?

    r_nan = Round(x,roundTies='toNaN')
r_nan =
     0
     0
   NaN
     1
     1

help

The next five sections are the code for a prototype. The file name is Round.m with a capital R so we can also access the round function from the current release. The code is available here: Round.m

    code = split(string(fileread('Round.m')),'%_');

The help text is concise.

    disp(code(1))
function x = Round(varargin)
% Rounds toward nearest decimal or integer.
%
% round(x)  Round each element of x to the nearest integer.
%
% round(x, n)  Round to the n-th decimal place.
% round(x, n, digits="significant")  Round to n significant digits.
% round(x, n, digits="decimals")  Same as round(x,n). 
% 
% round(x,..., roundTies="awayFromZero")
% round(x,..., roundTies="towardsZero")
% round(x,..., roundTies="toOdd") 
% round(x,..., roundTies="toEven")
% round(x,..., roundTies="towardsPlusInfinity")           
% round(x,..., roundTies="towardsMinusInfinity")         
%
% Ties are rare. round(x,n) is a tie only when 10^n*x is within
% roundoff error of a point halfway between two consecutive integers.

examples

The help text continues with a few examples.

    disp(code(2))
% Examples
% 
%    x = 123456.789
%
%    round(x)     123457
%    round(x,-3)  123000
%    round(x,2)   123456.79
% 
%    x = 1.125
%    
%    round(x)                       1.000
%    round(x,1)                     1.100
%    round(x,2)                     1.130
%    round(x,2,roundTies="toEven")  1.120 
%    round(x,2,roundTies="toOdd")   1.130
%    round(x,3)                     1.125
%    round(x,3,"significant")       1.130
%
%    x = 1.115 
%    xlo = 1115/1000 - eps/25
%    xhi = 1115/1000 + 24*eps/25
%
%    round(x,2)                       1.120
%    round(xlo,2,roundTies="toOdd")   1.110
%    round(xhi,2,roundTies="toEven")  1.120

main

Here is the body of Round. The numerically sensitive portions are the definition of exact and the choice of < or <= in the determination of t, the ties.

    disp(code(3))
%   main program

    [x,n,sig,tie] = parse(varargin{:});
    x0 = x;
    s = sign(x);
    x = abs(x);
    
    if sig
        n = n - ceil(log10(x));
    else
        n = n - zeros(size(x));
    end
    
    x = scale(x,n);
    z = x;
    f = z - floor(z);
    
    m = (f < 0.5);
    x(m) = floor(z(m));
    
    m = (f >= 0.5);
    x(m) = ceil(z(m));
    
    exact = (x0 == single(x0));
    if exact
        t = (f == 0.5);  % ties
    else
        t = abs(f - 0.5) <= eps(z);  % ties
    end
    x(t) = ceil(z(t));
    
    switch tie
        case 'even'
           m = (mod(x(t),2) == 1); 
        case 'odd'
           m = (mod(x(t),2) == 0);
        case 'plus'
           m = (s(t) < 0);
        case 'minus'
           m = (s(t) > 0);
        case 'zero'
           m = 1;
        case 'nan'
           m = NaN;
        otherwise
           m = 0;
    end
    x(t) = x(t) - m;

    x = s.*scale(x,-n);


parse

This parser handles the name-value pairs in a very simplistic manner. A more serious parser would be more discriminating.

    disp(code(4))
 
    function [x,n,sig,tie] = parse(varargin)
        x = varargin{1};
        n = 0;
        sig = false;
        tie = 'from';
        for k = 2:nargin
            vk =  varargin{k};
            if isnumeric(vk)
                n = vk;
            elseif vk == "significant"
                sig = true;
            elseif vk == "decimals" || ...
                vk == "roundTies" || ...
                vk == "digits"
                % ignore
            else
                tie = char(vk);
                caps = find(double(tie) < double('a'));
                if length(caps) > 1 && caps(2) > caps(1)+2
                    tie = lower(tie(caps(1):caps(2)-1));
                elseif length(caps) > 0 && length(tie(caps:end)) <= 5
                    tie = lower(tie(caps(1):end));
                else
                    error(vk + " not recognized.")
                end
            end
        end
    end 

scale

This scaling function always multiplies or divides by positive integer powers of 10, which are exact for n <= 16.

    disp(code(5))
    function x = scale(x,n)
        k = (n > 0);
        x(k) = x(k).*10.^n(k);
        k = (n < 0);
        x(k) = x(k)./10.^(-n(k));
    end

graphic

Here is the graphic from previous posts, now labeled by the paramValues.

    graphic

software

The prototype code is available here: Round.m




Published with MATLAB® R2021b

|
  • print

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.