Skip to content

Commit

Permalink
Add Unvote function
Browse files Browse the repository at this point in the history
  • Loading branch information
janos committed Dec 5, 2022
1 parent 0a0e80e commit 68c2772
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 84 deletions.
17 changes: 15 additions & 2 deletions schulze.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ func NewPreferences(choicesLength int) []int {
// have the same rank. Ranks do not have to be in consecutive order.
type Ballot[C comparable] map[C]int

// Vote updates the preferences passed as th first argument with the Ballot
// Vote updates the preferences passed as the first argument with the Ballot
// values.
func Vote[C comparable](preferences []int, choices []C, b Ballot[C]) error {
return vote(preferences, choices, b, 1) // add one to increment every pairwise preference
}

// Unvote removes the Ballot values from the preferences.
func Unvote[C comparable](preferences []int, choices []C, b Ballot[C]) error {
return vote(preferences, choices, b, -1) // subtract one to decrement every pairwise preference
}

// vote updates the preferences with ballot values according to the passed
// choices. The weight is the value which is added to the preferences slice
// values for pairwise wins. If the weight is 1, the ballot is added, and if it
// is -1 the ballot is removed.
func vote[C comparable](preferences []int, choices []C, b Ballot[C], weight int) error {
ranks, choicesCount, err := ballotRanks(choices, b)
if err != nil {
return fmt.Errorf("ballot ranks: %w", err)
Expand All @@ -37,7 +50,7 @@ func Vote[C comparable](preferences []int, choices []C, b Ballot[C]) error {
for _, i := range choices1 {
for _, choices1 := range rest {
for _, j := range choices1 {
preferences[int(i)*choicesCount+int(j)]++
preferences[int(i)*choicesCount+int(j)] += weight
}
}
}
Expand Down
242 changes: 160 additions & 82 deletions schulze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ import (
)

func TestVoting(t *testing.T) {
type ballot[C comparable] struct {
ballot schulze.Ballot[C]
unvote bool
}
for _, tc := range []struct {
name string
choices []string
ballots []schulze.Ballot[string]
ballots []ballot[string]
result []schulze.Result[string]
tie bool
}{
Expand All @@ -37,8 +41,8 @@ func TestVoting(t *testing.T) {
{
name: "single option one vote",
choices: []string{"A"},
ballots: []schulze.Ballot[string]{
{"A": 1},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 0},
Expand All @@ -47,8 +51,8 @@ func TestVoting(t *testing.T) {
{
name: "two options one vote",
choices: []string{"A", "B"},
ballots: []schulze.Ballot[string]{
{"A": 1},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 1},
Expand All @@ -58,9 +62,9 @@ func TestVoting(t *testing.T) {
{
name: "two options two votes",
choices: []string{"A", "B"},
ballots: []schulze.Ballot[string]{
{"A": 1},
{"A": 1, "B": 2},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 1},
Expand All @@ -70,10 +74,10 @@ func TestVoting(t *testing.T) {
{
name: "three options three votes",
choices: []string{"A", "B", "C"},
ballots: []schulze.Ballot[string]{
{"A": 1},
{"A": 1, "B": 2},
{"A": 1, "B": 2, "C": 3},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 3}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 2},
Expand All @@ -84,9 +88,9 @@ func TestVoting(t *testing.T) {
{
name: "tie",
choices: []string{"A", "B", "C"},
ballots: []schulze.Ballot[string]{
{"A": 1},
{"B": 1},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
{ballot: schulze.Ballot[string]{"B": 1}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 1},
Expand All @@ -98,11 +102,11 @@ func TestVoting(t *testing.T) {
{
name: "complex",
choices: []string{"A", "B", "C", "D", "E"},
ballots: []schulze.Ballot[string]{
{"A": 1, "B": 1},
{"B": 1, "C": 1, "A": 2},
{"A": 1, "B": 2, "C": 2},
{"A": 1, "B": 200, "C": 10},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1, "B": 1}},
{ballot: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 4},
Expand All @@ -115,11 +119,11 @@ func TestVoting(t *testing.T) {
{
name: "duplicate choice", // only the first of the duplicate choices should receive votes
choices: []string{"A", "B", "C", "C", "C"},
ballots: []schulze.Ballot[string]{
{"A": 1, "B": 1},
{"B": 1, "C": 1, "A": 2},
{"A": 1, "B": 2, "C": 2},
{"A": 1, "B": 200, "C": 10},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1, "B": 1}},
{ballot: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 4},
Expand All @@ -132,59 +136,59 @@ func TestVoting(t *testing.T) {
{
name: "example from wiki page",
choices: []string{"A", "B", "C", "D", "E"},
ballots: []schulze.Ballot[string]{
{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5},
{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5},
{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5},
{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5},
{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5},

{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5},
{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5},
{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5},
{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5},
{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5},

{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},
{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5},

{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5},
{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5},
{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5},

{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},
{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},
{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},
{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},
{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},
{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},
{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5},

{"C": 1, "B": 2, "A": 3, "D": 4, "E": 5},
{"C": 1, "B": 2, "A": 3, "D": 4, "E": 5},

{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},
{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},
{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},
{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},
{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},
{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},
{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5},

{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}},

{ballot: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}},
{ballot: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}},

{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}},

{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5}},

{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}},

{ballot: schulze.Ballot[string]{"C": 1, "B": 2, "A": 3, "D": 4, "E": 5}},
{ballot: schulze.Ballot[string]{"C": 1, "B": 2, "A": 3, "D": 4, "E": 5}},

{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},
{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},
{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},
{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},
{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},
{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},
{ballot: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}},

{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
{ballot: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}},
},
result: []schulze.Result[string]{
{Choice: "E", Index: 4, Wins: 4},
Expand All @@ -194,14 +198,82 @@ func TestVoting(t *testing.T) {
{Choice: "D", Index: 3, Wins: 0},
},
},
{
name: "unvote single option one vote",
choices: []string{"A"},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
{ballot: schulze.Ballot[string]{"A": 1}, unvote: true},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 0},
},
},
{
name: "unvote two options one vote",
choices: []string{"A", "B"},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1}},
{ballot: schulze.Ballot[string]{"A": 1}, unvote: true},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 0},
{Choice: "B", Index: 1, Wins: 0},
},
tie: true,
},
{
name: "unvote complex",
choices: []string{"A", "B", "C", "D", "E"},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1, "B": 1}},
{ballot: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}, unvote: true},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 3},
{Choice: "B", Index: 1, Wins: 2},
{Choice: "C", Index: 2, Wins: 2},
{Choice: "D", Index: 3, Wins: 0},
{Choice: "E", Index: 4, Wins: 0},
},
},
{
name: "multiple unvote complex",
choices: []string{"A", "B", "C", "D", "E"},
ballots: []ballot[string]{
{ballot: schulze.Ballot[string]{"A": 1, "B": 1}},
{ballot: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 1}, unvote: true},
{ballot: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}},
{ballot: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}, unvote: true},
{ballot: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}, unvote: true},
},
result: []schulze.Result[string]{
{Choice: "A", Index: 0, Wins: 4},
{Choice: "C", Index: 2, Wins: 3},
{Choice: "B", Index: 1, Wins: 2},
{Choice: "D", Index: 3, Wins: 0},
{Choice: "E", Index: 4, Wins: 0},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Run("functional", func(t *testing.T) {
preferences := schulze.NewPreferences(len(tc.choices))

for _, b := range tc.ballots {
if err := schulze.Vote(preferences, tc.choices, b); err != nil {
t.Fatal(err)
if b.unvote {
if err := schulze.Unvote(preferences, tc.choices, b.ballot); err != nil {
t.Fatal(err)
}
} else {
if err := schulze.Vote(preferences, tc.choices, b.ballot); err != nil {
t.Fatal(err)
}
}
}

Expand All @@ -217,8 +289,14 @@ func TestVoting(t *testing.T) {
v := schulze.NewVoting(tc.choices)

for _, b := range tc.ballots {
if err := v.Vote(b); err != nil {
t.Fatal(err)
if b.unvote {
if err := v.Unvote(b.ballot); err != nil {
t.Fatal(err)
}
} else {
if err := v.Vote(b.ballot); err != nil {
t.Fatal(err)
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions voting.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func (v *Voting[C]) Vote(b Ballot[C]) error {
return Vote(v.preferences, v.choices, b)
}

// Unvote removes a voting preferences from a single voting ballot.
func (v *Voting[C]) Unvote(b Ballot[C]) error {
return Unvote(v.preferences, v.choices, b)
}

// Compute calculates a sorted list of choices with the total number of wins for
// each of them. If there are multiple winners, tie boolean parameter is true.
func (v *Voting[C]) Compute() (results []Result[C], tie bool) {
Expand Down

0 comments on commit 68c2772

Please sign in to comment.