Author: Andy Kellerstrass (@zero5100)
License: MIT
Compatibility: Supports PostgreSQL v9.5+
This is an apportionment function using the Largest Remainder Method (LRM) and the Hare Quota. It is used to divide a given total amount between any number of buckets based on a provided proportion. The apportioned output values are always guaranteed to sum to the input total provided.
Proportions are provided to the second argument in the form of a JSONB object where the keys are the names of the outputs and the values are the proportions. The output object keys will match the input object keys, except that the input proportion value will be replaced with the apportioned amount. Output data keys are not sorted by input order or otherwise due to the nature of key ordering with the JSONB data type. (The nature meaning that JSONB keys don't have an order.)
The default data_scale
is 0, which will cause the output values to behave like integers. By changing the data_scale
to 2 you can use this function to safely divide money proportionally.
TODO: Add Options Documentation
Example: Apportion "integer" data. Example election data from Wikipedia. Uses the default data_scale
of (0).
SELECT apportion_lrm(election_data.total_seats,
jsonb_build_object(
'yellow_seats', election_data.yellow_votes,
'white_seats', election_data.white_votes,
'red_seats', election_data.red_votes,
'green_seats', election_data.green_votes,
'blue_seats', election_data.blue_votes,
'pink_seats', election_data.pink_votes
)
)
FROM (
SELECT 10 AS total_seats,
47000 AS yellow_votes,
16000 AS white_votes,
15800 AS red_votes,
12000 AS green_votes,
6100 AS blue_votes,
3100 AS pink_votes
) AS election_data;
{
"red_seats": 1,
"blue_seats": 1,
"pink_seats": 0,
"green_seats": 1,
"white_seats": 2,
"yellow_seats": 5
}
Example: Working with money. Divide profit proportionally between offices based on the employee count at each office. Uses data_scale
of (2).
SELECT
(
(apportion_result.per_office_profit->>'east_office_profit')::NUMERIC
)::MONEY AS east_office_profit,
(
(apportion_result.per_office_profit->>'west_office_profit')::NUMERIC
)::MONEY AS west_office_profit,
(
(apportion_result.per_office_profit->>'central_office_profit')::NUMERIC
)::MONEY AS central_office_profit,
(
(apportion_result.per_office_profit->>'east_office_profit')::NUMERIC +
(apportion_result.per_office_profit->>'west_office_profit')::NUMERIC +
(apportion_result.per_office_profit->>'central_office_profit')::NUMERIC
)::MONEY AS office_profit_sum
FROM (
SELECT apportion_lrm(ledger.total_profit::NUMERIC,
jsonb_build_object(
'east_office_profit', employee_count.east_office,
'west_office_profit', employee_count.west_office,
'central_office_profit', employee_count.central_office
), data_scale := 2
) AS per_office_profit
FROM (
SELECT '$14,347.77'::MONEY AS total_profit
) AS ledger,
(
SELECT
81 AS east_office,
26 AS west_office,
147 AS central_office
) AS employee_count
) AS apportion_result
east_office_profit | west_office_profit | central_office_profit | office_profit_sum |
---|---|---|---|
$4,575.47 | $1,468.67 | $8,303.63 | $14,347.77 |