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
- Category:
- History,
- Numerical Analysis,
- Precision
Comments
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.