feat: add static and median default grade strategies

Implements #2 and #3
main v0.1.0
Dominique Merle 3 years ago
parent 0c764e2582
commit 36c733f501

@ -84,6 +84,49 @@ func main() {
}
```
### Balancing uneven proposals
Sometimes, some proposals receive more judgments than others, and the tallies are unbalanced.
In those cases, a _default judgment_ strategy has to be picked:
#### Static Default Grade
Missing (or "I don't know") judgments are considered of a grade defined in advance,
usually `to reject` in order to incentivize proposals to be explicit and clear.
You may use `PollTally.BalanceWithStaticDefault(defaultGrade)` to that effect:
```go
pollTally := &PollTally{
AmountOfJudges: 10,
Proposals: []*ProposalTally{
{Tally: []uint64{2, 1, 2, 2, 1}}, // Proposal A
{Tally: []uint64{3, 1, 3, 1, 1}}, // Proposal B
{Tally: []uint64{0, 1, 1, 0, 0}}, // Proposal C
// …
},
}
defaultGrade := 0
pollTally.BalanceWithStaticDefault(defaultGrade)
// pollTally was mutated and now contains balanced proposals' tallies
// pollTally.Proposals[0].Tally == {4, 1, 2, 2, 1} // Proposal A is now balanced
// pollTally.Proposals[1].Tally == {4, 1, 3, 1, 1} // Proposal B is now balanced
// pollTally.Proposals[2].Tally == {8, 1, 1, 0, 0} // Proposal C is now balanced
```
#### Median Default Grade
Same behavior as static, but the default grade for each proposal is its median grade.
Use `PollTally.BalanceWithMedianDefault()`.
#### Normalization
> Not implemented yet ; would require either `math/big` for LCM or floating-point arithmetic
> This is part of why deciding on int types is so tricky.
## License

@ -7,6 +7,30 @@ type PollTally struct {
Proposals []*ProposalTally // Tallies of each proposal. Its order is preserved in the result.
}
// Mutates the PollTally
func (pollTally *PollTally) BalanceWithStaticDefault(defaultGrade uint8) (err error) {
for _, proposalTally := range pollTally.Proposals {
proposalErr := proposalTally.FillWithStaticDefault(pollTally.AmountOfJudges, defaultGrade)
if proposalErr != nil {
return proposalErr
}
}
return nil
}
// Mutates the PollTally
func (pollTally *PollTally) BalanceWithMedianDefault() (err error) {
for _, proposalTally := range pollTally.Proposals {
proposalErr := proposalTally.FillWithMedianDefault(pollTally.AmountOfJudges)
if proposalErr != nil {
return proposalErr
}
}
return nil
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
type ProposalTally struct {
Tally []uint64 // Amount of judgments received for each grade, from "worst" grade to "best" grade.
}
@ -23,7 +47,6 @@ func (proposalTally *ProposalTally) Copy() (_ *ProposalTally) {
for _, gradeTally := range proposalTally.Tally {
intTally = append(intTally, gradeTally) // uint64 is copied, hopefully
}
return &ProposalTally{
Tally: intTally,
}
@ -34,7 +57,6 @@ func (proposalTally *ProposalTally) CountJudgments() (_ uint64) {
for _, gradeTally := range proposalTally.Tally {
amountOfJudgments += gradeTally
}
return amountOfJudgments
}
@ -44,6 +66,7 @@ func (proposalTally *ProposalTally) CountAvailableGrades() (_ uint8) {
// Mutates the proposalTally.
func (proposalTally *ProposalTally) RegradeJudgments(fromGrade uint8, intoGrade uint8) (err error) {
if fromGrade == intoGrade {
return nil
}
@ -64,7 +87,6 @@ func (proposalTally *ProposalTally) RegradeJudgments(fromGrade uint8, intoGrade
// Mutates the proposalTally
func (proposalTally *ProposalTally) FillWithStaticDefault(upToAmount uint64, defaultGrade uint8) (err error) {
// More silent integer casting awkwardness… ; we need to fix this
missingAmount := int(upToAmount) - int(proposalTally.CountJudgments())
if missingAmount < 0 {
@ -84,12 +106,10 @@ func (proposalTally *ProposalTally) FillWithStaticDefault(upToAmount uint64, def
// Mutates the proposalTally
func (proposalTally *ProposalTally) FillWithMedianDefault(upToAmount uint64) (err error) {
analysis := proposalTally.Analyze()
fillErr := proposalTally.FillWithStaticDefault(upToAmount, analysis.MedianGrade)
if fillErr != nil {
return fillErr
}
return nil
}

@ -174,7 +174,7 @@ func TestProposalTally_FillWithMedianDefaultSuccesses(t *testing.T) {
}
}
func TestProposalTally_FillWithMedianDefaultFailureAmountToLow(t *testing.T) {
func TestProposalTally_FillWithMedianDefaultFailureAmountTooLow(t *testing.T) {
proposalTally := ProposalTally{Tally: []uint64{0, 1, 0, 1, 2, 3, 4}}
expectedTally := ProposalTally{Tally: []uint64{0, 1, 0, 1, 2, 3, 4}}
err := proposalTally.FillWithMedianDefault(5)
@ -183,3 +183,116 @@ func TestProposalTally_FillWithMedianDefaultFailureAmountToLow(t *testing.T) {
assert.Equal(t, expectedTally.Tally[i], proposalTally.Tally[i], fmt.Sprintf("Grade #%d", i))
}
}
func TestPollTally_BalanceWithStaticDefault(t *testing.T) {
type test struct {
name string
defaultGrade uint8
input PollTally
expected PollTally
}
tests := []test{
{
name: "Basic usage",
defaultGrade: 0,
input: PollTally{
AmountOfJudges: 10,
Proposals: []*ProposalTally{
{Tally: []uint64{1, 2, 2}},
{Tally: []uint64{2, 2, 2}},
{Tally: []uint64{0, 7, 3}},
},
},
expected: PollTally{
AmountOfJudges: 10,
Proposals: []*ProposalTally{
{Tally: []uint64{6, 2, 2}},
{Tally: []uint64{6, 2, 2}},
{Tally: []uint64{0, 7, 3}},
},
},
},
// …
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.input.BalanceWithStaticDefault(tt.defaultGrade)
assert.NoError(t, err, "Balancing should succeed")
for proposalIndex, proposalTally := range tt.input.Proposals {
for i := 0; i < len(proposalTally.Tally); i++ {
assert.Equal(t,
tt.expected.Proposals[proposalIndex].Tally[i],
proposalTally.Tally[i],
fmt.Sprintf("Grade #%d", i))
}
}
})
}
}
func TestProposalTally_BalanceWithStaticDefaultFailureAmountTooLow(t *testing.T) {
pollTally := &PollTally{
AmountOfJudges: 2,
Proposals: []*ProposalTally{
{Tally: []uint64{1, 2, 2}},
{Tally: []uint64{2, 2, 2}},
},
}
err := pollTally.BalanceWithStaticDefault(0)
assert.Error(t, err, "Filling should fail")
}
func TestPollTally_BalanceWithMedianDefault(t *testing.T) {
type test struct {
name string
input PollTally
expected PollTally
}
tests := []test{
{
name: "Basic usage",
input: PollTally{
AmountOfJudges: 10,
Proposals: []*ProposalTally{
{Tally: []uint64{1, 2, 2}},
{Tally: []uint64{2, 2, 2}},
{Tally: []uint64{0, 7, 3}},
},
},
expected: PollTally{
Proposals: []*ProposalTally{
{Tally: []uint64{1, 7, 2}},
{Tally: []uint64{2, 6, 2}},
{Tally: []uint64{0, 7, 3}},
},
},
},
// …
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.input.BalanceWithMedianDefault()
assert.NoError(t, err, "Balancing should succeed")
for proposalIndex, proposalTally := range tt.input.Proposals {
for i := 0; i < len(proposalTally.Tally); i++ {
assert.Equal(t,
tt.expected.Proposals[proposalIndex].Tally[i],
proposalTally.Tally[i],
fmt.Sprintf("Grade #%d", i))
}
}
})
}
}
func TestProposalTally_BalanceWithMedianDefaultFailureAmountTooLow(t *testing.T) {
pollTally := &PollTally{
AmountOfJudges: 2,
Proposals: []*ProposalTally{
{Tally: []uint64{1, 2, 2}},
{Tally: []uint64{2, 2, 2}},
},
}
err := pollTally.BalanceWithMedianDefault()
assert.Error(t, err, "Filling should fail")
}

Loading…
Cancel
Save