Click here to monitor SSC
SQLServerCentral is supported by Red Gate Software Ltd.
 
Log in  ::  Register  ::  Not logged in
 
 
 
        
Home       Members    Calendar    Who's On


Add to briefcase 12345»»»

Bankers Rounding Expand / Collapse
Author
Message
Posted Thursday, May 31, 2007 12:05 PM


SSC-Addicted

SSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-Addicted

Group: General Forum Members
Last Login: Friday, March 08, 2013 11:15 AM
Points: 440, Visits: 1,785

Seeing all the activity Grant Fritchey's article "The T-SQL Quiz" has generated, which is well worth a read btw, I thought I'd pose this.

Warning: You'll need to set aside an extended lunch break to work through all the code generated in the responses to the article , but boy will you learn a lot.

I was asked if it was possible to do bankers rounding in SQL.  In my usual way, I went off and did a little research (see comments below) before coming up with the following function.  The thing is, it has a bug!  Hope fully the comments in the code make it clear what's going wrong and where, but if anyone can enlighten me, please do so.  My math is too poor!  In my defense, if you give the test cases to Excel using the algorithm supplied, it goes wrong too!


-- =============================================
-- Create scalar function (FN)
-- =============================================
IF EXISTS (SELECT *
           FROM   sysobjects
           WHERE  name = N'fn_BRound')
  DROP FUNCTION fn_BRound

GO

CREATE FUNCTION fn_BRound
               (@p1 float(53),@p2 int)
RETURNS money


/* 

Round-to-even method (from http://en.wikipedia.org/wiki/Rounding)

This method is also known as unbiased rounding or as statistician's rounding or as bankers' rounding.
It is identical to the common method of rounding except when the digit(s) following the rounding digit
start with a five and have no non-zero digits after it. The new algorithm is:

    * Decide which is the last digit to keep.
    * Increase it by 1 if the next digit is 6 or more, or a 5 followed by one or more non-zero digits.
    * Leave it the same if the next digit is 4 or less
    * Otherwise, all that follows the last digit is a 5 and possibly trailing zeroes;
      then change the last digit to the nearest even digit. That is, increase the rounded
      digit if it is currently odd; leave it if it is already even.

With all rounding schemes there are two possible outcomes: increasing the rounding digit by one or
leaving it alone. With traditional rounding, if the number has a value less than the half-way mark
between the possible outcomes, it is rounded down; if the number has a value exactly half-way or
greater than half-way between the possible outcomes, it is rounded up. The round-to-even method is the
same except that numbers exactly half-way between the possible outcomes are sometimes rounded up—sometimes down.

Although it is customary to round the number 4.5 up to 5, in fact 4.5 is no nearer to 5 than it is to 4
(it is 0.5 away from either). When dealing with large sets of scientific or statistical data, where trends
are important, traditional rounding on average biases the data upwards slightly. Over a large set of
data, or when many subsequent rounding operations are performed as in digital signal processing, the
round-to-even rule tends to reduce the total rounding error, with (on average) an equal portion of numbers
rounding up as rounding down. This generally reduces the upwards skewing of the result.

Round-to-even is used rather than round-to-odd as the latter rule would prevent rounding to a result of zero.

Examples:

    * 3.016 rounded to hundredths is 3.02 (because the next digit (6) is 6 or more)
    * 3.013 rounded to hundredths is 3.01 (because the next digit (3) is 4 or less)
    * 3.015 rounded to hundredths is 3.02 (because the next digit is 5, and the hundredths digit (1) is odd)
    * 3.045 rounded to hundredths is 3.04 (because the next digit is 5, and the hundredths digit (4) is even)
    * 3.04501 rounded to hundredths is 3.05 (because the next digit is 5, but it is followed by non-zero digits)

-- =============================================
-- Example to execute function
-- =============================================
SELECT dbo.fn_BRound (282.26545, 100) -- 100 to nearest penny, 20 to nearest 5 pence, 10 to nearest 10 pence,
                                      -- 5 to nearest 20 pence, 2 to nearest 50 pence, 1 to nearest pound/dollar/euro

        Code (poorly?) converted from VB example @ http://support.microsoft.com/kb/196652

 */
AS
  BEGIN
    Declare  @Temp float(53),@FixTemp float(53)
    
    Select @Temp = @p1 * @p2
    
    --not right!
    -- According to Excel help Fix (used in the algorithm pinched from MS) in VBA is equivalent to
    -- Sgn(number) * Int(Abs(number))

    -- So why does this not work?
    -- Select @Temp = @Temp + 0.5 * Sign(@p1)
    -- Select @FixTemp = Sign(@Temp) * Floor(Abs(@Temp))

    --This is the closest I can get, but it is still wrong for certain values
    Select @FixTemp = Sign(@Temp + 0.5 * Sign(@p1)) * Floor(Abs(@Temp + 0.5 * Sign(@p1)))
    
    -- Handle rounding of .5 in a special manner
    If @Temp - Floor(@Temp) = 0.5
      Begin
        If @FixTemp / 2 <> Floor(@FixTemp / 2)  -- Is Temp odd
          -- Reduce Magnitude by 1 to make even
          Select @FixTemp = @FixTemp - Sign(@p1)
      End
    
    Return @FixTemp / @p2
  END

