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.