# HG changeset patch # User Sean E. Russell # Date 1613327485 21600 # Sun Feb 14 12:31:25 2021 -0600 # Node ID c7a6b4f30427a70986b06cf29bb03218509ecf89 # Parent 0000000000000000000000000000000000000000 Version 1 diff --git a/go.mod b/go.mod new file mode 100644 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module ser1.net/sbb + +go 1.15 diff --git a/input-1.csv b/input-1.csv new file mode 100644 --- /dev/null +++ b/input-1.csv @@ -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 diff --git a/input-2.csv b/input-2.csv new file mode 100644 --- /dev/null +++ b/input-2.csv @@ -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 diff --git a/main.go b/main.go new file mode 100644 --- /dev/null +++ b/main.go @@ -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)}) +}