Search code examples
postgresqlfloating-pointplpgsqlroundingfloating-accuracy

postgresql round half down function


the round(numeric,integer) function in PostgreSQL only rounds up:

round(cast (41.0255 as numeric),3) ==> 41.026

Since we need a round function which returns 41.025 and (quite surprisingly) there isn't such a function in PostgreSQL (we're using 9.1.5), we wrote a "wrapper" function which in the very first version is quite naive and rough...but we didn't find anything better due the lack of native support for this kind of problems in plpgsql.

The code is showed below. The problem is that it's too slow for our purposes. Could you suggest a better way to deal with this task?

Here's the code:

    CREATE OR REPLACE FUNCTION round_half_down(numeric,integer) RETURNS numeric 
    AS $$
    DECLARE
      arg ALIAS FOR $1;
      rnd ALIAS FOR $2;
      tmp1 numeric;
      res numeric;
    BEGIN
      tmp1:=arg;
      IF cast(tmp1 as varchar) ~ '5$'  THEN res:=trunc(arg,rnd);
      ELSE res:=round(arg,rnd);
      END IF;

      RETURN res;
    END;
    $$ LANGUAGE plpgsql;

I need to cast the numeric value and use regexp...that's what (I suppose) kills performances.

Just so you know: we need this because we have to compare numbers that were stored in two different columns (on two different tables) but with different numeric data type: one is double and one is real. The problem is that when inserting into a real data type column, PostgreSQL performs ROUND HALF DOWN while it doesn't provide such option via its mathematical functions!

EDIT:
The function is actually bugged. Was a first quick rewriting as an attempt to improve performances of a working function but very slow.

Behavior must match the following:
IF the decimal digit being put off from rounding is <=5 => trunc
ELSE round up.

Some examples:

select round_half_down(cast (41.002555 as numeric),3) -- 41.002 
select round_half_down(cast (41.002555 as numeric),4) -- 41.0025 
select round_half_down(cast (41.002555 as numeric),5) -- 41.00255 

while the round function in PostgreSQL gives:

select round(cast (41.002555 as numeric),3) -- 41.003

Solution

  • We have to compare numbers that were stored in two different columns (on two different tables) but with different numeric data type: one is double and one is real.

    This should be extremely fast and simple:

    SELECT dp_col, real_col
    FROM   tbl
    WHERE  dp_col::real = real_col
    

    Basically, just cast the double precision number to real for comparison.


    If that doesn't work for you, this SQL function should do a proper job and work faster:

    CREATE OR REPLACE FUNCTION round_half_down1(numeric, int)
      RETURNS numeric LANGUAGE sql AS
    $func$
    SELECT CASE WHEN abs($1%0.1^$2) < .6 * 0.1^$2 THEN
             trunc($1, $2)
        ELSE round($1, $2) END;
    $func$
    

    Now fixed for negative numbers, with input from @sufleR in the comments.
    You can also just use the contained CASE expression.

    % .. modulo operator
    ^ .. exponentiation


    Here is a quick test you can use for benchmarking:

    SELECT n                                   -- Total runtime: 36.524 ms
          ,round_half_down1(n,3)               -- Total runtime: 70.493 ms
          ,round_down_to_decimal_places(n,3)   -- Total runtime: 74.690 ms
          ,round_half_down(n,3)                -- Total runtime: 82.191 ms
    FROM  (SELECT random()::numeric AS n FROM generate_series(1,10000)) x
    WHERE  round_down_to_decimal_places(n,3)
        <> round_half_down1(n,3)
    

    It also demonstrates how @Parveen's function and your edited version miscalculate - they trunc() where they shouldn't.