Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

prorata.gno

5.47 Kb · 173 lines
  1// Package prorata splits an integer amount across weighted claimants without
  2// leaking or inventing a unit.
  3//
  4// Every payout in a game is this operation: a pot, a roster, and a weight each.
  5// Written the obvious way, `amount*weight/total` per claimant, it is wrong in
  6// two ways that only show up with real money in the contract. The shares are
  7// each rounded down, so their sum is short of the amount by up to one unit per
  8// claimant, and that dust either accumulates in the contract forever or gets
  9// handed to whoever the code happens to pay last. And `amount*weight` overflows
 10// an int64 long before either factor does, which turns a large payout negative.
 11//
 12// Split fixes both. It distributes by largest remainder: floor every share,
 13// then hand the leftover units out one at a time to the claimants whose
 14// truncated fraction was largest, ties going to the lower index. The result
 15// sums to EXACTLY the amount, every time, and the rule is deterministic, so
 16// every node computes the same split and a Render of it does not change between
 17// calls.
 18//
 19// Nothing here reads the chain and nothing holds coins: the caller owns the
 20// roster and the custody. A game built on this package is at
 21// [r/moul/x/games/lastwords](/r/moul/x/games/lastwords/v0).
 22package prorata
 23
 24import (
 25	"errors"
 26	"sort"
 27)
 28
 29const maxInt64 = int64(9223372036854775807)
 30
 31var (
 32	// ErrNegativeAmount is returned when the amount to split is negative.
 33	ErrNegativeAmount = errors.New("prorata: amount must not be negative")
 34	// ErrNegativeWeight is returned when any weight is negative.
 35	ErrNegativeWeight = errors.New("prorata: weights must not be negative")
 36	// ErrNoWeight is returned when there is nobody to pay, or every weight is
 37	// zero. The caller must decide where the amount goes instead: silently
 38	// returning an empty split would strand it.
 39	ErrNoWeight = errors.New("prorata: total weight is zero, nothing to split across")
 40	// ErrOverflow is returned when the arithmetic does not fit in an int64.
 41	ErrOverflow = errors.New("prorata: weights overflow int64")
 42)
 43
 44// Split divides amount across weights and returns one share per weight, in the
 45// same order. The shares sum to exactly amount.
 46//
 47// A zero weight is allowed and receives nothing, including no remainder unit:
 48// somebody with no claim is not paid dust.
 49func Split(amount int64, weights []int64) ([]int64, error) {
 50	if amount < 0 {
 51		return nil, ErrNegativeAmount
 52	}
 53	total, err := Total(weights)
 54	if err != nil {
 55		return nil, err
 56	}
 57	if total == 0 {
 58		return nil, ErrNoWeight
 59	}
 60
 61	shares := make([]int64, len(weights))
 62	if amount == 0 {
 63		return shares, nil
 64	}
 65
 66	// Floor each share, and keep the truncated numerator to rank by.
 67	rems := make([]entry, 0, len(weights))
 68	assigned := int64(0)
 69	for i, w := range weights {
 70		if w == 0 {
 71			continue
 72		}
 73		share, rem, err := divide(amount, w, total)
 74		if err != nil {
 75			return nil, err
 76		}
 77		shares[i] = share
 78		assigned += share
 79		rems = append(rems, entry{index: i, rem: rem})
 80	}
 81
 82	// Hand the leftover out by largest remainder. There are strictly fewer
 83	// leftover units than claimants, so one pass over the ranking is enough.
 84	left := amount - assigned
 85	if left <= 0 {
 86		return shares, nil
 87	}
 88	sort.Sort(byRemainder(rems))
 89	for i := 0; i < len(rems) && left > 0; i++ {
 90		shares[rems[i].index]++
 91		left--
 92	}
 93	return shares, nil
 94}
 95
 96// Share returns what one weight is owed out of amount, rounded down. It is the
 97// "what would I get" query, and deliberately does NOT account for the
 98// remainder: use Split when the shares have to add up.
 99func Share(amount, weight, total int64) (int64, error) {
100	if amount < 0 {
101		return 0, ErrNegativeAmount
102	}
103	if weight < 0 || total < 0 {
104		return 0, ErrNegativeWeight
105	}
106	if total == 0 {
107		return 0, ErrNoWeight
108	}
109	if weight > total {
110		weight = total
111	}
112	share, _, err := divide(amount, weight, total)
113	return share, err
114}
115
116// Total sums weights, refusing an int64 overflow rather than wrapping into a
117// negative total that would make every share nonsense.
118func Total(weights []int64) (int64, error) {
119	sum := int64(0)
120	for _, w := range weights {
121		if w < 0 {
122			return 0, ErrNegativeWeight
123		}
124		if w > maxInt64-sum {
125			return 0, ErrOverflow
126		}
127		sum += w
128	}
129	return sum, nil
130}
131
132// divide returns amount*weight/total rounded down, plus the remainder of that
133// division, without ever computing amount*weight.
134//
135// It splits the amount into whole multiples of total and what is left over:
136// amount = q*total + r, so amount*weight/total is exactly q*weight +
137// r*weight/total. The first term is exact and the second multiplies a value
138// already smaller than total, which is what keeps the product in range.
139func divide(amount, weight, total int64) (share, rem int64, err error) {
140	q := amount / total
141	r := amount % total
142	if weight != 0 && q > maxInt64/weight {
143		return 0, 0, ErrOverflow
144	}
145	whole := q * weight
146	if weight != 0 && r > maxInt64/weight {
147		return 0, 0, ErrOverflow
148	}
149	part := r * weight
150	if whole > maxInt64-part/total {
151		return 0, 0, ErrOverflow
152	}
153	return whole + part/total, part % total, nil
154}
155
156// entry is one claimant's truncated fraction, for ranking the leftover.
157type entry struct {
158	index int
159	rem   int64
160}
161
162// byRemainder ranks by largest truncated remainder, ties by lowest index, so
163// the split is identical on every node.
164type byRemainder []entry
165
166func (b byRemainder) Len() int      { return len(b) }
167func (b byRemainder) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
168func (b byRemainder) Less(i, j int) bool {
169	if b[i].rem != b[j].rem {
170		return b[i].rem > b[j].rem
171	}
172	return b[i].index < b[j].index
173}