Round - Округление это не просто!
Поскольку я занимался написанием банковских систем вопросы точности расчетов и количесва значащих цифр в подсчетах имеют для меня первостепенное значение. В этом топике я решил чуть-чуть подъитожить эту тему.
В качеcтве начала для этого разговора об округлениях я рекомендую ознакомиться вот с этой микрософтовской статьей, в которой собственно и обозначены существующие ТИПЫ ОКРУГЛЕНИЙ:
- Rounding Explained
- Rounding Down
- Rounding Up
- Arithmetic Rounding
- Banker's Rounding
- Random Rounding
- Alternate Rounding
Теперь, когда становится ясно, что вопрос округления не так прост, как кажется на первый взгляд, я приведу фонкцию БАНКОВСКОГО округления для SQL с сайта sqlservercentral:
00001: create FUNCTION RoundBanker 00002: ( @Amt numeric(38,16) 00003: , @RoundToDecimal tinyint 00004: ) 00005: RETURNS numeric(38,16) 00006: AS 00007: BEGIN 00008: declare @RoundedAmt numeric(38,16) 00009: , @WholeAmt integer 00010: , @Decimal tinyint 00011: , @Ten numeric(38,16) 00012: set @Ten = 10.0 00013: set @WholeAmt = ROUND(@Amt,0, 1 ) 00014: set @RoundedAmt = @Amt - @WholeAmt 00015: set @Decimal = 16 00016: While @Decimal > @RoundToDecimal 00017: BEGIN 00018: set @Decimal = @Decimal - 1 00019: if 5 = ( ROUND(@RoundedAmt * POWER( @Ten, @Decimal + 1 ) ,0,1) - (ROUND(@RoundedAmt * POWER( @Ten, @Decimal ) ,0,1) * 10) ) 00020: and 0 = cast( ( ROUND(@RoundedAmt * POWER( @Ten, @Decimal ) ,0,1) - (ROUND(@RoundedAmt * POWER( @Ten, @Decimal - 1 ) ,0,1) * 10) ) AS INTEGER ) % 2 00021: SET @RoundedAmt = ROUND(@RoundedAmt,@Decimal, 1 ) 00022: ELSE 00023: SET @RoundedAmt = ROUND(@RoundedAmt,@Decimal, 0 ) 00024: END 00025: RETURN ( @RoundedAmt + @WholeAmt ) 00026: END 00027: GO
В целом же, на уровне SQL существуют следующие типы округлений:
00001: declare @i int, @S1 decimal (10,4) , @S2 decimal (10,4), @S3 decimal (10,4), @S4 decimal (10,4), @S5 decimal (10,4), @S6 decimal (10,4) 00002: select @i=0, @S1=0, @S2=0, @S3=0, @S4=0 , @S5=0 , @S6=0 00003: While @i<100 Begin 00004: select @i=@i+1, 00005: @S1=@S1+ Floor (cast('0.'+ cast(@i as varchar) as decimal(10,4)) ), 00006: @S2=@S2+ Left (cast('0.'+ cast(@i as varchar) as decimal(10,4)),3), 00007: @S3=@S3+ (cast('0.'+ cast(@i as varchar) as decimal(10,4)) ), 00008: @S4=@S4+ Round (cast('0.'+ cast(@i as varchar) as decimal(10,4)),1), 00009: @S5=@S5+ dbo.RoundBanker(cast('0.'+ cast(@i as varchar) as decimal(10,4)),1), 00010: @S6=@S6+ Ceiling (cast('0.'+ cast(@i as varchar) as decimal(10,4)) ) 00011: 00012: End 00013: select @S1 as 'Floor()',@S2 as 'Left(,3)', @S3 as 'Без округления', @S4 as 'Round(,1)', @S5 as 'RoundBanker(,1)', @S6 as 'Ceiling()'Которые дают следующие результаты:
Floor() Left(,3) Без округления Round(,1) RoundBanker(,1) Ceiling() ------- -------- -------------- --------------- --------- --------- 0.0000 49.6000 53.6500 54.1000 53.7000 100.0000
После того, как вы осознаете разницу в этих цифрах (особенно при подсчете одной и той же суммы денег) вы поймете важность такой казалось бы малозначительной темы как округление...Но продолжим...
В шестом бейсике существуют следующие типа округлений:
00001: Private Sub Form_Load() 00002: 'Бейсик не позволяет напрямую создать тип DECIMAL - только VARIANT с подтипом CDEC, ну или так: 00003: Dim S1 As Currency, S2 As Currency, S3 As Currency, S4 As Currency, S5 As Currency 00004: For i = 0 To 99 00005: S1 = S1 + Excel.WorksheetFunction.RoundDown(CDec("0." & CStr(i)), 1) 00006: S2 = S2 + CDec("0." & CStr(i)) 00007: S3 = S3 + Round(CDec("0." & CStr(i)), 1) 00008: S4 = S4 + Excel.WorksheetFunction.Round(CDec("0." & CStr(i)), 1) 00009: S5 = S5 + Excel.WorksheetFunction.RoundUp(CDec("0." & CStr(i)), 1) 00010: Next 00011: Debug.Print "Excel.RoundDown", "Без округления", "Round", "Excel.Round", "Excel.RoundUP" 00012: Debug.Print S1, S2, S3, S4, S5 00013: End SubКоторые дают следующие результаты:
Excel.RoundDown Без округления Round Excel.Round Excel.RoundUP 49.5 53.55 53.6 54 57.6
Как видите, в результаты округлений в бейсике практически не совпададают с округлениями на уровне SQL... Хотя подсчитывается та же самая сумма вклада!
Именно поэтому в .NET существуют десятки различных механизмов округлений. Вот механизмы для типа DECIMAL:
00001: Sub Main() 00002: Dim S1 As Decimal, S2 As Decimal, S3 As Decimal, S4 As Decimal, S5 As Decimal, S6 As Decimal, S7 As Decimal 00003: Dim S8 As Decimal, S9 As Decimal, S10 As Decimal, S11 As Decimal, S12 As Decimal, S13 As Decimal, S14 As Decimal 00004: For i = 0 To 99 00005: S1 += CDec("0." & i) 00006: 00007: S2 += Math.Round(CDec("0." & i), 1) 00008: S3 += Math.Round(CDec("0." & i), 1, MidpointRounding.AwayFromZero) 00009: S4 += Math.Round(CDec("0." & i), 1, MidpointRounding.ToEven) 00010: 'предыдущие три способа окрушления можно получить и через Decimal.Round 00011: S5 += Decimal.ToOACurrency(CDec("0." & i)) 00012: ' 00013: S6 += Math.Truncate(CDec("0." & i)) 00014: S7 += Math.Floor(CDec("0." & i)) 00015: S8 += Math.Ceiling(CDec("0." & i)) 00016: ' 00017: S9 += CDec(System.Data.SqlTypes.SqlDecimal.Round(CDec("0." & i), 1)) 00018: S10 += CDec(System.Data.SqlTypes.SqlDecimal.Ceiling(CDec("0." & i))) 00019: S11 += CDec(System.Data.SqlTypes.SqlDecimal.Floor(CDec("0." & i))) 00020: ' 00021: 'В .NET2 есть также множество других функций округления, например в System.Xml.Xsl.Runtime.XsltFunctions() 00022: Next 00023: Console.WriteLine("Без округления: " & CStr(S1)) 00024: Console.WriteLine("Math.Round: " & CStr(S2)) 00025: Console.WriteLine("Math.Round (,,MidpointRounding.AwayFromZero): " & CStr(S3)) 00026: Console.WriteLine("Math.Round (,,MidpointRounding.ToEven): " & CStr(S4)) 00027: Console.WriteLine("Decimal.ToOACurrency: " & CStr(S5)) 00028: Console.WriteLine("Math.Truncate: " & CStr(S6)) 00029: Console.WriteLine("Math.Floor: " & CStr(S7)) 00030: Console.WriteLine("Math.Ceiling: " & CStr(S8)) 00031: Console.WriteLine("SqlTypes.SqlDecimal.Round: " & CStr(S9)) 00032: Console.WriteLine("SqlTypes.SqlDecimal.Ceiling: " & CStr(S10)) 00033: Console.WriteLine("SqlTypes.SqlDecimal.Floor: " & CStr(S11)) 00034: Console.ReadLine() 00035: End SubКоторые дают следующие результаты:
Без округления: Math.Round: Math.Round (,,MidpointRounding.AwayFromZero): Math.Round (,,MidpointRounding.ToEven): Decimal.ToOACurrency: 53.55 53.6 54.0 53.6 535500 Math.Truncate: Math.Floor: Math.Ceiling: SqlTypes.SqlDecimal.Round: SqlTypes.SqlDecimal.Ceiling: SqlTypes.SqlDecimal.Floor: 0 0 99 54.00 99 0
Не правда ли, впечатляющие различия !!! А ведь это та же сумма, что подсчитывалась и в SQL и на шестерке!!! Есть даже такая банковская шутка - если у клиента крепкая крыша - то округляем как RoundUP, если крыша дохлая, то округляем RoundDown, а если клиент вообще лох - то округляем его вклад до нуля !!! Ну взлянув на различия в способах округления - можно осознать, что в этой шутке только небольшая доля шутки...
Именно в силу столь различных результатов округлений .NET просто ЗАТОЧЕН для написания СОБСТВЕННЫХ типов округлений. Помимо переопределения функций округления и сравнения (как любых других стандарных функций общего плана) в пространстве имен DECIMAL есть куча методов типа Decimal.op_GreaterThanOrEqual, позволяющих САМОМУ переопределить в коде как именно поступать с пятеркой (или иной цифрой) в последнем знаке.
Кроме того, в NET2 можно вызвать и методы рабочей страницы EXCEL - только почему-то Враппер COM-NET поддерживает только числа с плавающей запятой (но не с DECIMAL). Впрочем есть метод - на тесте он приведен - получить напрямую представление DECIMAL в NET и разобрать его самому. Кроме того, как видите выше - есть методы из пространства имен SqlTypes, однако по странному стечению обстоятельств реультаты они дают ИНЫЕ, чем в SQL.
У меня также вызывает удивление тот факт, что, несмотря на утверждения вышеуказанной микрософтовской статьи, результаты округлений в VB6 - SQL - NET практически не совпадают. А ведь есть еще и VBscript и Jscript! Кроме того, в пространстве Windows.Forms есть куча совершенно ИНЫХ методов округлений (обычно используемых для графики). Кроме того, свое округление есть в XSLT-преобразованиях, как на уровне SQL, так и на уровне System.XML
Кроме того для получения правильных результатов, надо еще учитывать количество ЗНАЧАЩИХ знаков. Ну собственно как и при любых арифметических операциях вообще. О чем речь? А о том, что если два числа известны с некоторой точностью, например 28,65 +/- 0,01 и 28,66 +/- 0,01 то при их умножении и делении точность повышается, в данном случае станет 0,005, а вот при вычитании двух вышеприведенных чисел результат будет РАВЕН НУЛЮ. Как это ни парадоксально может показаться кому-то. Поэтому вопросы округления следует рассматривать в комплексе с вопросами ТОЧНОСТИ результата.
Думаю, после прочтения этой моей заметки вы проникнетесь уважением к слову ОКРУГЛЕНИЕ...
|