2020-01-12 15:12:22 +00:00
|
|
|
package keeper
|
|
|
|
|
|
|
|
import (
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
|
2020-05-11 19:45:00 +00:00
|
|
|
// It uses the largest remainder method: https://en.wikipedia.org/wiki/Largest_remainder_method
|
|
|
|
// See also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
|
2020-01-12 15:12:22 +00:00
|
|
|
func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int {
|
2020-05-11 19:45:00 +00:00
|
|
|
// Limit input to +ve numbers as algorithm hasn't been scoped to work with -ve numbers.
|
2020-01-12 15:12:22 +00:00
|
|
|
if amount.IsNegative() {
|
|
|
|
panic("negative amount")
|
|
|
|
}
|
2020-05-11 19:45:00 +00:00
|
|
|
if len(buckets) < 1 {
|
|
|
|
panic("no buckets")
|
|
|
|
}
|
2020-01-12 15:12:22 +00:00
|
|
|
for _, bucket := range buckets {
|
|
|
|
if bucket.IsNegative() {
|
|
|
|
panic("negative bucket")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-11 19:45:00 +00:00
|
|
|
// 1) Split the amount by weights, recording whole number part and remainder
|
|
|
|
|
2020-01-12 15:12:22 +00:00
|
|
|
totalWeights := totalInts(buckets...)
|
2020-05-11 19:45:00 +00:00
|
|
|
if !totalWeights.IsPositive() {
|
|
|
|
panic("total weights must sum to > 0")
|
|
|
|
}
|
2020-01-12 15:12:22 +00:00
|
|
|
|
|
|
|
quotients := make([]quoRem, len(buckets))
|
|
|
|
for i := range buckets {
|
2020-05-11 19:45:00 +00:00
|
|
|
// amount * ( weight/total_weight )
|
2020-01-12 15:12:22 +00:00
|
|
|
q := amount.Mul(buckets[i]).Quo(totalWeights)
|
|
|
|
r := amount.Mul(buckets[i]).Mod(totalWeights)
|
|
|
|
quotients[i] = quoRem{index: i, quo: q, rem: r}
|
|
|
|
}
|
|
|
|
|
2020-05-11 19:45:00 +00:00
|
|
|
// 2) Calculate total left over from remainders, and apportion it to buckets with the highest remainder (to minimize error)
|
|
|
|
|
|
|
|
// sort by decreasing remainder order
|
2020-01-12 15:12:22 +00:00
|
|
|
sort.Slice(quotients, func(i, j int) bool {
|
2020-05-11 19:45:00 +00:00
|
|
|
return quotients[i].rem.GT(quotients[j].rem)
|
2020-01-12 15:12:22 +00:00
|
|
|
})
|
|
|
|
|
2020-05-11 19:45:00 +00:00
|
|
|
// calculate total left over from remainders
|
2020-01-12 15:12:22 +00:00
|
|
|
allocated := sdk.ZeroInt()
|
|
|
|
for _, qr := range quotients {
|
|
|
|
allocated = allocated.Add(qr.quo)
|
|
|
|
}
|
|
|
|
leftToAllocate := amount.Sub(allocated)
|
|
|
|
|
2020-05-11 19:45:00 +00:00
|
|
|
// apportion according to largest remainder
|
2020-01-12 15:12:22 +00:00
|
|
|
results := make([]sdk.Int, len(quotients))
|
|
|
|
for _, qr := range quotients {
|
|
|
|
results[qr.index] = qr.quo
|
|
|
|
if !leftToAllocate.IsZero() {
|
|
|
|
results[qr.index] = results[qr.index].Add(sdk.OneInt())
|
|
|
|
leftToAllocate = leftToAllocate.Sub(sdk.OneInt())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results
|
|
|
|
}
|
|
|
|
|
|
|
|
type quoRem struct {
|
|
|
|
index int
|
|
|
|
quo sdk.Int
|
|
|
|
rem sdk.Int
|
|
|
|
}
|
|
|
|
|
|
|
|
// totalInts adds together sdk.Ints
|
|
|
|
func totalInts(is ...sdk.Int) sdk.Int {
|
|
|
|
total := sdk.ZeroInt()
|
|
|
|
for _, i := range is {
|
|
|
|
total = total.Add(i)
|
|
|
|
}
|
|
|
|
return total
|
|
|
|
}
|