GO

SET NOCOUNT  ON

-- =============================================
-- Example to execute function
-- =============================================
SELECT dbo.fn_BRound(511.945,100) -- Right - 511.94
SELECT dbo.fn_BRound(512.945,100) -- Wrong - 512.95
SELECT dbo.fn_BRound(578.945,100) -- Any number between the one above and below, ending .945
SELECT dbo.fn_BRound(654.945,100) -- Wrong - 654.95
SELECT dbo.fn_BRound(655.945,100) -- Right - 655.94

--examples as mentioned in comment above
SELECT dbo.fn_BRound(3.016,100)
SELECT dbo.fn_BRound(3.013,100)
SELECT dbo.fn_BRound(3.015,100)
SELECT dbo.fn_BRound(3.045,100)
SELECT dbo.fn_BRound(3.04501,100)

 

This is not urgent, as the requirement has gone away, but it does intrigue me as to what the £$%* is going on.  Hint:  Make the function return a float and you will see what's rounding up, though I can't figure out why.

Dave J




http://glossopian.co.uk/
"I don't know what I don't know."
Post #370393
Posted Thursday, May 31, 2007 12:09 PM


SSC-Insane

SSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-Insane

Group: General Forum Members
Last Login: Yesterday @ 11:07 PM
Points: 21,625, Visits: 27,468

Without digging into myself at the moment, I'd say the problem is using float.  This is an imprecise numeric representation.  Try changing the floats to a large decimal, and see if you still have the same problem.

If I get some free time I will try it out myself.




Lynn Pettis

For better assistance in answering your questions, click here
For tips to get better help with Performance Problems, click here
For Running Totals and its variations, click here or when working with partitioned tables
For more about Tally Tables, click here
For more about Cross Tabs and Pivots, click here and here
Managing Transaction Logs

SQL Musings from the Desert Fountain Valley SQL (My Mirror Blog)
Post #370395
Posted Thursday, May 31, 2007 12:18 PM


SSC-Addicted

SSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-AddictedSSC-Addicted

Group: General Forum Members
Last Login: Friday, March 08, 2013 11:15 AM
Points: 440, Visits: 1,785

Wow! - quick response!

Replaced all the float declares with decimal(38,20) but I still get bad results

                     
---------------------
511.9500                     
---------------------
512.9500                    
---------------------
578.9500                    
---------------------
654.9500
--------------------
655.9500                     
---------------------
3.0200                     
---------------------
3.0100                     
---------------------
3.0200                     
---------------------
3.0500                     
---------------------
3.0500

Now the examples from wikipedia are broke too

I tried the money data type but that didn't work either, I think it's the algorithm, which I pinched 'cos I'm too stupid to write my own

Cheers

Dave J




http://glossopian.co.uk/
"I don't know what I don't know."
Post #370400
Posted Thursday, May 31, 2007 2:14 PM
SSCrazy Eights

SSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy EightsSSCrazy Eights

Group: Moderators
Last Login: Tuesday, April 09, 2013 12:53 PM
Points: 8,357, Visits: 684

Yeah, but decimal(30,20) works perfectly in the examples.

 

511.9400
512.9400
578.9400
654.9400
655.9400


3.0200
3.0100
3.0200
3.0400
3.0500




Post #370427
Posted Thursday, May 31, 2007 2:22 PM
SSCarpal Tunnel

SSCarpal TunnelSSCarpal TunnelSSCarpal TunnelSSCarpal TunnelSSCarpal TunnelSSCarpal TunnelSSCarpal TunnelSSCarpal TunnelSSCarpal Tunnel

Group: General Forum Members
Last Login: Today @ 4:02 AM
Points: 4,553, Visits: 8,208
Here is the mistake:

... Floor(Abs(@Temp + 0.5 * Sign(@p1)))
Post #370431
Posted Thursday, May 31, 2007 3:03 PM


SSC-Insane

SSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-Insane

Group: General Forum Members
Last Login: Yesterday @ 11:07 PM
Points: 21,625, Visits: 27,468

Here is what I came up with:

CREATE FUNCTION dbo.fn_BRound (
    @TestValue decimal(38,12),
    @pos int
)
RETURNS money
as begin
    return round(@TestValue, @pos, case when (nullif(round(@TestValue * power(cast(10 as decimal(38,12)), @pos),0,1),round((@TestValue * power(cast(10 as decimal(38,12)), @pos))/2,0,1) * 2) is not null) or (nullif(round((@TestValue * power(cast(10 as decimal(38,12)), @pos)),0,1),round((@TestValue * power(cast(10 as decimal(38,12)), @pos))/2,0,1) * 2) is null and (@TestValue * power(cast(10 as decimal(38,12)), @pos)) - round((@TestValue * power(cast(10 as decimal(38,12)), @pos)),0,1) > .5) then 0 else 1 end)
end

I also changed it to use the format of the round statement.  If you want to round to the hundreths you call it as dbo.fn_BRound(test_value, 2).




Lynn Pettis

