|
| 1 | +package score |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "bytes" |
| 8 | + "encoding/binary" |
| 9 | +) |
| 10 | + |
| 11 | +func factorial(x int) int { |
| 12 | + f := 1 |
| 13 | + for i := 2; i <= x; i++ { |
| 14 | + f *= i |
| 15 | + } |
| 16 | + return f |
| 17 | +} |
| 18 | + |
| 19 | +// Returnst he smallest number of bits required to represent the supplied int. |
| 20 | +func log2(x int) int { |
| 21 | + i := 0 |
| 22 | + n := 1 |
| 23 | + for { |
| 24 | + if n >= x { |
| 25 | + return i |
| 26 | + } |
| 27 | + i++ |
| 28 | + n *= 2 |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +func sliceRemove(s []int, i int) []int { |
| 33 | + return append(s[:i], s[i+1:]...) |
| 34 | +} |
| 35 | + |
| 36 | +func lehmerCodeToRowInternal(lehmer int, i int, permutation []int, elements []int) []int { |
| 37 | + f := factorial(i) |
| 38 | + d := lehmer / f |
| 39 | + k := lehmer % f |
| 40 | + permutation = append(permutation, elements[d]) |
| 41 | + elements = sliceRemove(elements, d) |
| 42 | + |
| 43 | + if i == 0 { |
| 44 | + return permutation |
| 45 | + } |
| 46 | + |
| 47 | + return lehmerCodeToRowInternal(k, i-1, permutation, elements) |
| 48 | +} |
| 49 | + |
| 50 | +func lehmerCodeToRow(lehmer int, elementCount int) []int { |
| 51 | + elements := []int{} |
| 52 | + for i := 1; i <= elementCount; i++ { |
| 53 | + elements = append(elements, i) |
| 54 | + } |
| 55 | + return lehmerCodeToRowInternal(lehmer, elementCount-1, []int{}, elements) |
| 56 | +} |
| 57 | + |
| 58 | +func rowEncodingToRows(rowEncoding []byte, numCandidates, numVoters int) [][]int { |
| 59 | + // We'll draw `numVoters` permutations from the byte slice. If there are no remaining bytes, we will assume the next row is a 0. |
| 60 | + // Each row will be a Lehmer Code represented by ceil(ceil(log_2(numCandidates!)) / 8) bytes. |
| 61 | + |
| 62 | + // So we will draw no more than numVoters * ceil(ceil(log_2(numCandidates!)) / 8) bytes from the slice. |
| 63 | + encodingIndex := 0 |
| 64 | + drawBytes := func(n int) []byte { |
| 65 | + b := []byte{} |
| 66 | + for i := 0; i < n; i++ { |
| 67 | + if encodingIndex < len(rowEncoding) { |
| 68 | + b = append(b, rowEncoding[i]) |
| 69 | + encodingIndex++ |
| 70 | + } else { |
| 71 | + b = append(b, 0) |
| 72 | + } |
| 73 | + } |
| 74 | + return b |
| 75 | + } |
| 76 | + |
| 77 | + permutationCount := factorial(numCandidates) |
| 78 | + |
| 79 | + // TODO: By using more bits than we actually need (the ceiling operation), |
| 80 | + // we're wasting bits by not generating unique configurations from them. For every datum, we should be wasting |
| 81 | + // less than a single bit. This will require sub-byte data tracking. |
| 82 | + byteCount := (log2(permutationCount) + 7) / 8 |
| 83 | + |
| 84 | + readLehmerCode := func() int { |
| 85 | + b := drawBytes(byteCount) |
| 86 | + padBytes := 8 - (len(b) % 8) |
| 87 | + for i := 0; i < padBytes; i++ { |
| 88 | + b = append(b, 0) |
| 89 | + } |
| 90 | + // fmt.Printf("b: %#v\n", b) |
| 91 | + var num uint64 |
| 92 | + err := binary.Read(bytes.NewReader(b), binary.LittleEndian, &num) |
| 93 | + if err != nil { |
| 94 | + panic(fmt.Sprintf("error marshalling bytes to int: %v", err)) |
| 95 | + } |
| 96 | + |
| 97 | + return int(num) % permutationCount |
| 98 | + } |
| 99 | + |
| 100 | + // TODO: Support abstained votes with a bit mask. |
| 101 | + rows := [][]int{} |
| 102 | + for i := 0; i < numVoters; i++ { |
| 103 | + rows = append(rows, lehmerCodeToRow(readLehmerCode(), numCandidates)) |
| 104 | + } |
| 105 | + |
| 106 | + return rows |
| 107 | +} |
| 108 | + |
| 109 | +// Returns: |
| 110 | +// - winner - the Condorcet winner if `ok` is true |
| 111 | +// - ok - whether or not there is a Condorcet winner |
| 112 | +func getCondorcetWinner(sumMatrix MatchupMatrix) (int, bool) { |
| 113 | + candidateCount := len(sumMatrix) |
| 114 | + losslessCandidates := []int{} |
| 115 | + for a := 0; a < candidateCount; a++ { |
| 116 | + lossless := true |
| 117 | + for b := 0; b < candidateCount; b++ { |
| 118 | + if a == b { |
| 119 | + continue |
| 120 | + } |
| 121 | + |
| 122 | + if beats(sumMatrix, a, b) != 1 { |
| 123 | + lossless = false |
| 124 | + break |
| 125 | + } |
| 126 | + } |
| 127 | + if lossless == true { |
| 128 | + losslessCandidates = append(losslessCandidates, a) |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + if len(losslessCandidates) == 1 { |
| 133 | + return losslessCandidates[0], true |
| 134 | + } |
| 135 | + |
| 136 | + return 0, false |
| 137 | +} |
| 138 | + |
| 139 | +func FuzzScoreRows(f *testing.F) { |
| 140 | + f.Fuzz(func(t *testing.T, rowEncoding []byte) { |
| 141 | + // TODO: Vary the integer parameters here a bit. |
| 142 | + initialWinnerCount := 7 |
| 143 | + voterCount := 7 |
| 144 | + candidateCount := 9 |
| 145 | + rows := rowEncodingToRows(rowEncoding, candidateCount, voterCount) |
| 146 | + |
| 147 | + var oldLosers []int = nil |
| 148 | + |
| 149 | + for winnerCount := initialWinnerCount; winnerCount >= 1; winnerCount-- { |
| 150 | + t.Run(fmt.Sprintf("WinnerCount=%d", winnerCount), func(t *testing.T) { |
| 151 | + losers, winners, tie, sumMatrix := ScoreRows(rows, winnerCount) |
| 152 | + |
| 153 | + ctxMsg := fmt.Sprintf("winners: %v\nlosers: %v\ntie: %v\nrows: %v\nseed: %v\nsumMatrix: %v", winners, losers, tie, rows, rowEncoding, sumMatrix) |
| 154 | + |
| 155 | + t.Run("WinnerCount", func(t *testing.T) { |
| 156 | + if len(tie) == 0 && len(winners) != winnerCount { |
| 157 | + t.Errorf("selected wrong number of winners, want: %d, got: %d", winnerCount, len(winners)) |
| 158 | + } |
| 159 | + }) |
| 160 | + |
| 161 | + t.Run("ReturnTotal", func(t *testing.T) { |
| 162 | + total := len(winners) + len(losers) + len(tie) |
| 163 | + if total != candidateCount { |
| 164 | + t.Errorf("expected winners, losers, and tie to total %d but got %d\n%s\n", candidateCount, total, ctxMsg) |
| 165 | + } |
| 166 | + }) |
| 167 | + |
| 168 | + t.Run("CondorcetWinner", func(t *testing.T) { |
| 169 | + if cw, ok := getCondorcetWinner(sumMatrix); ok { |
| 170 | + cwInWinners := false |
| 171 | + for _, w := range winners { |
| 172 | + if w == cw { |
| 173 | + cwInWinners = true |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + if !cwInWinners { |
| 178 | + t.Errorf("Condorcet winner %d was not in returned winners\n%s", cw, ctxMsg) |
| 179 | + } |
| 180 | + } |
| 181 | + }) |
| 182 | + |
| 183 | + if oldLosers != nil { |
| 184 | + t.Run("LosersMonotonic", func(t *testing.T) { |
| 185 | + if len(losers) < len(oldLosers) { |
| 186 | + t.Errorf("decreased winner count but losers were not a superset of previous losers") |
| 187 | + } |
| 188 | + |
| 189 | + for i := 0; i < len(oldLosers); i++ { |
| 190 | + if losers[i] != oldLosers[i] { |
| 191 | + t.Errorf("losers at winnerCount=%d (%v) was not a superset of losers at winnerCount=%d (%v)", winnerCount, losers, winnerCount+1, oldLosers) |
| 192 | + } |
| 193 | + } |
| 194 | + }) |
| 195 | + } |
| 196 | + oldLosers = losers |
| 197 | + }) |
| 198 | + } |
| 199 | + |
| 200 | + // To test: |
| 201 | + // x The winners, losers, and tie always sum to the candidates |
| 202 | + // - When decreasing the winner count, the new losers are always a superset of the old ones. |
| 203 | + // - The intersection of the Smith set and the losers is always null |
| 204 | + }) |
| 205 | +} |
0 commit comments