Reputation: 3258
I'm trying to calculate tiered rates for a stay at some lodging. Lets say we have a weekly, half week, and daily rate for a property.
period_name | nights | rate
-------------------------------------
WEEK | 7 | 100
HALFWEEK | 3 | 50
DAY | 1 | 25
How would I query this with a total number of nights and get a break down of what periods qualify, going from longest to shortest? Some examples results
10 nights
We break 10 into (7 days) + (3 days). The 7 days will be at the WEEK rate (100). The 3 days will be at the HALFWEEK rate (50). Here it qualifies for (1 WEEK @ 100) + (1 HALFWEEK @ 50)
period_name | nights | rate | num | subtotal
----------------------------------------------
WEEK | 7 | 100 | 1 | 100
HALFWEEK | 3 | 50 | 1 | 50
4 nights
We break 4 into (3 days) + (1 day). The 3 days will be at the HALFWEEK rate (50). The 1 day will be at the DAY rate (25). Here it qualifies for (1 HALFWEEK @ 50) + (1 DAY @ 25)
period_name | nights | rate | num | subtotal
----------------------------------------------
HALFWEEK | 3 | 50 | 1 | 50
DAY | 1 | 25 | 1 | 25
16 nights
We break 16 into (14 days) + (2 days). The 14 days will be at the WEEK rate (multiplied by 2), (100 * 2). The 2 days will be at the DAY rate (2 x 25). Here it qualifies for (2 WEEK @ 100) + (2 DAY @ 25)
period_name | nights | rate | num | subtotal
----------------------------------------------
WEEK | 7 | 100 | 2 | 200
DAY | 1 | 25 | 2 | 50
I thought about using the lag window function, but now sure how I'd keep track of the days already applied by the previous period.
Upvotes: 1
Views: 800
Reputation: 23756
You can do this with a CTE RECURSIVE query.
http://sqlfiddle.com/#!17/0ac709/1
Tier table (which can be dynamically expanded):
id name days rate
-- --------- ---- ----
1 WEEK 7 100
2 DAYS 1 25
3 HALF_WEEK 3 50
4 MONTH 30 200
Days data:
id num
-- ---
1 10
2 31
3 30
4 19
5 14
6 108
7 3
8 5
9 1
10 2
11 7
Result:
num_id num days total_price
------ --- ----------------------------------------------- -----------
1 10 {"MONTH: 0","WEEK: 1","HALF_WEEK: 1","DAYS: 0"} 150
2 31 {"MONTH: 1","WEEK: 0","HALF_WEEK: 0","DAYS: 1"} 225
3 30 {"MONTH: 1","WEEK: 0","HALF_WEEK: 0","DAYS: 0"} 200
4 19 {"MONTH: 0","WEEK: 2","HALF_WEEK: 1","DAYS: 2"} 300
5 14 {"MONTH: 0","WEEK: 2","HALF_WEEK: 0","DAYS: 0"} 200
6 108 {"MONTH: 3","WEEK: 2","HALF_WEEK: 1","DAYS: 1"} 875
7 3 {"MONTH: 0","WEEK: 0","HALF_WEEK: 1","DAYS: 0"} 50
8 5 {"MONTH: 0","WEEK: 0","HALF_WEEK: 1","DAYS: 2"} 100
9 1 {"MONTH: 0","WEEK: 0","HALF_WEEK: 0","DAYS: 1"} 25
10 2 {"MONTH: 0","WEEK: 0","HALF_WEEK: 0","DAYS: 2"} 50
11 7 {"MONTH: 0","WEEK: 1","HALF_WEEK: 0","DAYS: 0"} 100
The idea:
First I took this query to calculate your result for one value (19):
SELECT
days / 7 as WEEKS,
days % 7 / 3 as HALF_WEEKS,
days % 7 % 3 / 1 as DAYS
FROM
(SELECT 19 as days) s
Here you can see the recursive structure for the module operation terminated by an integer division. Because a more generic version should be necessary I thought about a recursive version. With PostgreSQL WITH RECURSIVE clause this is possible
https://www.postgresql.org/docs/current/static/queries-with.html
So thats the final query
WITH RECURSIVE days_per_tier(row_no, name, days, rate, counts, mods, num_id, num) AS (
SELECT
row_no,
name,
days,
rate,
num.num / days,
num.num % days,
num.id,
num.num
FROM (
SELECT
*,
row_number() over (order by days DESC) as row_no -- C
FROM
testdata.tiers) tiers, -- A
(SELECT id, num FROM testdata.numbers) num -- B
WHERE row_no = 1
UNION
SELECT
days_per_tier.row_no + 1,
tiers.name,
tiers.days,
tiers.rate,
mods / tiers.days, -- D
mods % tiers.days, -- E
days_per_tier.num_id,
days_per_tier.num
FROM
days_per_tier,
(SELECT
*,
row_number() over (order by days DESC) as row_no
FROM testdata.tiers) tiers
WHERE days_per_tier.row_no + 1 = tiers.row_no
)
SELECT
num_id,
num,
array_agg(name || ': ' || counts ORDER BY days DESC) as days,
sum(total_rate_per_tier) as total_price -- G
FROM (
SELECT
*,
rate * counts as total_rate_per_tier -- F
FROM days_per_tier) s
GROUP BY num_id, num
ORDER BY num_Id
The WITH RECURSIVE contains the starting point of the recursion UNION the recursion part. The starting point simply gets the tiers (A) and numbers (B). To order the tiers due to their days I add a row count (C; only necessary if the corresponding ids are not in the right order as in my example. This could happen if you add another tier).
The recursion part takes the previous SELECT result (which is stored in days_per_tier
) and calculates the next remainder and integer division (D, E). All other columns are only for holding the origin values (exception the increasing row counter which is responsible for the recursion itself).
After the recursion the counts and rates are multiplied (F) and then grouped by the origin number id which generated the total sum (G)
Edit: Added the rate function and the sqlfiddle link.
Upvotes: 1
Reputation: 1923
Here what you need to do is first fire an SQL command to retrieve all condition and write down the function for your business logic.
For Example.
I will fire below query into the database.
Select * from table_name order by nights desc
In result, I will get the data sorted by night in descending order that means first will be 7 then 3 then 1.
Let's suppose I need to find for 11 days.
I will fetch the first record which will be 7 and check it will 11.
if(11 > 7){// execute this if in a loop till it's greater then 7, same for 3 & 1
days = 11-7;
price += price_from_db;
package += package_from_db;
}else{
// goto fetch next record and check the above condition with next record.
}
Note: I write down an algorithm instead of language-specific code.
Upvotes: 0