For better assistance in answering your questions, click here
For tips to get better help with Performance Problems, click here
For Running Totals and its variations, click here or when working with partitioned tables
For more about Tally Tables, click here
For more about Cross Tabs and Pivots, click here and here
Managing Transaction Logs

SQL Musings from the Desert Fountain Valley SQL (My Mirror Blog)
Post #370444
Posted Thursday, May 31, 2007 4:43 PM


SSC-Dedicated

SSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-Dedicated

Group: General Forum Members
Last Login: Yesterday @ 7:51 PM
Points: 32,910, Visits: 26,800

Heh... I've been all through this on one unbelievably long post before and then I struck gold quite by accident... Unbeknowst to most, the STR function does Banker's rounding "auto-magically"...  try it...

SELECT STR(3.016,10,2)   --rounded to hundredths is 3.02 (because the next digit (6) is 6 or more) 
SELECT STR(3.013,10,2)   --rounded to hundredths is 3.01 (because the next digit (3) is 4 or less)
SELECT STR(3.015,10,2)   --rounded to hundredths is 3.02 (because the next digit is 5, and the hundredths digit (1) is odd)
SELECT STR(3.045,10,2)   --rounded to hundredths is 3.04 (because the next digit is 5, and the hundredths digit (4) is even)
SELECT STR(3.04501,10,2) --rounded to hundredths is 3.05 (because the next digit is 5, but it is followed by non-zero digits)
{EDIT} Please disregard this... Lynn Petis found a fly in the ointment and I can't explain it.  Sorry, folks.
{2nd Edit}... Serqiy figured out the error... I mistook rounding of the underlying FLOATS for Bankers' Rounding 


--Jeff Moden
"RBAR is pronounced "ree-bar" and is a "Modenism" for "Row-By-Agonizing-Row".

First step towards the paradigm shift of writing Set Based code:
Stop thinking about what you want to do to a row... think, instead, of what you want to do to a column."

For better, quicker answers on T-SQL questions, click on the following...
http://www.sqlservercentral.com/articles/Best+Practices/61537/

For better answers on performance questions, click on the following...
http://www.sqlservercentral.com/articles/SQLServerCentral/66909/
Post #370462
Posted Thursday, May 31, 2007 4:51 PM


SSC-Dedicated

SSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-Dedicated

Group: General Forum Members
Last Login: Yesterday @ 7:51 PM
Points: 32,910, Visits: 26,800

p.s.

Heh... looks like David and I got our rounding examples from the same source...

http://en.wikipedia.org/wiki/Bankers%27_rounding#Round-to-even_method

 



--Jeff Moden
"RBAR is pronounced "ree-bar" and is a "Modenism" for "Row-By-Agonizing-Row".

First step towards the paradigm shift of writing Set Based code:
Stop thinking about what you want to do to a row... think, instead, of what you want to do to a column."

For better, quicker answers on T-SQL questions, click on the following...
http://www.sqlservercentral.com/articles/Best+Practices/61537/

For better answers on performance questions, click on the following...
http://www.sqlservercentral.com/articles/SQLServerCentral/66909/
Post #370463
Posted Thursday, May 31, 2007 5:59 PM


SSC-Insane

SSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-InsaneSSC-Insane

Group: General Forum Members
Last Login: Yesterday @ 11:07 PM
Points: 21,625, Visits: 27,468
Am I doing something wrong here:

select str(612.945, 10, 2) -- returns 612.95, not 612.94

 




Lynn Pettis

For better assistance in answering your questions, click here
For tips to get better help with Performance Problems, click here
For Running Totals and its variations, click here or when working with partitioned tables
For more about Tally Tables, click here
For more about Cross Tabs and Pivots, click here and here
Managing Transaction Logs

SQL Musings from the Desert Fountain Valley SQL (My Mirror Blog)
Post #370479
Posted Thursday, May 31, 2007 6:21 PM


SSC-Dedicated

SSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-DedicatedSSC-Dedicated

Group: General Forum Members
Last Login: Yesterday @ 7:51 PM
Points: 32,910, Visits: 26,800

Yeah... the definitions on the reference I made are not exactly correct... here's the long post I was talking about... the 3rd post (Andrew) on the first page on the following thread explains it much more simply as does the 3rd from the last post (Jeff Moden) on the last page...

http://www.sqlservercentral.com/forums/shwmessage.aspx?forumid=8&messageid=246556

{Edit}... Sorry folks... I posted some bad info... Serqiy figured out the error... I mistook rounding of the underlying FLOATS for Bankers' Rounding 


--Jeff Moden
"RBAR is pronounced "ree-bar" and is a "Modenism" for "Row-By-Agonizing-Row".

First step towards the paradigm shift of writing Set Based code:
Stop thinking about what you want to do to a row... think, instead, of what you want to do to a column."

For better, quicker answers on T-SQL questions, click on the following...
http://www.sqlservercentral.com/articles/Best+Practices/61537/

For better answers on performance questions, click on the following...
http://www.sqlservercentral.com/articles/SQLServerCentral/66909/
Post #370481
« Prev Topic | Next Topic »

Add to briefcase 12345»»»

Permissions Expand / Collapse