forum-msg-id-135075-edit

Original date:2020-09-29 13:27:08 Edited by: petelomax Subject: Re: Novice Level Project - simple, fun and useful

Coincidentally (while on a completely unrelated code-fragment-formatting-issue-mini-trawl) I just ran into the bankers rounding section of the phix docs, and was obviously reminded of this thread, and ended up completely rewriting both the previously frankly rubbish code and the previously rather dour and depressing documentation:

integer pennies = bankers_rounding(atom pence, integer precison=1)

In the standard round() function, exact .5s are rounded up (matching Euphoria) whereas banker's rounding yields the nearest even number, eg:

constant x1 = {-4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5}
?apply(x1,round)
?apply(x1,bankers_rounding)
function precision100(atom xi) return bankers_rounding(xi*100,100) end function
?apply(x1,precision100)
-- output:
-- {-4,-3,-2,-1,0,1,2,3,4,5}
-- {-4,-4,-2,-2,0,0,2,2,4,4}
-- {-400,-400,-200,-200,0,0,200,200,400,400}

The reason you might want to use banker's rounding is so that the average of the rounded numbers is as close as possible to the average of the original numbers (ditto total), whereas always rounding up obviously introduces a systemic bias.
Above, the standard rounding average is out by 0.1, whereas the average x1 and the average bankers are both exactly zero (not that it would be difficult to construct a deliberately biased x1 such that the opposite were true; you only get better outputs for evenly-dispersed/unbiased inputs).

Note that this is only possible when working in whole (integer) pennies/cents (and above), not pounds/dollars to 2dp, and the precision is not inverted (see Technicalia). Should you want to store things in 2dp format, that's fine as long as you round(*100) to get exact whole integer penny/cent values back out when you retrieve them, that is prior to attempting any subsequent calculations that are likely to end up requiring any further banker's rounding, but of course you don't need to do that if all you're going to do is print or add and subtract them. [It should make no difference whatsoever if you use round() or bankers_rounding() to retrieve the whole integer penny/cent values.]

Technicalia:
Since eg 0.045 cannot be held exactly in an IEEE 754 floating point, it would be rather difficult to coerce it to (almost) 0.04 rather than (almost) 0.05 - it would instead naturally fall depending on whether the nearest admissible representation was out by +/-epsilon. An abs(frac-0.5)<1e-13 or similar rather than frac=0.5 (see pmaths.e) might work, but could introduce another (small) bias of the very same kind that we are trying to eliminate. Best to just stick with integers. There is of course nothing to stop you taking a copy of bankers_rounding() and creating a (renamed) application specific version of it, tweaked however you like.

Likewise if bankers_rounding() had an inverted precision, inaccuracies in the representation of(eg) 0.01 could totally spanner it, hence it has an non-inverted (and integer) precision.
The integer return type of bankers_rounding is quite, quite, deliberate.

And the (new) code is just:

global function bankers_rounding(atom pence, integer precision=1)
integer pennies, -- (or nearest 100, etc, but never nearest < 1 )
s = sign(pence), whole
pence = abs(pence)/precision
whole = floor(pence)
atom fract = pence-whole
if fract=0.5 then
pennies = whole+and_bits(whole,1)
else
pennies = floor(0.5+pence)
end if
pennies *= s*precision
return pennies
end function