Здесь представлена цепочка выражений, полученная из исходных дат ввода и количества. Вы можете легко подать это в свой метод Recurse
, хотя я рекомендую один из других методов генерации месяцев, например, используя таблицу чисел, особенно если даты могут варьироваться в течение многих лет.
Для частичных месяцев он вычисляет долю в зависимости от количества дней, охватываемых в этом месяце. Делитель - это общее количество дней в этом месяце. Иногда бухгалтеры рассматривают месяц как имеющий 30 дней, поэтому вам придется решить, подходит ли это.
Полная сумма делится на полные месяцы, взвешенная одинаково независимо от длины, плюс две частичные суммы, взвешенные по их индивидуальным пропорциям их соответствующих месяцев. Сначала вычисляется полная сумма месяца, и результат округляется; частичные месяцы зависят от этого расчета и отмечают мой комментарий в конце о последствиях округления к копейке. Окончательные результаты должны быть осторожны, чтобы правильно распределить последнюю пенни, чтобы сумма была правильной.
with Expr1 as (
select *,
StartPeriod as RangeStart, EndPeriod as RangeEnd,
case when datediff(month, StartPeriod, EndPeriod) < 1 then null else
datediff(month, StartPeriod, EndPeriod) + 1
- case when datepart(day, StartPeriod) <> 1
then 1 else 0 end
- case when month(EndPeriod) = month(dateadd(day, 1, EndPeriod))
then 1 else 0 end
end as WholeMonths,
case when datepart(day, StartPeriod) <> 1
then 1 else 0 end as IsPartialStart,
case when month(EndPeriod) = month(dateadd(day, 1, EndPeriod))
then 1 else 0 end as IsPartialEnd,
datepart(day, StartPeriod) as StartPartialComplement,
datepart(day, EndPeriod) as EndPartialOffset,
datepart(day,
dateadd(day, -1, dateadd(month, datediff(month, 0, StartPeriod) + 1, 0))
) as StartPartialDaysInMonth,
datepart(day,
dateadd(day, -1, dateadd(month, datediff(month, 0, EndPeriod) + 1, 0))
) as EndPartialDaysInMonth
from #TempData
),
Expr2 as (
select *,
case when IsPartialStart = 1
then StartPartialDaysInMonth - StartPartialComplement + 1
else 0 end as StartPartialDays,
case when IsPartialEnd = 1
then EndPartialOffset else 0 end as EndPartialDays
from Expr1
),
Expr3 as (
select *,
cast(round(Amount/(
WholeMonths
+ StartPartialDays/cast(StartPartialDaysInMonth as float)
+ EndPartialDays/cast(EndPartialDaysInMonth as float)
), 2) as numeric(10, 2)) as WholeMonthAllocation,
StartPartialDays/cast(StartPartialDaysInMonth as float) as StartPartialFraction,
EndPartialDays/cast(EndPartialDaysInMonth as float) as EndPartialFraction
from Expr2
),
Expr4 as (
select *,
cast(case when IsPartialEnd = 0
then Amount - WholeMonthAllocation * WholeMonths
else StartPartialFraction * WholeMonthAllocation
end as numeric(10, 2)) as StartPartialAmount,
cast(case when IsPartialEnd = 0 then 0
else Amount
- WholeMonthAllocation * WholeMonths
- StartPartialFraction * WholeMonthAllocation
end as numeric(10, 2)) as EndPartialAmount
from Expr3
),
...
Из этих значений вы можете определить, какая сумма должна закончиться в конечном результате после того, как вы создали все дополнительные строки. Это выражение сделает трюк, включив ваш исходный запрос. (Поскольку SQL Скрипки был вниз, я не был в состоянии проверить все это :)
... /* all of the above */
Recurse AS (
SELECT
RangeStart, RangeEnd, IsPartialStart, IsPartialEnd,
StartPartialAmount, EndPartialAmount, WholeMonthAllocation,
Company, InvoiceDate, StartPeriod,
CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0)) AS DATE) EOM,
EndPeriod, SchoolDistrict,
case
when datediff(month, RangeStart, RangeEnd) = 0 then Amount
when IsPartialStart = 1 then StartPartialAmount
else WholeMonthAllocation
end as Amount
FROM Expr4
UNION ALL
SELECT
RangeStart, RangeEnd, IsPartialStart, IsPartialEnd,
StartPartialAmount, EndPartialAmount, WholeMonthAllocation,
Company, InvoiceDate,
CAST(DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0) AS DATE) AS StartPeriod,
CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+2,0)) AS DATE) EOM,
EndPeriod, SchoolDistrict,
case
-- final month is when StartPeriod is one month before RangeEnd.
-- remember this is recursive!
when IsPartialEnd = 1 and datediff(month, StartPeriod, RangeEnd) = 1
then EndPartialAmount
else WholeMonthAllocation
end as Amount
FROM Recurse
WHERE EOM < EndPeriod
)
SELECT
Company, InvoiceDate, StartPeriod,
CASE WHEN EndPeriod < EOM THEN EndPeriod ELSE EOM END EndPeriod,
SchoolDistrict, Amount
FROM Recurse
Я добавил/псевдонимами RangeStart
и RangeEnd
значения, чтобы избежать путаницы с StartPeriod
и EndPeriod
, которые вы используете в как вашу временную таблицу, так и выходной запрос. Значения Range- представляют собой начало и конец полного диапазона, а значения Period - - это вычисленные значения, которые вызывают отдельные периоды. Адаптируйте, как вы считаете нужным.
Редактировать # 1: Я понял, что не обработал случай, когда начало и конец падают в том же месяце: возможно, есть более чистый способ сделать все это. Я просто закончил обнуление выражения WholeMonths
, чтобы избежать возможного деления на ноль. Выражение case
в конце выхватывает это условие и просто возвращает исходное значение Amount
. Хотя вам, вероятно, не нужно беспокоиться о том, чтобы разобраться с датой начала и окончания, меня перевернули, я пошел вперед и привязал их все вместе с тем же самым тестом < 1
.
Редактировать # 2: Как только у меня было место, чтобы попробовать это, ваш тестовый пример показал, что округление теряло копейки и подбирало окончательный расчет частичного месяца, даже когда это было фактически одно из целого месяцы. Поэтому мне пришлось приспособиться к тому, чтобы искать случай, когда нет окончательного частичного месяца. Это находится в Expr4
. Я также заметил несколько незначительных синтаксических ошибок, которые вы отметили.
Рекурсивный запрос позволяет видеть месяцы в порядке и немного упрощает логику. Якорь всегда будет начальным месяцем, поэтому ни одна из последних месяцев логики не применяется и аналогично для другой половины запроса. Если вы в конечном итоге переключение это с регулярным присоединиться к таблице чисел вы хотите использовать выражение, как это вместо:
case
when datediff(month, RangeStart, RangeEnd) = 0
then Amount
when IsPartialStart = 1 and is first month...
then StartPartialAmount
when IsPartialEnd = 1 and is final month...
then EndPartialAmount
else WholeMonthAllocation
end as Amount
Edit # 3: Также следует помнить, что этот метод не подходит, когда имея дело с очень небольшими суммами, где округление будет искажать результаты. Примеры:
$ 0.13, разделенный с 02 по 01 декабря, дает [.01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .02 ] $ 0.08, разделённый с 02 января по 01 декабря, дает [.01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01, -.03] $ 0.08 с 31 января по 31 декабря, дает [-.03, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01] $ 0,05, разделенный 31 января до 30 ноября дает [.05, .00, .00, .00, .00, .00, .00, .00, .00, .00, .00] $ 0,05, разделенный 31 января на 01 декабря, дает [.00 , .00, .00, .00, .00, .00, .00, .00, .00, .00, .00, .05] $ 0,30, разделенный с 02 января по 1 марта, дает [.15, .15, .00]
Вы пробовали группу? – Yossi
Можете ли вы просто использовать 'DATEDIFF()', чтобы получить количество месяцев, необходимое для деления суммы на? –
Вам нужно будет описать, как вы хотите сделать распространение. Должны ли все полные месяцы получать равные суммы с остатком, указанным в частичные месяцы в начале? Может ли быть и неполный месяц в конце? Или вы просто хотите выделить по количеству дней в месяце? – shawnt00