Skip to content

Commit

Permalink
Type parameters and performance improvements (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
janos committed Nov 29, 2022
1 parent d23206b commit 0a0e80e
Show file tree
Hide file tree
Showing 11 changed files with 502 additions and 271 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.19

- name: Checkout
uses: actions/checkout@v1
Expand All @@ -34,7 +34,7 @@ jobs:
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.41.1
version: v1.50.0
args: --timeout 10m

- name: Vet
Expand Down
25 changes: 10 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@ The Schulze method is a [Condorcet method](https://en.wikipedia.org/wiki/Condorc

White paper [Markus Schulze, "The Schulze Method of Voting"](https://arxiv.org/pdf/1804.02973.pdf).

## Compute
## Vote and Compute

`Compute(v VoteMatrix) (scores []Score, tie bool)` is the core function in the library. It implements the Schulze method on the most compact required representation of votes, called `VoteMatrix`. It returns the ranked list of choices from the matrix, with the first one as the winner. In case that there are multiple choices with the same score, the returned `tie` boolean flag is true.
`Vote` and `Compute` are the core functions in the library. They implement the Schulze method on the most compact required representation of votes, here called preferences that is properly initialized with the `NewPreferences` function. `Vote` writes the `Ballot` values to the provided preferences and `Compute` returns the ranked list of choices from the preferences, with the first one as the winner. In case that there are multiple choices with the same score, the returned `tie` boolean flag is true.

`VoteMatrix` holds number of votes for every pair of choices. A convenient structure to record this map is implemented as the `Voting` type in this package, but it is not required to be used.
The act of voting represents calling the `Vote` function with a `Ballot` map where keys in the map are choices and values are their rankings. Lowest number represents the highest rank. Not all choices have to be ranked and multiple choices can have the same rank. Ranks do not have to be in consecutive order.

## Voting

`Voting` is the in-memory data structure that allows voting ballots to be submitted, to export the `VoteMatrix` and also to compute the ranked list of choices.
`Voting` holds number of votes for every pair of choices. It is a convenient construct to use when the preferences slice does not have to be exposed, and should be kept safe from accidental mutation. Methods on the Voting type are not safe for concurrent calls.

The act of voting represents calling the `Voting.Vote(b Ballot) error` function with a `Ballot` map where keys in the map are choices and values are their rankings. Lowest number represents the highest rank. Not all choices have to be ranked and multiple choices can have the same rank. Ranks do not have to be in consecutive order.

## Example

Expand All @@ -35,38 +34,34 @@ import (
)

func main() {
// Create a new voting.
e := schulze.NewVoting("A", "B", "C", "D", "E")
choices := []string{"A", "B", "C"}
preferences := schulze.NewPreferences(len(choices))

// First vote.
if err := e.Vote(schulze.Ballot{
if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{
"A": 1,
}); err != nil {
log.Fatal(err)
}

// Second vote.
if err := e.Vote(schulze.Ballot{
if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{
"A": 1,
"B": 1,
"D": 2,
"C": 2,
}); err != nil {
log.Fatal(err)
}

// Calculate the result.
result, tie := e.Compute()
result, tie := schulze.Compute(preferences, choices)
if tie {
log.Fatal("tie")
}
fmt.Println("winner:", result[0].Choice)
}
```

## Alternative voting implementations

Function `Compute` is deliberately left exported with `VoteMatrix` map to allow different voting implementations. The `Voting` type in this package is purely in-memory but in reality, a proper way of authenticating users and storing the voting records are crucial and may require implementation with specific persistence features.

## License

This application is distributed under the BSD-style license found in the [LICENSE](LICENSE) file.
20 changes: 20 additions & 0 deletions bitset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2022, Janoš Guljaš <janos@resenje.org>
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package schulze

type bitSet []uint64

func newBitset(size uint) bitSet {
return bitSet(make([]uint64, size/64+1))
}

func (s bitSet) set(i uint) {
s[i/64] |= 1 << (i % 64)
}

func (s bitSet) isSet(i uint) bool {
return s[i/64]&(1<<(i%64)) != 0
}
46 changes: 46 additions & 0 deletions bitset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2022, Janoš Guljaš <janos@resenje.org>
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package schulze

import (
"math/rand"
"testing"
"time"
)

func TestBitSet(t *testing.T) {
contains := func(i uint, values []uint) bool {
for _, v := range values {
if i == v {
return true
}
}
return false
}

seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
size := uint(r.Intn(12345))
s := newBitset(size)
values := make([]uint, 0)
for i, count := uint(0), uint(r.Intn(100)); i < count; i++ {
values = append(values, uint(r.Intn(int(size))))
}
for _, v := range values {
s.set(v)
}
for i := uint(0); i < size; i++ {
if contains(i, values) {
if !s.isSet(i) {
t.Errorf("expected value %v is not set (seed %v)", i, seed)
}
} else {
if s.isSet(i) {
t.Errorf("value %v is unexpectedly set (seed %v)", i, seed)
}
}
}
}
10 changes: 4 additions & 6 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ package schulze

import "fmt"

// UnknownChoiceError represent an error in case that a choice that is not in
// the voting is used.
type UnknownChoiceError struct {
Choice string
type UnknownChoiceError[C comparable] struct {
Choice C
}

func (e *UnknownChoiceError) Error() string {
return fmt.Sprintf("unknown choice %s", e.Choice)
func (e *UnknownChoiceError[C]) Error() string {
return fmt.Sprintf("schulze: unknown choice %v", e.Choice)
}
47 changes: 47 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2022, Janoš Guljaš <janos@resenje.org>
// All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package schulze_test

import (
"errors"
"strings"
"testing"

"resenje.org/schulze"
)

func TestVoting_Vote_UnknownChoiceError(t *testing.T) {
v := schulze.NewVoting([]int{0, 2, 5, 7})

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)
}
if verr.Choice != 20 {
t.Fatalf("got unknown choice error choice %v, want %v", verr.Choice, 20)
}
if !strings.Contains(verr.Error(), "20") {
t.Fatal("choice index not found in error string")
}
}

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})
var verr *schulze.UnknownChoiceError[int]
if !errors.As(err, &verr) {
t.Fatalf("got error %v, want UnknownChoiceError", err)
}
if verr.Choice != 20 {
t.Fatalf("got unknown choice error choice %v, want %v", verr.Choice, 20)
}
if !strings.Contains(verr.Error(), "20") {
t.Fatal("choice index not found in error string")
}
}
41 changes: 36 additions & 5 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,57 @@ import (

func ExampleVoting() {
// Create a new voting.
e := schulze.NewVoting("A", "B", "C", "D", "E")
v := schulze.NewVoting([]string{"A", "B", "C"})

// First vote.
if err := e.Vote(schulze.Ballot{
if err := v.Vote(schulze.Ballot[string]{
"A": 1,
}); err != nil {
log.Fatal(err)
}

// Second vote.
if err := e.Vote(schulze.Ballot{
if err := v.Vote(schulze.Ballot[string]{
"A": 1,
"B": 1,
"D": 2,
"C": 2,
}); err != nil {
log.Fatal(err)
}

// Calculate the result.
result, tie := e.Compute()
result, tie := v.Compute()
if tie {
log.Fatal("tie")
}
fmt.Println("winner:", result[0].Choice)

// Output: winner: A
}

func ExampleNewPreferences() {
// Create a new voting.
choices := []string{"A", "B", "C"}
preferences := schulze.NewPreferences(len(choices))

// First vote.
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]{
"A": 1,
"B": 1,
"C": 2,
}); err != nil {
log.Fatal(err)
}

// Calculate the result.
result, tie := schulze.Compute(preferences, choices)
if tie {
log.Fatal("tie")
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module resenje.org/schulze

go 1.16
go 1.19
Loading

0 comments on commit 0a0e80e

Please sign in to comment.