Bankers Round in PostgreSQL

Bankers Round (aka "half even up") is a rounding scheme very used in financial apps as it produces statistically less rounding issues than the most commonly extended "half up".
The problem is that Postgres' round function implements "half up". Read on to find out how to implement "half even up".
Python already defaults to bankers rounding, but to make it more explicit here's a function using the decimal module that implements a bround function that rounds to 2 decimal places using ROUND_HALF_EVEN:
>>> from decimal import ROUND_HALF_EVEN, Decimal
...
...
... def bround(val: Decimal) -> Decimal:
... return val.quantize(Decimal("0.01"), ROUND_HALF_EVEN)
...
>>> bround(Decimal("2.125")), bround(Decimal("2.135"))
(Decimal('2.12'), Decimal('2.14'))
With Postgres' round the results are different:
localhost=> select round(2.125, 2), round(2.135, 2);
round | round
-------+-------
2.13 | 2.14
(1 row)
So now that the issue is clear, how do you create your own bround function in plpgsql?
create or replace function bround(value numeric, prec integer default 0)
returns numeric as $$
declare
power_val numeric := power(10, prec)::numeric;
scaled numeric := value * power_val;
truncated numeric := trunc(scaled);
result numeric;
begin
result := case
when abs(scaled - truncated) = 0.5 then
case
when truncated % 2 = 0 then truncated
else truncated + sign(scaled)
end
else round(scaled)
end;
return round(result / power_val, prec);
end;
$$ language plpgsql strict immutable parallel safe;
This function now produces the expected output:
localhost=> select bround(2.125, 2), bround(2.135, 2);
bround | bround
--------+--------
2.12 | 2.14
(1 row)
The astute reader will have noticed that I have marked the function as
strict immutable parallel safe
. This basically tells the system that the function does not touch
the database and it's safe to run in parallel. In my testing it makes it run 2.5x faster! See the
docs for more information.