@@ 0,0 1,15 @@
+5,4,0
+4,1,5
+5,4,0
+5,4,0
+0,1,5
+0,1,5
+0,1,5
+5,4,0
+0,1,5
+4,1,5
+5,4,0
+5,4,0
+0,5,1
+0,1,5
+5,4,0
@@ 0,0 1,15 @@
+5,4,0
+4,1,5
+5,4,0
+5,4,0
+0,1,5
+0,1,5
+0,1,5
+5,4,0
+0,1,5
+4,1,5
+5,4,0
+5,4,0
+0,5,4
+0,1,5
+5,4,0
@@ 0,0 1,322 @@
+package main
+
+// score-better-balance calculator
+// from https://www.reddit.com/r/EndFPTP/comments/lil4zz/scorebetterbalance_a_proposal_to_fix_some/
+
+import (
+ "encoding/csv"
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+func main() {
+ /**************************/
+ /* Process the input data */
+ /**************************/
+ input := csv.NewReader(os.Stdin)
+ input.LazyQuotes = true
+ lines, err := input.ReadAll()
+ if err != nil {
+ panic(err)
+ }
+ // All of the row/column values are strings. Turn them into ints
+ ints, badVotes := toInts(lines)
+ if len(badVotes) > 0 {
+ fmt.Println("Vote errors encountered:")
+ // We'll print info about the votes at the end.
+ }
+
+ /*********************/
+ /* The SBB algorithm */
+ /*********************/
+
+ // As defined, R is max-min.
+ R := getRange(ints)
+ // Find the winner purely from score (step 1)
+ // > 1) get the score leader
+ w, _ := winner(ints)
+ // Calculate the alter matrix (step 2). This is a matrix of [0, r, -r] values
+ // `w` in this case is the column index of the leader from (1)
+ // > 2) alter every ballot to let everyone above the score leader +r points
+ // and everyone belowe the score leader -r points (allowing going out of
+ // normal range)
+ diffs := diff(ints, w, R)
+ // Sum the scores and diffs to compute the final (step 3)
+ ttl := total(ints, diffs)
+ // wc is the index of the final winner
+ wc := max(ttl)
+
+ printDetails(wc, lines, badVotes, ints, diffs, ttl)
+}
+
+// getRange finds the maximum and minimum score values in the entire file.
+// This means that if the legal range is 0-5, but nobody scores any
+// candidate above 4, the range will be 0-4.
+func getRange(vs [][]int) int {
+ var max, min int
+ for _, r := range vs {
+ for _, c := range r {
+ if c > max {
+ max = c
+ }
+ if c < min {
+ min = c
+ }
+ }
+ }
+ return max - min
+}
+
+// subtotal sums up the columns, and returns a single row of the sums
+func subtotal(lines [][]int) []int {
+ if len(lines) < 1 {
+ return []int{}
+ }
+ ttls := make([]int, len(lines[0]))
+ for _, row := range lines {
+ for c, cell := range row {
+ ttls[c] += cell
+ }
+ }
+ return ttls
+}
+
+// winner finds the column with the highest sum of values in a matrix,
+// and returns the index of the winning column as well as the calculated
+// score. the winner by score.
+func winner(lines [][]int) (col int, score int) {
+ ts := subtotal(lines)
+ i := max(ts)
+ return i, ts[i]
+}
+
+// max finds the column with the highest score in a row of scores and returns the index of that row.
+func max(cols []int) (col int) {
+ var score int
+ for c, cell := range cols {
+ if cell > score {
+ col = c
+ score = cell
+ }
+ }
+ return col
+}
+
+// diff calculates the difference matrix based on the leader score. This is step 2 in SBB:
+// - Given r, the range of scores -- aka score max - score min
+// - If candidate score > leader score (for this voter), value is +r
+// - If candidate score < leader score (for this voter), value is -r
+// - The leader value is 0
+// - If the value of a canditate has the same value as the voter gave the lead candidate, that value is 0
+// The function returns a matrix where every cell is 0, r, or -r
+func diff(lines [][]int, lidx, r int) [][]int {
+ rv := make([][]int, 0, len(lines))
+ for _, row := range lines {
+ lls := row[lidx]
+ dr := make([]int, len(row))
+ for c, cell := range row {
+ if c == lidx {
+ continue
+ }
+ switch {
+ case cell < lls:
+ dr[c] = -r
+ case cell > lls:
+ dr[c] = r
+ default:
+ dr[c] = 0
+ }
+ }
+ rv = append(rv, dr)
+ }
+ return rv
+}
+
+// total adds two matrixes together. This is part of step 3, where the score totals are modified by the
+// difference matrix ([-r, 0, r]). It then calculates the total for each column and returns a single
+// row of those values, s.t. for each column C, the value is the score S plus the modifier M.
+//
+// The matrixes `lines` and `diffs` must have the same dimension. This is the *only* error that can
+// occur in this function, and it results in an empty array being returned.
+func total(lines [][]int, diffs [][]int) []int {
+ if len(lines) < 1 || len(diffs) != len(lines) {
+ return []int{}
+ }
+ ttls := make([]int, len(lines[0]))
+ for r, row := range lines {
+ dr := diffs[r]
+ if len(dr) != len(row) {
+ return []int{}
+ }
+ for c, cell := range row {
+ ttls[c] += cell + dr[c]
+ }
+ }
+ return ttls
+}
+
+/****************************************************************************************/
+/* The functions below are all utility functions not relevant to the vote calculations; */
+/* they print or convert arrays and values. */
+/****************************************************************************************/
+
+// printDetails cut out from main() to keep main terse; nothing here is
+// relevant to the voting calculation, and is only output.
+func printDetails(wc int, lines, badVotes [][]string, ints [][]int, diffs [][]int, ttl []int) {
+ // The rest of this is just dumping out the calculations.
+ fmt.Printf("Total votes in input: %d\n", len(lines))
+ printWithHeader("Summary of distributions", true, uniq(lines))
+ subttl := subtotal(ints)
+ printWithHeader("Subtotals", false, subttl)
+ subWc := max(subttl)
+ fmt.Printf("Winner by score: %s with %d\n", toS(subWc), subttl[subWc])
+ printWithHeader("Summary of diffs", true, uniq(toString(diffs)))
+ printWithHeader("Modifiers", false, subtotal(diffs))
+ printWithHeader("Totals", false, ttl)
+ fmt.Printf("Highest combined total: %s with %d\n", toS(wc), ttl[wc])
+
+ if len(badVotes) > 0 {
+ fmt.Printf("\n%d records were invalid.", len(badVotes))
+ printWithHeader("The # column is the line number in the input file.", true, badVotes)
+ }
+}
+
+// uniq finds unique vote sets and combines them. The returned array is a
+// collapsed matrix of votes with the first column being the number of votes
+// that voted the same way. This is mostly a utility that makes reasoning
+// about the data easier for humans, and is not used in the voting calculation.
+func uniq(lines [][]string) [][]string {
+ counts := make(map[string]int)
+ for _, row := range lines {
+ rs := strings.Join(row, ",")
+ counts[rs] += 1
+ }
+ rv := make([][]string, 0, len(counts))
+ for k, v := range counts {
+ votes := strings.Split(k, ",")
+ row := make([]string, 0, len(votes)+1)
+ row = append(row, strconv.Itoa(v))
+ row = append(row, votes...)
+ rv = append(rv, row)
+ }
+ return rv
+}
+
+// strToInt converts an array*array of strings to array*array of ints
+// If the cell is empty, it is recorded as 0
+// If there are non-int values in the record, the entire record is kicked out
+// The return errs array contains error records, with the first value [0] being the
+// original array line number.
+func toInts(lines [][]string) (ints [][]int, errs [][]string) {
+ ints = make([][]int, 0, len(lines))
+ errs = make([][]string, 0)
+ var err error
+outer:
+ for r, srow := range lines {
+ irow := make([]int, len(srow))
+ for c, cell := range srow {
+ if cell != "" {
+ irow[c], err = strconv.Atoi(cell)
+ if err != nil {
+ nsrow := append([]string{strconv.Itoa(r)}, srow...)
+ errs = append(errs, nsrow)
+ continue outer
+ }
+ }
+ }
+ ints = append(ints, irow)
+ }
+ return ints, errs
+}
+
+func toString(lines [][]int) [][]string {
+ strs := make([][]string, 0, len(lines))
+ for _, srow := range lines {
+ irow := make([]string, len(srow))
+ for c, cell := range srow {
+ irow[c] = strconv.Itoa(cell)
+ }
+ strs = append(strs, irow)
+ }
+ return strs
+}
+
+// printWithHeader is a utility function to print out a matrix, with an
+// optional header where each column is a candidate (A, B, C, etc). Optionally,
+// the first column is a count of the number of votes that voted exactly
+// the same way.
+func printWithHeader(d string, count bool, v interface{}) {
+ fmt.Println(d)
+ switch a := v.(type) {
+ case [][]string:
+ if count {
+ sort.Slice(a, func(i, j int) bool {
+ return a[i][0] > a[j][0]
+ })
+ }
+ printHeader(len(a[0]), count)
+ for _, r := range a {
+ if !count {
+ fmt.Printf(" \t")
+ }
+ fmt.Println(strings.Join(r, "\t"))
+ }
+ case [][]int:
+ if count {
+ sort.Slice(a, func(i, j int) bool {
+ return a[i][0] > a[j][0]
+ })
+ }
+ printHeader(len(a[0]), count)
+ for _, r := range a {
+ if !count {
+ fmt.Printf(" \t")
+ }
+ for _, c := range r {
+ fmt.Printf("%d\t", c)
+ }
+ fmt.Println()
+ }
+ case []int:
+ if count {
+ sort.Slice(a, func(i, j int) bool {
+ return a[i] > a[j]
+ })
+ }
+ printHeader(len(a), count)
+ if !count {
+ fmt.Printf(" \t")
+ }
+ for _, c := range a {
+ fmt.Printf("%d\t", c)
+ }
+ fmt.Println()
+ default:
+ }
+}
+
+// printHeader prints a line of n candidates starting with candidate A. c
+// determines whether or not the array contains a count as the first column
+// of the number of votes in the first column.
+func printHeader(n int, c bool) {
+ if c {
+ n -= 1
+ }
+ fmt.Printf("#\t")
+ a := byte('A')
+ var i byte
+ m := byte(n)
+ for i = 0; i < m; i++ {
+ fmt.Printf("%s\t", string([]byte{a + i}))
+ }
+ fmt.Println()
+}
+
+// toS converts a column index to a candidate string; column 0 is candidate A,
+// column 1 is candidate B, and so on. 0-based.
+func toS(b int) string {
+ return string([]byte{'A' + byte(b)})
+}