counter.gs
// Code mostly by Gulliver, edited by Eluvatar
// Contact Gulliver for permission to distribute
// Contact Eluvatar if you don't know how to contact Gulliver and he'll contact
// Gulliver for you
function fractionalSTVCount() {
//get sheets
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("Raw Ballots");
var results = spreadsheet.getSheetByName("Results");
results.getRange("A1").setValue("Round");
//initialize arrays and hashes
var candidates = [];
var counts_by_round = [];
var statuses = {};
var counts = {};
var seniority = {};
var ballots = {"free":[],"empty":[]};
//get candidates and seniority
var column = 2;
var candidate = sheet.getRange(1,column).getValue();
while(candidate != "") {
candidates.push(candidate);
statuses[candidate] = "unelected";
counts[candidate] = 0;
ballots[candidate] = [];
seniority[candidate] = column;
results.getRange(1,column).setValue(candidate);
column++;
candidate = sheet.getRange(1,column).getValue();
}
//set check column
results.getRange(1,column).setValue("Check");
//get ballots
var row = 2;
var first_rank = sheet.getRange(2,2).getValue();
while(first_rank != "") {
column = 2;
var rank = first_rank;
var ballot = {"value":1};
while(rank != "") {
if(rank == "No opinion ") {
ballot[sheet.getRange(1,column).getValue()] = null;
} else {
ballot[sheet.getRange(1,column).getValue()] = rank;
}
column++;
rank = sheet.getRange(row,column).getValue();
}
ballots.free.push(ballot);
row++;
first_rank = sheet.getRange(row,2).getValue();
}
//more variables and quota
var num_candidates = candidates.length;
var candidates_left = num_candidates;
var seats_left = 5;
var num_ballots = ballots.free.length;
var quota = Math.round((num_ballots / (seats_left + 1)) + 0.5);
//count ballots
var round = 1;
while(seats_left > 0 && candidates_left > 0) {
//assign free ballots to candidates or mark as exhausted;
results.getRange(round + 1, 1).setValue(round);
while(ballots.free.length > 0) {
var ballot = ballots.free.pop();
var best_candidates = [];
var best_rank = num_candidates;
for(i in candidates) {
candidate = candidates[i];
if(statuses[candidate] == "unelected") {
var rank = ballot[candidate];
if(rank != null) {
if(rank < best_rank) {
best_rank = rank;
best_candidates = [];
best_candidates.push(candidate);
} else if(rank == best_rank) {
best_candidates.push(candidate);
}
}
}
}
if(best_candidates.length > 0) {
var new_value = ballot.value / best_candidates.length;
for(i = 0; i < best_candidates.length; i++) {
candidate = best_candidates[i];
counts[candidate] += new_value;
if(i == 0) {
ballot.value = new_value;
ballots[candidate].push(ballot);
} else {
var new_ballot = {"value":new_value};
for(j in candidates) {
new_ballot[candidates[j]] = ballot[candidates[j]];
}
ballots[candidate].push(new_ballot);
}
}
} else {
ballots.empty.push(ballot);
}
}
counts_by_round.push(counts);
//check if any candidate has met quota
var winner_found = false;
var winners = [];
var check = 0;
var cell = null;
for(i = 0; i < candidates.length; i++) {
candidate = candidates[i];
check += counts[candidate];
results.getRange(round + 1, i + 2).setValue(counts[candidate]);
if(statuses[candidate] == "unelected") {
if(counts[candidate] >= quota || candidates_left <= seats_left) {
winner_found = true;
statuses[candidate] = "elected";
winners.push(candidate);
seats_left--;
candidates_left--;
}
}
}
results.getRange(round + 1, candidates.length + 2).setValue(check);
//transfer surpluses or eliminate candidate
if(winner_found) {
for(i in winners) {
candidate = winners[i];
var ratio = (counts[candidate] - quota) / counts[candidate];
if(ratio > 0) {
for(j in ballots[candidate]) {
var ballot = ballots[candidate][j]
var new_ballot = {};
for(k in candidates) {
new_ballot[candidates[k]] = ballot[candidates[k]];
}
new_ballot.value = ballot.value * ratio;
ballots.free.push(new_ballot);
ballot.value -= new_ballot.value;
counts[candidate] -= new_ballot.value;
}
}
}
} else {
var worse_candidate = null;
for(i = 0; i < candidates.length; i++) {
candidate = candidates[i];
if(statuses[candidate] == "unelected") {
if(worse_candidate == null || counts[candidate] < counts[worse_candidate]) {
worse_candidate = candidate;
} else if(counts[candidate] == counts[worse_candidate]) {
var tie_broken = false;
for(j = round - 1; j > 0; j--) {
if(counts_by_round[j][candidate] < counts_by_round[j][worse_candidate]) {
worse_candidate = candidate;
tie_broken = true;
break;
}
}
if(!tie_broken) {
if(seniority[candidate] > seniority[worse_candidate]) {
worse_candidate = candidate;
}
}
}
}
}
statuses[worse_candidate] = "eliminated";
while(ballots[worse_candidate].length > 0) {
ballots.free.push(ballots[worse_candidate].pop());
}
counts[worse_candidate] = 0;
candidates_left--;
}
//update spreadsheet
for(i = 0; i < candidates.length; i++) {
candidate = candidates[i];
var cell = results.getRange(round + 1, i + 2);
if(statuses[candidate] == "elected") {
cell.setBackgroundColor("RoyalBlue");
} else if(statuses[candidate] == "eliminated") {
cell.setBackgroundColor("IndianRed");
}
}
round++;
}
Browser.msgBox("Done counting", Browser.Buttons.OK);
}
Generated by GNU enscript 1.6.4.