diff --git a/errors_test.go b/errors_test.go index 4463b52..44d83f2 100644 --- a/errors_test.go +++ b/errors_test.go @@ -16,7 +16,7 @@ import ( func TestVoting_Vote_UnknownChoiceError(t *testing.T) { v := schulze.NewVoting([]int{0, 2, 5, 7}) - err := v.Vote(schulze.Ballot[int]{20: 1}) + _, err := v.Vote(schulze.Ballot[int]{20: 1}) var verr *schulze.UnknownChoiceError[int] if !errors.As(err, &verr) { t.Fatalf("got error %v, want UnknownChoiceError", err) @@ -33,7 +33,7 @@ func TestVote_UnknownChoiceError(t *testing.T) { choices := []int{0, 2, 5, 7} preferences := schulze.NewPreferences(len(choices)) - err := schulze.Vote(choices, preferences, schulze.Ballot[int]{20: 1}) + _, err := schulze.Vote(choices, preferences, schulze.Ballot[int]{20: 1}) var verr *schulze.UnknownChoiceError[int] if !errors.As(err, &verr) { t.Fatalf("got error %v, want UnknownChoiceError", err) diff --git a/example_test.go b/example_test.go index cb71616..ad1acc7 100644 --- a/example_test.go +++ b/example_test.go @@ -17,14 +17,14 @@ func ExampleVoting() { v := schulze.NewVoting([]string{"A", "B", "C"}) // First vote. - if err := v.Vote(schulze.Ballot[string]{ + if _, err := v.Vote(schulze.Ballot[string]{ "A": 1, }); err != nil { log.Fatal(err) } // Second vote. - if err := v.Vote(schulze.Ballot[string]{ + if _, err := v.Vote(schulze.Ballot[string]{ "A": 1, "B": 1, "C": 2, @@ -48,14 +48,14 @@ func ExampleNewPreferences() { preferences := schulze.NewPreferences(len(choices)) // First vote. - if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{ + if _, err := schulze.Vote(preferences, choices, schulze.Ballot[string]{ "A": 1, }); err != nil { log.Fatal(err) } // Second vote. - if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{ + if _, err := schulze.Vote(preferences, choices, schulze.Ballot[string]{ "A": 1, "B": 1, "C": 2, diff --git a/schulze.go b/schulze.go index d13ad6b..605c485 100644 --- a/schulze.go +++ b/schulze.go @@ -25,15 +25,62 @@ 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 +// Record represents a single vote with ranked choices. It is a list of Ballot +// values. The first ballot is the list with the first choices, the second +// ballot is the list with the second choices, and so on. +type Record[C comparable] [][]C + // 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 +// values. A record of a complete and normalized preferences is returned that +// can be used to unvote. +func Vote[C comparable](preferences []int, choices []C, b Ballot[C]) (Record[C], error) { + ranks, choicesCount, err := ballotRanks(choices, b) + if err != nil { + return nil, fmt.Errorf("ballot ranks: %w", err) + } + + for rank, choices1 := range ranks { + rest := ranks[rank+1:] + for _, i := range choices1 { + for _, choices1 := range rest { + for _, j := range choices1 { + preferences[int(i)*choicesCount+int(j)] += 1 + } + } + } + } + + r := make([][]C, len(ranks)) + for rank, indexes := range ranks { + if r[rank] == nil { + r[rank] = make([]C, 0, len(indexes)) + } + for _, index := range indexes { + r[rank] = append(r[rank], choices[index]) + } + } + + return r, nil } // 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 +func Unvote[C comparable](preferences []int, choices []C, r Record[C]) error { + choicesCount := len(choices) + + for rank, choices1 := range r { + rest := r[rank+1:] + for _, choice1 := range choices1 { + i := getChoiceIndex(choices, choice1) + for _, choices1 := range rest { + for _, choice2 := range choices1 { + j := getChoiceIndex(choices, choice2) + preferences[int(i)*choicesCount+int(j)] -= 1 + } + } + } + } + + return nil } // SetChoices updates the preferences passed as the first argument by changing @@ -60,30 +107,6 @@ func SetChoices[C comparable](preferences []int, current, updated []C) []int { return updatedPreferences } -// 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) - } - - for rank, choices1 := range ranks { - rest := ranks[rank+1:] - for _, i := range choices1 { - for _, choices1 := range rest { - for _, j := range choices1 { - preferences[int(i)*choicesCount+int(j)] += weight - } - } - } - } - - return nil -} - // Result represents a total number of wins for a single choice. type Result[C comparable] struct { // The choice value. diff --git a/schulze_test.go b/schulze_test.go index d23deda..413e6dc 100644 --- a/schulze_test.go +++ b/schulze_test.go @@ -20,8 +20,8 @@ import ( func TestVoting(t *testing.T) { type ballot[C comparable] struct { - ballot schulze.Ballot[C] - unvote bool + vote schulze.Ballot[C] + unvote schulze.Record[C] } for _, tc := range []struct { name string @@ -45,7 +45,7 @@ func TestVoting(t *testing.T) { name: "single option one vote", choices: []string{"A"}, ballots: []ballot[string]{ - {ballot: schulze.Ballot[string]{"A": 1}}, + {vote: schulze.Ballot[string]{"A": 1}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 0}, @@ -55,7 +55,7 @@ func TestVoting(t *testing.T) { name: "two options one vote", choices: []string{"A", "B"}, ballots: []ballot[string]{ - {ballot: schulze.Ballot[string]{"A": 1}}, + {vote: schulze.Ballot[string]{"A": 1}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 1}, @@ -66,8 +66,8 @@ func TestVoting(t *testing.T) { name: "two options two votes", choices: []string{"A", "B"}, ballots: []ballot[string]{ - {ballot: schulze.Ballot[string]{"A": 1}}, - {ballot: schulze.Ballot[string]{"A": 1, "B": 2}}, + {vote: schulze.Ballot[string]{"A": 1}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 1}, @@ -78,9 +78,9 @@ func TestVoting(t *testing.T) { name: "three options three votes", choices: []string{"A", "B", "C"}, 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}}, + {vote: schulze.Ballot[string]{"A": 1}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2, "C": 3}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 2}, @@ -92,8 +92,8 @@ func TestVoting(t *testing.T) { name: "tie", choices: []string{"A", "B", "C"}, ballots: []ballot[string]{ - {ballot: schulze.Ballot[string]{"A": 1}}, - {ballot: schulze.Ballot[string]{"B": 1}}, + {vote: schulze.Ballot[string]{"A": 1}}, + {vote: schulze.Ballot[string]{"B": 1}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 1}, @@ -106,10 +106,10 @@ func TestVoting(t *testing.T) { name: "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}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 1}}, + {vote: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 4}, @@ -123,10 +123,10 @@ 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: []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}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 1}}, + {vote: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 4}, @@ -140,58 +140,58 @@ func TestVoting(t *testing.T) { name: "example from wiki page", choices: []string{"A", "B", "C", "D", "E"}, 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}}, + {vote: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "C": 2, "B": 3, "E": 4, "D": 5}}, + + {vote: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}}, + {vote: schulze.Ballot[string]{"A": 1, "D": 2, "E": 3, "C": 4, "B": 5}}, + + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"B": 1, "E": 2, "D": 3, "A": 4, "C": 5}}, + + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "B": 3, "E": 4, "D": 5}}, + + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "A": 2, "E": 3, "B": 4, "D": 5}}, + + {vote: schulze.Ballot[string]{"C": 1, "B": 2, "A": 3, "D": 4, "E": 5}}, + {vote: schulze.Ballot[string]{"C": 1, "B": 2, "A": 3, "D": 4, "E": 5}}, + + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + {vote: schulze.Ballot[string]{"D": 1, "C": 2, "E": 3, "B": 4, "A": 5}}, + + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, + {vote: schulze.Ballot[string]{"E": 1, "B": 2, "A": 3, "D": 4, "C": 5}}, }, result: []schulze.Result[string]{ {Choice: "E", Index: 4, Wins: 4}, @@ -205,8 +205,8 @@ func TestVoting(t *testing.T) { 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}, + {vote: schulze.Ballot[string]{"A": 1}}, + {unvote: schulze.Record[string]{{"A"}}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 0}, @@ -216,8 +216,8 @@ func TestVoting(t *testing.T) { 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}, + {vote: schulze.Ballot[string]{"A": 1}}, + {unvote: schulze.Record[string]{{"A"}, {"B"}}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 0}, @@ -229,11 +229,11 @@ func TestVoting(t *testing.T) { 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}, + {vote: schulze.Ballot[string]{"A": 1, "B": 1}}, + {vote: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}}, + {unvote: schulze.Record[string]{{"A"}, {"B", "C"}, {"D", "E"}}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 3}, @@ -247,13 +247,13 @@ func TestVoting(t *testing.T) { 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}, + {vote: schulze.Ballot[string]{"A": 1, "B": 1}}, + {vote: schulze.Ballot[string]{"B": 1, "C": 1, "A": 2}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 2, "C": 2}}, + {unvote: schulze.Record[string]{{"A", "B"}, {"C", "D", "E"}}}, + {vote: schulze.Ballot[string]{"A": 1, "B": 200, "C": 10}}, + {unvote: schulze.Record[string]{{"A"}, {"B", "C"}, {"D", "E"}}}, + {unvote: schulze.Record[string]{{"B", "C"}, {"A"}, {"D", "E"}}}, }, result: []schulze.Result[string]{ {Choice: "A", Index: 0, Wins: 4}, @@ -269,12 +269,12 @@ func TestVoting(t *testing.T) { preferences := schulze.NewPreferences(len(tc.choices)) for _, b := range tc.ballots { - if b.unvote { - if err := schulze.Unvote(preferences, tc.choices, b.ballot); err != nil { + if b.unvote != nil { + if err := schulze.Unvote(preferences, tc.choices, b.unvote); err != nil { t.Fatal(err) } } else { - if err := schulze.Vote(preferences, tc.choices, b.ballot); err != nil { + if _, err := schulze.Vote(preferences, tc.choices, b.vote); err != nil { t.Fatal(err) } } @@ -292,12 +292,12 @@ func TestVoting(t *testing.T) { v := schulze.NewVoting(tc.choices) for _, b := range tc.ballots { - if b.unvote { - if err := v.Unvote(b.ballot); err != nil { + if b.unvote != nil { + if err := v.Unvote(b.unvote); err != nil { t.Fatal(err) } } else { - if err := v.Vote(b.ballot); err != nil { + if _, err := v.Vote(b.vote); err != nil { t.Fatal(err) } } @@ -315,6 +315,31 @@ func TestVoting(t *testing.T) { } } +func TestUnvote_afterSetChoices(t *testing.T) { + choices := []string{"A", "B", "C"} + preferences := schulze.NewPreferences(len(choices)) + + ballot := schulze.Ballot[string]{"A": 1, "B": 2} + report, err := schulze.Vote(preferences, choices, ballot) + if err != nil { + t.Fatal(err) + } + + updatedChoices := []string{"A", "D", "B", "C"} + + updatedPreferences := schulze.SetChoices(preferences, choices, updatedChoices) + + if err := schulze.Unvote(updatedPreferences, updatedChoices, report); err != nil { + t.Fatal(err) + } + + wantPreferences := make([]int, len(updatedPreferences)) + + if !reflect.DeepEqual(updatedPreferences, wantPreferences) { + t.Errorf("got preferences %v, want %v", updatedPreferences, wantPreferences) + } +} + func TestSetChoices(t *testing.T) { validatePreferences := func(t *testing.T, updatedPreferences, validationPreferences, currentPreferences []int, currentChoices, updatedChoices []string) { t.Helper() @@ -695,7 +720,7 @@ func TestSetChoices(t *testing.T) { t.Run("functional", func(t *testing.T) { currentPreferences := schulze.NewPreferences(len(tc.current)) for _, b := range tc.ballots { - if err := schulze.Vote(currentPreferences, tc.current, b); err != nil { + if _, err := schulze.Vote(currentPreferences, tc.current, b); err != nil { t.Fatal(err) } } @@ -703,7 +728,7 @@ func TestSetChoices(t *testing.T) { validationPreferences := schulze.NewPreferences(updatedChoicesCount) for _, b := range tc.ballots { b := removeChoices(b, removedChoices(tc.current, tc.updated)) - if err := schulze.Vote(validationPreferences, tc.updated, b); err != nil { + if _, err := schulze.Vote(validationPreferences, tc.updated, b); err != nil { t.Fatal(err) } } @@ -722,7 +747,7 @@ func TestSetChoices(t *testing.T) { t.Run("Voting", func(t *testing.T) { currentVoting := schulze.NewVoting(tc.current) for _, b := range tc.ballots { - if err := currentVoting.Vote(b); err != nil { + if _, err := currentVoting.Vote(b); err != nil { t.Fatal(err) } } @@ -730,7 +755,7 @@ func TestSetChoices(t *testing.T) { validationVoting := schulze.NewVoting(tc.updated) for _, b := range tc.ballots { b := removeChoices(b, removedChoices(tc.current, tc.updated)) - if err := validationVoting.Vote(b); err != nil { + if _, err := validationVoting.Vote(b); err != nil { t.Fatal(err) } } @@ -769,7 +794,7 @@ func BenchmarkVoting_Vote(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - if err := v.Vote(schulze.Ballot[string]{ + if _, err := v.Vote(schulze.Ballot[string]{ "a": 1, }); err != nil { b.Fatal(err) @@ -786,7 +811,7 @@ func BenchmarkVote(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{ + if _, err := schulze.Vote(preferences, choices, schulze.Ballot[string]{ "a": 1, }); err != nil { b.Fatal(err) @@ -811,7 +836,7 @@ func BenchmarkVoting_Results(b *testing.B) { ballot[choices[rand.Intn(choicesCount)]] = 3 ballot[choices[rand.Intn(choicesCount)]] = 20 ballot[choices[rand.Intn(choicesCount)]] = 20 - if err := v.Vote(ballot); err != nil { + if _, err := v.Vote(ballot); err != nil { b.Fatal(err) } } @@ -839,7 +864,7 @@ func BenchmarkResults(b *testing.B) { ballot[choices[rand.Intn(choicesCount)]] = 3 ballot[choices[rand.Intn(choicesCount)]] = 20 ballot[choices[rand.Intn(choicesCount)]] = 20 - if err := schulze.Vote(preferences, choices, ballot); err != nil { + if _, err := schulze.Vote(preferences, choices, ballot); err != nil { b.Fatal(err) } } diff --git a/voting.go b/voting.go index 21d1a31..538810d 100644 --- a/voting.go +++ b/voting.go @@ -22,14 +22,15 @@ func NewVoting[C comparable](choices []C) *Voting[C] { } } -// Vote adds a voting preferences by a single voting ballot. -func (v *Voting[C]) Vote(b Ballot[C]) error { +// Vote adds a voting preferences by a single voting ballot. A record of a +// complete and normalized preferences is returned that can be used to unvote. +func (v *Voting[C]) Vote(b Ballot[C]) (Record[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) +func (v *Voting[C]) Unvote(r Record[C]) error { + return Unvote(v.preferences, v.choices, r) } // SetChoices updates the voting accommodate the changes to the choices. It is