Dividing a number with remainders

  • I need help figuring out in SQL Server (TSQL) how to divide a number by either 2,3,4, or 6 and spreading it across that number of rows depending on what we are dividing the number by where it comes back to the original number...That is confusing I know so let me break it down...

    Bill for 143.23 that will be paid out through 2 months...When you divide that by 2, you come back with 71.62, but if you multiply that number by 2, you come back with 143.24, not the amount for the bill...the end result has to be 71.62 for month1 and 71.61 for month2...Basically when there is a remainder, that has to be applied to the first month...

    143.23:

    Month1 = 71.62

    Month2 = 71.61

    Another example...Same amount but have to divide by 6

    143.23

    Month1 = 23.88

    Month2 = 23.87

    Month3 = 23.87

    Month4 = 23.87

    Month5 = 23.87

    Month6 = 23.87

    Thanks

  • Also remember the amounts can change as well what it is divided by...

  • Does this give you any ideas?

    select Ceiling((143.230*100) / 6)/100, floor((143.230*100) / 6)/100

  • Maybe something like this. If you change/remove the third parameter of round() you'll get different results. This will always return the first payment equal or greater than all the other payments.

    ALTER FUNCTION Payments

    (

    @Amount decimal(12,4),

    @Payments int

    )

    RETURNS TABLE AS

    RETURN

    WITH E(n) AS(

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1

    )

    ,cteTally(n) AS(

    SELECT TOP(@Payments) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM E a, E b

    )

    SELECT n AS PaymentNumber,

    CASE WHEN n = 1

    THEN @Amount - (ROUND(@Amount/@Payments, 2,1) * (@Payments - 1))

    ELSE ROUND(@Amount/@Payments, 2,1) END AS PaymentAmount

    FROM cteTally

    GO

    CREATE TABLE #SampleData(

    TotalPayment decimal(12, 4),

    Payments int

    )

    INSERT INTO #SampleData

    VALUES(143.23,2), (143.23,5), (143.23,6)

    SELECT *

    FROM #SampleData

    CROSS APPLY Payments(TotalPayment, Payments)

    GO

    DROP TABLE #SampleData

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • Thank you both for the responses, let me try them out and see what I come up with!

  • djj (1/29/2015)


    Does this give you any ideas?

    select Ceiling((143.230*100) / 6)/100, floor((143.230*100) / 6)/100

    I was happy as I thought this was going to work as it worked for most of my examples, however, it doesnt account for a 2 penny remainder:

    Using the solution above, the result was:

    Month1 = 47.68

    Month2 = 47.67

    Month3 = 47.67

    Totaling 143.02, off by a penny!

    Here is what is needed...

    Example: Amount is 143.03, and have to spread it out over 3 months:

    Expected Result:

    Month1 = 47.69

    Month2 = 47.67

    Month3 = 47.67

    Thanks again for the input though!!!!

  • Louis's code puts the entire remainder in the first payment, which is technically what you asked for. Another approach would be to add 0.01 per month to make up the difference.

    DECLARE @amount DECIMAL(8,2) = 143.23;

    DECLARE @periods INT = 6;

    SELECT Period = number,

    Payment = pmt + CASE WHEN number <= residual THEN 0.01 ELSE 0 END

    FROM master.dbo.spt_values

    CROSS JOIN (

    -- Calculate how many payments need an extra 0.01

    SELECT pmt, residual = CAST((@amount - pmt * @periods) * 100 AS INT)

    FROM (

    -- Calculate the base payment, rounded down

    SELECT pmt = CAST(FLOOR(@amount * 100 / @periods) / 100 AS DECIMAL(8,2))

    ) p

    ) r

    WHERE type = 'P' AND number BETWEEN 1 AND @periods;

  • First thought is that is going to work...Still have to run through some examples but this is looking like the way to go! Thanks Luis

  • Scott Coleman (1/29/2015)


    Louis's code puts the entire remainder in the first payment, which is technically what you asked for. Another approach would be to add 0.01 per month to make up the difference.

    DECLARE @amount DECIMAL(8,2) = 143.23;

    DECLARE @periods INT = 6;

    SELECT Period = number,

    Payment = pmt + CASE WHEN number <= residual THEN 0.01 ELSE 0 END

    FROM master.dbo.spt_values

    CROSS JOIN (

    -- Calculate how many payments need an extra 0.01

    SELECT pmt, residual = CAST((@amount - pmt * @periods) * 100 AS INT)

    FROM (

    -- Calculate the base payment, rounded down

    SELECT pmt = CAST(FLOOR(@amount * 100 / @periods) / 100 AS DECIMAL(8,2))

    ) p

    ) r

    WHERE type = 'P' AND number BETWEEN 1 AND @periods;

    Yes you are correct, that is what I was asking and that is what the result was...I had not worked with it yet, I was going over djj's solution first, then moved on to Luis'...But I believe this is going to do the trick...Now just have to integrate it into my existing code!! Thanks so much guys...

  • Just to give both options together to compare them. The first version including all the remainder in the first month and the second version putting an extra cent per month.

    ALTER FUNCTION Payments

    (

    @Amount decimal(12,4),

    @Payments int

    )

    RETURNS TABLE AS

    RETURN

    WITH E(n) AS(

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1

    )

    ,cteTally(n) AS(

    SELECT TOP(@Payments) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM E a, E b

    )

    SELECT n AS PaymentNumber,

    CASE WHEN n = 1

    THEN @Amount - (ROUND(@Amount/@Payments, 2,1) * (@Payments - 1))

    ELSE ROUND(@Amount/@Payments, 2,1) END AS PaymentAmount

    FROM cteTally

    GO

    ALTER FUNCTION Payments2

    (

    @Amount decimal(12,4),

    @Payments int

    )

    RETURNS TABLE AS

    RETURN

    WITH E(n) AS(

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1

    )

    ,cteTally(n) AS(

    SELECT TOP(@Payments) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM E a, E b

    )

    SELECT n AS PaymentNumber,

    ROUND(@Amount/@Payments, 2,1)

    + CASE WHEN t.n <= r.residual

    THEN 0.01

    ELSE 0 END AS PaymentAmount

    FROM cteTally t

    CROSS JOIN (SELECT (@Amount - (ROUND(@Amount/@Payments, 2,1) * (@Payments))) * 100) r(residual)

    GO

    CREATE TABLE #SampleData(

    TotalPayment decimal(12, 4),

    Payments int

    )

    INSERT INTO #SampleData

    VALUES(143.23,2), (143.23,5), (143.23,6)

    SELECT *

    FROM #SampleData

    CROSS APPLY Payments(TotalPayment, Payments) p1

    CROSS APPLY Payments2(TotalPayment, Payments) p2

    WHERE p1.PaymentNumber = p2.PaymentNumber

    GO

    DROP TABLE #SampleData

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2
  • You might want to read this article:

    Financial Rounding of Allocations [/url]

    The problem solved there differs slightly in that you're trying to allocate an amount across rows with different values, but I'm pretty sure the same technique can be used to achieve this "penny rounding."


    My mantra: No loops! No CURSORs! No RBAR! Hoo-uh![/I]

    My thought question: Have you ever been told that your query runs too fast?

    My advice:
    INDEXing a poor-performing query is like putting sugar on cat food. Yeah, it probably tastes better but are you sure you want to eat it?
    The path of least resistance can be a slippery slope. Take care that fixing your fixes of fixes doesn't snowball and end up costing you more than fixing the root cause would have in the first place.

    Need to UNPIVOT? Why not CROSS APPLY VALUES instead?[/url]
    Since random numbers are too important to be left to chance, let's generate some![/url]
    Learn to understand recursive CTEs by example.[/url]
    [url url=http://www.sqlservercentral.com/articles/St

  • Or maybe it would be best to just skip all of that insufferable reading and do it like this.

    WITH SampleData (ID, Amount, Months) AS

    (

    SELECT 1, 143.23, 2 UNION ALL SELECT 2, 143.25, 6

    ),

    Tally (n) AS

    (

    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))

    FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a(n)

    )

    SELECT ID, Amount, Months, n, DividedAmt, TotalAmt

    ,CorrectedAmt=DividedAmt +

    CASE WHEN ABS((TotalAmt-Amount)*100) <= n-1

    THEN 0

    ELSE .01*SIGN((TotalAmt-Amount))

    END

    FROM

    (

    SELECT ID, Amount, Months, n, DividedAmt

    ,TotalAmt=SUM(DividedAmt) OVER (PARTITION BY ID)

    FROM

    (

    SELECT ID, Amount, Months, n

    ,DividedAmt=CAST(Amount/Months AS DECIMAL(10,2))

    FROM SampleData a

    CROSS APPLY

    (

    SELECT TOP (Months) n

    FROM Tally

    ORDER BY n

    ) b

    ) a

    ) a;

    Note that most of the work (in fact both of the CTEs and the CROSS APPLY of the inner query, is just setting up the data/exploding the rows.

    Edit: Looking back I see that this solution is quite similar to Luisi's. Should have looked before I leaped.


    My mantra: No loops! No CURSORs! No RBAR! Hoo-uh![/I]

    My thought question: Have you ever been told that your query runs too fast?

    My advice:
    INDEXing a poor-performing query is like putting sugar on cat food. Yeah, it probably tastes better but are you sure you want to eat it?
    The path of least resistance can be a slippery slope. Take care that fixing your fixes of fixes doesn't snowball and end up costing you more than fixing the root cause would have in the first place.

    Need to UNPIVOT? Why not CROSS APPLY VALUES instead?[/url]
    Since random numbers are too important to be left to chance, let's generate some![/url]
    Learn to understand recursive CTEs by example.[/url]
    [url url=http://www.sqlservercentral.com/articles/St

  • Luis Cazares (1/29/2015)


    Maybe something like this. If you change/remove the third parameter of round() you'll get different results. This will always return the first payment equal or greater than all the other payments.

    ALTER FUNCTION Payments

    (

    @Amount decimal(12,4),

    @Payments int

    )

    RETURNS TABLE AS

    RETURN

    WITH E(n) AS(

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL

    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1

    )

    ,cteTally(n) AS(

    SELECT TOP(@Payments) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM E a, E b

    )

    SELECT n AS PaymentNumber,

    CASE WHEN n = 1

    THEN @Amount - (ROUND(@Amount/@Payments, 2,1) * (@Payments - 1))

    ELSE ROUND(@Amount/@Payments, 2,1) END AS PaymentAmount

    FROM cteTally

    GO

    CREATE TABLE #SampleData(

    TotalPayment decimal(12, 4),

    Payments int

    )

    INSERT INTO #SampleData

    VALUES(143.23,2), (143.23,5), (143.23,6)

    SELECT *

    FROM #SampleData

    CROSS APPLY Payments(TotalPayment, Payments)

    GO

    DROP TABLE #SampleData

    Luis, or someone else, I am a fellow DBA but I have not seen this before! If it is not too much to ask, can you just kind of explain what this code is doing! I am familiar with CTE's, ROW_NUMBER() OVER, but I guess I am confused as to what the SELECT 1 UNION ALL over and over is really doing...And the WITH E(n), and in the ROW_NUMBER SELECT statement, the FROM E a, E b?

    That has me all confused! I know the output is exactly what I need, I just would like to know how the code is working to achieve it...

    Thanks

  • asm1212 (1/30/2015)


    Luis, or someone else, I am a fellow DBA but I have not seen this before! If it is not too much to ask, can you just kind of explain what this code is doing! I am familiar with CTE's, ROW_NUMBER() OVER, but I guess I am confused as to what the SELECT 1 UNION ALL over and over is really doing...And the WITH E(n), and in the ROW_NUMBER SELECT statement, the FROM E a, E b?

    That has me all confused! I know the output is exactly what I need, I just would like to know how the code is working to achieve it...

    Thanks

    Sure, I'll try to explain.

    The SELECT 1 UNION ALL are just generating rows. If you run the code inside the first CTE, you'll get 12 rows with a single column having always 1. The value is not important, we just need the rows.

    WITH E(n) is declaring a CTE named E which will have only one column named n.

    FROM E a, E b is doing a CROSS JOIN with the CTE to go from 12 rows to 144 (12*12 or 12^2). This is the old join syntax defined by standard SQL-86, while CROSS JOIN is SQL-92 version. E is the name of the CTE and a and b are alias.

    Is this clear enough? I'd suggest you to comment every part that you might have trouble remembering so you can immediately remember what it's doing every time you look at the code.

    Luis C.
    General Disclaimer:
    Are you seriously taking the advice and code from someone from the internet without testing it? Do you at least understand it? Or can it easily kill your server?

    How to post data/code on a forum to get the best help: Option 1 / Option 2

Viewing 14 posts - 1 through 13 (of 13 total)

You must be logged in to reply to this topic. Login to reply