Хорошо. Я почти уверен, что оригинальный алгоритм (как написано) и опубликованный код (как написано) не совсем отвечают на почту для теста, описанного @Mathias.
Мое предполагаемое использование этого алгоритма - немного более конкретное применение. Вместо вычисления% с использованием (@amt / @SumAmt)
, как показано в исходном вопросе. У меня есть фиксированная сумма в долларах, которую нужно разделить или распределить по нескольким элементам на основе% разделения, определенного для каждого из этих элементов. % Разделения составляет 100%, однако, прямое умножение часто приводит к десятичным числам, которые (когда вынуждены округлять до целых $) не составляют общую сумму, которую я разделяю. В этом суть проблемы.
Я вполне уверен, что исходный ответ @Dav не работает в тех случаях, когда (как описано @Mathias) округленные значения равны для нескольких срезов. Эту проблему с исходным алгоритмом и кодом можно суммировать с помощью одного контрольного примера:
Возьмите 100 долларов и разделите его на 3 части, используя 33,333333% в качестве процента.
Использование кода, отправленного @jtw (при условии, что это точная реализация оригинального алгоритма), дает неверный ответ о выделении 33 долл. США для каждого элемента (в результате общая сумма составляет 99 долл.), Поэтому он не проходит тест.
Я думаю, что более точный алгоритм может быть:
- Иметь промежуточную сумму, которая начинается с 0
- Для каждого элемента в группе:
- Рассчитать сумму округленного распределения как
( [Amount to be Split] * [% to Split] )
- Рассчитать совокупный остаток как
[Remainder] + ( [UnRounded Amount] - [Rounded Amount] )
- Если
Round( [Remainder], 0 ) > 1
ИЛИ текущий элемент является ПОСЛЕДНИМ ПУНКТОМ в списке, тогда установите выделение элемента = [Rounded Amount] + Round( [Remainder], 0 )
- еще установить распределение элемента =
[Rounded Amount]
- Повторите для следующего элемента
Реализован в T-SQL, выглядит так:
-- Start of Code --
Drop Table #SplitList
Create Table #SplitList ( idno int , pctsplit decimal(5, 4), amt int , roundedAmt int )
-- Test Case #1
--Insert Into #SplitList Values (1, 0.3333, 100, 0)
--Insert Into #SplitList Values (2, 0.3333, 100, 0)
--Insert Into #SplitList Values (3, 0.3333, 100, 0)
-- Test Case #2
--Insert Into #SplitList Values (1, 0.20, 57, 0)
--Insert Into #SplitList Values (2, 0.20, 57, 0)
--Insert Into #SplitList Values (3, 0.20, 57, 0)
--Insert Into #SplitList Values (4, 0.20, 57, 0)
--Insert Into #SplitList Values (5, 0.20, 57, 0)
-- Test Case #3
--Insert Into #SplitList Values (1, 0.43, 10, 0)
--Insert Into #SplitList Values (2, 0.22, 10, 0)
--Insert Into #SplitList Values (3, 0.11, 10, 0)
--Insert Into #SplitList Values (4, 0.24, 10, 0)
-- Test Case #4
Insert Into #SplitList Values (1, 0.50, 75, 0)
Insert Into #SplitList Values (2, 0.50, 75, 0)
Declare @R Float
Declare @Results Float
Declare @unroundedAmt Float
Declare @idno Int
Declare @roundedAmt Int
Declare @amt Float
Declare @pctsplit Float
declare @rowCnt int
Select @R = 0
select @rowCnt = 0
-- Define the cursor
Declare SplitList Cursor For
Select idno, pctsplit, amt, roundedAmt From #SplitList Order By amt Desc
-- Open the cursor
Open SplitList
-- Assign the values of the first record
Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
-- Loop through the records
While @@FETCH_STATUS = 0
Begin
-- Get derived Amounts from cursor
select @unroundedAmt = ( @amt * @pctsplit )
select @roundedAmt = Round( @unroundedAmt, 0 )
-- Remainder
Select @R = @R + @unroundedAmt - @roundedAmt
select @rowCnt = @rowCnt + 1
-- Magic Happens! (aka Secret Sauce)
if ( round(@R, 0 ) >= 1 ) or ( @@CURSOR_ROWS = @rowCnt ) Begin
select @Results = @roundedAmt + round( @R, 0 )
select @R = @R - round( @R, 0 )
End
else Begin
Select @Results = @roundedAmt
End
If Round(@Results, 0) <> 0
Begin
Update #SplitList Set roundedAmt = @Results Where idno = @idno
End
-- Assign the values of the next record
Fetch Next From SplitList Into @idno, @pctsplit, @amt, @roundedAmt
End
-- Close the cursor
Close SplitList
Deallocate SplitList
-- Now do the check
Select * From #SplitList
Select Sum(roundedAmt), max( amt ),
case when max(amt) <> sum(roundedamt) then 'ERROR' else 'OK' end as Test
From #SplitList
-- End of Code --
Что дает окончательный набор результатов для контрольного примера:
idno pctsplit amt roundedAmt
1 0.3333 100 33
2 0.3333 100 34
3 0.3333 100 33
Насколько я могу судить (и у меня есть несколько тестовых случаев в коде), это обрабатывает все эти ситуации довольно изящно.