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
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.
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
Yeah, but decimal(30,20) works perfectly in the examples.
511.9400512.9400578.9400654.9400655.9400
3.02003.01003.02003.04003.0500
Here is what I came up with:
CREATE FUNCTION dbo.fn_BRound ( @TestValue decimal(38,12), @pos int)RETURNS moneyas 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).
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
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
select
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