diff --git a/example_test.go b/example_test.go index ad1acc7..fe4f9e6 100644 --- a/example_test.go +++ b/example_test.go @@ -19,6 +19,7 @@ func ExampleVoting() { // First vote. if _, err := v.Vote(schulze.Ballot[string]{ "A": 1, + "C": 2, }); err != nil { log.Fatal(err) } @@ -27,19 +28,40 @@ func ExampleVoting() { if _, err := v.Vote(schulze.Ballot[string]{ "A": 1, "B": 1, + }); err != nil { + log.Fatal(err) + } + + // Third vote. + if _, err := v.Vote(schulze.Ballot[string]{ + "A": 1, + "B": 2, "C": 2, }); err != nil { log.Fatal(err) } // Calculate the result. - result, tie := v.Compute() + result, duels, tie := v.Compute() if tie { - log.Fatal("tie") + log.Fatal("Tie") } - fmt.Println("winner:", result[0].Choice) - // Output: winner: A + for duel := duels(); duel != nil; duel = duels() { + winner, defeated := duel.Outcome() + if winner == nil { + fmt.Printf("Options %s and %s are in tie %v\n", duel.Left.Choice, duel.Right.Choice, duel.Left.Strength) + } else { + fmt.Printf("Options %s defeats %s by (%v - %v) = %v votes\n", winner.Choice, defeated.Choice, winner.Strength, defeated.Strength, duel.Left.Strength-defeated.Strength) + } + } + + fmt.Println("Winner:", result[0].Choice) + + // Output: Options A defeats B by (2 - 0) = 2 votes + // Options A defeats C by (3 - 0) = 3 votes + // Options B and C are in tie 0 + // Winner: A } func ExampleNewPreferences() { @@ -64,7 +86,7 @@ func ExampleNewPreferences() { } // Calculate the result. - result, tie := schulze.Compute(preferences, choices) + result, _, tie := schulze.Compute(preferences, choices) if tie { log.Fatal("tie") } diff --git a/schulze.go b/schulze.go index aafc70d..cdd6265 100644 --- a/schulze.go +++ b/schulze.go @@ -180,6 +180,13 @@ func SetChoices[C comparable](preferences []int, current, updated []C) []int { return updatedPreferences } +type Choice[C comparable] struct { + // The choice value. + Value C + // 0-based ordinal number of the choice in the choice slice. + Index int +} + // Result represents a total number of wins for a single choice. type Result[C comparable] struct { // The choice value. @@ -188,26 +195,96 @@ type Result[C comparable] struct { Index int // Number of wins in pairwise comparisons to other choices votings. Wins int - // Total number of votes in wins in pairwise comparisons to other choices - // votings. - // Strength does not effect the winner, and may be less then the - // Strength of the choice with more wins. + // Total number of votes in the weakest link of the strongest path in wins + // in pairwise comparisons to other choices votings. Strength does not + // effect the winner, and may be less then the Strength of the choice with + // more wins. Strength int // Total number of preferred votes (difference between votes of the winner - // choice and the opponent choice) in wins in pairwise comparisons to other - // choices votings. Advantage does not effect the winner, and may be less - // then the Advantage of the choice with more wins. The code with less wins - // and greater Advantage had stronger but fewer wins and that information - // can be taken into the analysis of the results. + // choice and the opponent choice) in the weakest link of the strongest path + // in wins in pairwise comparisons to other choices votings. Advantage does + // not effect the winner, and may be less then the Advantage of the choice + // with more wins. The code with less wins and greater Advantage had + // stronger but fewer wins and that information can be taken into the + // analysis of the results. Advantage int } // Compute calculates a sorted list of choices with the total number of wins for // each of them by reading preferences data previously populated by the Vote // function. If there are multiple winners, tie boolean parameter is true. -func Compute[C comparable](preferences []int, choices []C) (results []Result[C], tie bool) { +func Compute[C comparable](preferences []int, choices []C) (results []Result[C], duels DuelsIterator[C], tie bool) { strengths := calculatePairwiseStrengths(choices, preferences) - return calculateResults(choices, strengths) + results, tie = calculateResults(choices, strengths) + return results, newDuelsIterator(choices, strengths), tie +} + +// DuelsIterator is a function that returns the next Duel ordered by the choice indexes. +type DuelsIterator[C comparable] func() *Duel[C] + +func newDuelsIterator[C comparable](choices []C, strengths []int) (duels DuelsIterator[C]) { + choicesCount := len(choices) + choiceIndexRow := 0 + choiceIndexColumn := 1 + + return func() *Duel[C] { + if choiceIndexRow >= choicesCount || choiceIndexColumn >= choicesCount { + return nil + } + + defer func() { + choiceIndexColumn++ + if choiceIndexColumn >= choicesCount { + choiceIndexRow++ + choiceIndexColumn = choiceIndexRow + 1 + } + }() + + return &Duel[C]{ + Left: ChoiceStrength[C]{ + Choice: choices[choiceIndexRow], + Index: choiceIndexRow, + Strength: strengths[choiceIndexRow*choicesCount+choiceIndexColumn], + }, + Right: ChoiceStrength[C]{ + Choice: choices[choiceIndexColumn], + Index: choiceIndexColumn, + Strength: strengths[choiceIndexColumn*choicesCount+choiceIndexRow], + }, + } + } +} + +// Duel represents a pairwise comparison between two choices that are compared +// by their strongest paths strengths (number of votes in the weakest link of +// the strongest path). +type Duel[C comparable] struct { + Left ChoiceStrength[C] + Right ChoiceStrength[C] +} + +// Outcome returns the the winner and the defeated choice in the pairwise +// comparison of their strengths. If nils are returned, the outcome of the duel +// is a tie. +func (d Duel[C]) Outcome() (winner, defeated *ChoiceStrength[C]) { + if d.Left.Strength > d.Right.Strength { + return &d.Left, &d.Right + } + if d.Right.Strength > d.Left.Strength { + return &d.Right, &d.Left + } + return nil, nil // tie +} + +// ChoiceStrength stores the strength of a choice. The strength is the number of +// votes in the weakest link of the strongest path between votes for different +// choices. +type ChoiceStrength[C comparable] struct { + // The choice value. + Choice C + // 0-based ordinal number of the choice in the choice slice. + Index int + Strength int } type choiceIndex int @@ -337,7 +414,7 @@ func calculateResults[C comparable](choices []C, strengths []int) (results []Res for i := 0; i < choicesCount; i++ { var wins int - var popularity int + var strength int var advantage int for j := 0; j < choicesCount; j++ { @@ -346,7 +423,7 @@ func calculateResults[C comparable](choices []C, strengths []int) (results []Res sji := strengths[j*choicesCount+i] if sij > sji { wins++ - popularity += sij + strength += sij advantage += sij - sji } } @@ -355,7 +432,7 @@ func calculateResults[C comparable](choices []C, strengths []int) (results []Res Choice: choices[i], Index: i, Wins: wins, - Strength: popularity, + Strength: strength, Advantage: advantage, }) } diff --git a/schulze_test.go b/schulze_test.go index 304be40..4a10334 100644 --- a/schulze_test.go +++ b/schulze_test.go @@ -292,7 +292,7 @@ func TestVoting(t *testing.T) { } } - result, tie := schulze.Compute(preferences, tc.choices) + result, _, tie := schulze.Compute(preferences, tc.choices) if tie != tc.tie { t.Errorf("got tie %v, want %v", tie, tc.tie) } @@ -315,7 +315,7 @@ func TestVoting(t *testing.T) { } } - result, tie := v.Compute() + result, _, tie := v.Compute() if tie != tc.tie { t.Errorf("got tie %v, want %v", tie, tc.tie) } @@ -951,7 +951,7 @@ func BenchmarkVoting_Results(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = v.Compute() + _, _, _ = v.Compute() } } @@ -979,7 +979,7 @@ func BenchmarkResults(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = schulze.Compute(preferences, choices) + _, _, _ = schulze.Compute(preferences, choices) } } diff --git a/voting.go b/voting.go index 538810d..3d18ef2 100644 --- a/voting.go +++ b/voting.go @@ -41,6 +41,6 @@ func (v *Voting[C]) SetChoices(updated []C) { // 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) { +func (v *Voting[C]) Compute() (results []Result[C], duels DuelsIterator[C], tie bool) { return Compute(v.preferences, v.choices) }