Skip to content

Commit

Permalink
STV last round use "Final Round Surplus" rather than inactive (#884)
Browse files Browse the repository at this point in the history
* STV last round use "Final Round Surplus" rather than inactive

* PR Review: clean up, simpler configs

* fix incorrect transfers

* clean up with ternary operator

* bring text variations within the enum

* PR Review Comments: clean up STATUSES_TO_PRINT
  • Loading branch information
artoonie authored Sep 25, 2024
1 parent 93341ef commit 599f098
Show file tree
Hide file tree
Showing 24 changed files with 187 additions and 104 deletions.
67 changes: 61 additions & 6 deletions src/main/java/network/brightspots/rcv/CastVoteRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,67 @@ Map<String, BigDecimal> getWinnerToFractionalValue() {
// as far as tabulation is concerned, all that matters is whether
// it is active or not.
enum StatusForRound {
ACTIVE,
DID_NOT_RANK_ANY_CANDIDATES,
EXHAUSTED_CHOICE,
INVALIDATED_BY_OVERVOTE,
INVALIDATED_BY_SKIPPED_RANKING,
INVALIDATED_BY_REPEATED_RANKING,
ACTIVE(
false,
"Active",
"active"
),
DID_NOT_RANK_ANY_CANDIDATES(
true,
"Did Not Rank Any Candidates",
"didNotRankAnyCandidates"
),
EXHAUSTED_CHOICE(
true,
"Inactive Ballots by Exhausted Choices",
"exhaustedChoices"
),
INVALIDATED_BY_OVERVOTE(
true,
"Inactive Ballots by Overvotes",
"overvotes"
),
INVALIDATED_BY_SKIPPED_RANKING(
true,
"Inactive Ballots by Skipped Rankings",
"skippedRankings"
),
INVALIDATED_BY_REPEATED_RANKING(
true,
"Inactive Ballots by Repeated Rankings",
"repeatedRankings"
),
FINAL_ROUND_SURPLUS(
false,
"Final Round Surplus",
"finalRoundSurplus"
);

private final boolean isInactiveBallot;
private final String titleCaseKey;
private final String camelCaseKey;

StatusForRound(
boolean isInactiveBallot,
String titleCaseKey,
String camelCaseKey
) {
this.isInactiveBallot = isInactiveBallot;
this.titleCaseKey = titleCaseKey;
this.camelCaseKey = camelCaseKey;
}

public boolean isInactiveBallot() {
return isInactiveBallot;
}

public String getTitleCaseKey() {
return titleCaseKey;
}

public String getCamelCaseKey() {
return camelCaseKey;
}
}

enum VoteOutcomeType {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/network/brightspots/rcv/ContestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@ boolean isNonIntegerWinningThresholdEnabled() {
return rawConfig.rules.nonIntegerWinningThreshold;
}

boolean usesSurpluses() {
return getNumberOfWinners() > 1 && !isMultiSeatBottomsUpUntilNWinnersEnabled();
}

boolean isHareQuotaEnabled() {
return rawConfig.rules.hareQuota;
}
Expand Down
66 changes: 36 additions & 30 deletions src/main/java/network/brightspots/rcv/ResultsWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import javafx.util.Pair;
import network.brightspots.rcv.ContestConfig.TabulateBySlice;
import network.brightspots.rcv.RawContestConfig.CvrSource;
Expand Down Expand Up @@ -78,6 +79,14 @@ class ResultsWriter {
private String timestampString;
// map from round number to residual surplus generated in that round
private Map<Integer, BigDecimal> roundToResidualSurplus;
// statuses to print in all summary files
// (additional fields are added if needed in specific summary filetypes)
private static final List<StatusForRound> STATUSES_TO_PRINT = List.of(
StatusForRound.INVALIDATED_BY_OVERVOTE,
StatusForRound.INVALIDATED_BY_SKIPPED_RANKING,
StatusForRound.EXHAUSTED_CHOICE,
StatusForRound.INVALIDATED_BY_REPEATED_RANKING);


// visible for testing
@SuppressWarnings("WeakerAccess")
Expand Down Expand Up @@ -382,20 +391,9 @@ private void generateSummarySpreadsheet(
csvPrinter.println();
}

List<Pair<String, StatusForRound>> statusesToPrint = new ArrayList<>();
statusesToPrint.add(new Pair<>("Overvotes",
StatusForRound.INVALIDATED_BY_OVERVOTE));
statusesToPrint.add(new Pair<>("Skipped Rankings",
StatusForRound.INVALIDATED_BY_SKIPPED_RANKING));
statusesToPrint.add(new Pair<>("Exhausted Choices",
StatusForRound.EXHAUSTED_CHOICE));
statusesToPrint.add(new Pair<>("Repeated Rankings",
StatusForRound.INVALIDATED_BY_REPEATED_RANKING));

for (Pair<String, StatusForRound> statusToPrint : statusesToPrint) {
csvPrinter.print("Inactive Ballots by " + statusToPrint.getKey());
for (StatusForRound status : STATUSES_TO_PRINT) {
csvPrinter.print(status.getTitleCaseKey());

StatusForRound status = statusToPrint.getValue();
for (int round = 1; round <= numRounds; round++) {
BigDecimal thisRoundInactive = roundTallies.get(round).getBallotStatusTally(status);
csvPrinter.print(thisRoundInactive);
Expand Down Expand Up @@ -455,6 +453,21 @@ private void generateSummarySpreadsheet(
csvPrinter.println();
}

if (config.usesSurpluses()) {
// row for final round surplus (if needed)
csvPrinter.print(StatusForRound.FINAL_ROUND_SURPLUS.getTitleCaseKey());
for (int round = 1; round <= numRounds; round++) {
BigDecimal finalRoundSurplus =
roundTallies.get(round).getBallotStatusTally(StatusForRound.FINAL_ROUND_SURPLUS);
csvPrinter.print(finalRoundSurplus.equals(BigDecimal.ZERO) ? "" : finalRoundSurplus);

// Don't display transfer or percentage of residual surplus
csvPrinter.print("");
csvPrinter.print("");
}
csvPrinter.println();
}

if (isSlice) {
csvPrinter.println();
csvPrinter.print(String.format("*Elect/Eliminate decisions are from the full contest. "
Expand Down Expand Up @@ -1012,23 +1025,16 @@ private Map<String, BigDecimal> updateCandidateNamesInTally(RoundTally roundSumm
}

private Map<String, BigDecimal> getInactiveJsonMap(RoundTally roundTally) {
Map<String, BigDecimal> inactiveMap = new HashMap<>();
Pair<String, StatusForRound>[] statusesToPrint =
new Pair[] {
new Pair<>("overvotes",
StatusForRound.INVALIDATED_BY_OVERVOTE),
new Pair<>("skippedRankings",
StatusForRound.INVALIDATED_BY_SKIPPED_RANKING),
new Pair<>("repeatedRankings",
StatusForRound.INVALIDATED_BY_REPEATED_RANKING),
new Pair<>("exhaustedChoices",
StatusForRound.EXHAUSTED_CHOICE),
};
for (Pair<String, StatusForRound> statusToPrint : statusesToPrint) {
inactiveMap.put(
statusToPrint.getKey(), roundTally.getBallotStatusTally(statusToPrint.getValue()));
Map<String, BigDecimal> result = STATUSES_TO_PRINT.stream()
.collect(Collectors.toMap(StatusForRound::getCamelCaseKey,
roundTally::getBallotStatusTally));

if (config.usesSurpluses() && roundTally.getRoundNumber() == numRounds) {
result.put(StatusForRound.FINAL_ROUND_SURPLUS.getCamelCaseKey(),
roundTally.getBallotStatusTally(StatusForRound.FINAL_ROUND_SURPLUS));
}
return inactiveMap;

return result;
}

// adds action objects to input action list representing all actions applied this round
Expand All @@ -1047,7 +1053,7 @@ private void addActionObjects(
TallyTransfers tallyTransfers) {
// check for valid candidates:
// "drop undeclared write-in" may result in no one actually being eliminated
if (candidates != null && candidates.size() > 0) {
if (candidates != null && !candidates.isEmpty()) {
// transfers contains all vote transfers for this round
// we add one to the round since transfers are currently stored under the round AFTER
// the tallies which triggered them
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/network/brightspots/rcv/RoundTally.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,15 @@ public List<String> getSortedCandidatesByTally() {

private void countBallots() {
inactiveBallotSum = BigDecimal.ZERO;
activeBallotSum = BigDecimal.ZERO;
ballotStatusTallies.forEach(
(statusForRound, tally) -> {
if (statusForRound != StatusForRound.ACTIVE) {
if (statusForRound.isInactiveBallot()) {
inactiveBallotSum = inactiveBallotSum.add(tally);
} else {
activeBallotSum = activeBallotSum.add(tally);
}
});

activeBallotSum = ballotStatusTallies.get(StatusForRound.ACTIVE);
}

private void ensureFinalized() {
Expand Down
43 changes: 21 additions & 22 deletions src/main/java/network/brightspots/rcv/Tabulator.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Set<String> tabulate(Progress progress) throws TabulationAbortedException {
}
// In multi-seat contests, we always redistribute the surplus (if any) unless bottoms-up
// is enabled.
if (config.getNumberOfWinners() > 1 && !config.isMultiSeatBottomsUpUntilNWinnersEnabled()) {
if (config.usesSurpluses()) {
for (String winner : winners) {
BigDecimal candidateVotes = currentRoundTally.getCandidateTally(winner);
// number that were surplus (beyond the required threshold)
Expand Down Expand Up @@ -502,9 +502,8 @@ private boolean shouldContinueTabulating() {
// bottoms-up is enabled, in which case we can stop as soon as we've declared the winners.
keepTabulating =
numWinnersDeclared < config.getNumberOfWinners()
|| (config.getNumberOfWinners() > 1
&& winnerToRound.containsValue(currentRound)
&& !config.isMultiSeatBottomsUpUntilNWinnersEnabled());
|| (config.usesSurpluses()
&& winnerToRound.containsValue(currentRound));
}
return keepTabulating;
}
Expand Down Expand Up @@ -1006,24 +1005,10 @@ private void recordSelectionForCastVoteRecord(
}
}

String outcomeDescription;
switch (statusForRound) {
case ACTIVE -> outcomeDescription = selectedCandidate;
case DID_NOT_RANK_ANY_CANDIDATES -> outcomeDescription =
"did not rank any candidates" + additionalLogText;
case INVALIDATED_BY_OVERVOTE -> outcomeDescription =
"invalidated by overvote" + additionalLogText;
case EXHAUSTED_CHOICE -> outcomeDescription =
"exhausted choice" + additionalLogText;
case INVALIDATED_BY_SKIPPED_RANKING -> outcomeDescription =
"invalidated by skipped ranking" + additionalLogText;
case INVALIDATED_BY_REPEATED_RANKING -> outcomeDescription =
"invalidated by repeated ranking" + additionalLogText;
default ->
// Programming error: we missed a status here
throw new RuntimeException("Unexpected ballot status: " + statusForRound);
}
VoteOutcomeType outcomeType =
final String outcomeDescription = statusForRound == StatusForRound.ACTIVE
? selectedCandidate
: statusForRound.getTitleCaseKey() + additionalLogText;
final VoteOutcomeType outcomeType =
selectedCandidate == null ? VoteOutcomeType.EXHAUSTED : VoteOutcomeType.COUNTED;
cvr.logRoundOutcome(
currentRoundTally.getRoundNumber(),
Expand Down Expand Up @@ -1095,6 +1080,7 @@ && isCandidateContinuing(cvr.getCurrentRecipientOfVote())) {

// iterate through the rankings in this cvr from most to least preferred.
// for each ranking:
// check if it's a final round surplus
// if it results in an overvote or undervote, exhaust the cvr
// if a selected candidate is continuing, count cvr for that candidate
// if no selected candidate is continuing, look at the next ranking
Expand All @@ -1115,6 +1101,19 @@ && isCandidateContinuing(cvr.getCurrentRecipientOfVote())) {
Integer rank = rankCandidatesPair.getKey();
CandidatesAtRanking candidates = rankCandidatesPair.getValue();

// check for final round surplus
if (config.usesSurpluses()
&& config.getNumberOfWinners() == winnerToRound.size()) {
recordSelectionForCastVoteRecord(
cvr,
roundTally,
roundTallyBySlice,
null,
StatusForRound.FINAL_ROUND_SURPLUS,
"");
break;
}

// check for skipped ranking exhaustion
if (config.getMaxSkippedRanksAllowed() != Integer.MAX_VALUE
&& (rank - lastRankSeen > config.getMaxSkippedRanksAllowed() + 1)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ MARY LYNN MCPHERSON,3373,5.67%,5,3378,5.68%,101,3479,5.85%,0.0000,3479.0000,5.85
ISHMAEL ISRAEL,3305,5.55%,5,3310,5.56%,64,3374,5.67%,0.0000,3374.0000,5.67%,-3374.0000,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
CASPER HILL,1280,2.15%,4,1284,2.15%,-1284,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Undeclared Write-ins,342,0.57%,-342,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Active Ballots,59463,,,59174,,,58876,,,58876.0000,,,57861.0000,,,56780.0000,,,54875.0000,,,51478.0000,,,48418.0000,,,42155.0000,,,41977.0000,,
Active Ballots,59463,,,59174,,,58876,,,58876.0000,,,57861.0000,,,56780.0000,,,54875.0000,,,51478.0000,,,48418.0000,,,42155.0000,,,42154.5192,,
Current Round Threshold,14866,,,14866,,,14866,,,14866,,,14866,,,14866,,,14866,,,14866,,,14866,,,14866,,,14866,,
Inactive Ballots by Overvotes,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Skipped Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Exhausted Choices,67,,289,356,,298,654,,0.0000,654.0000,,1015.0000,1669.0000,,1081.0000,2750.0000,,1905.0000,4655.0000,,3397.0000,8052.0000,,3060.0000,11112.0000,,6263.0000,17375.0000,,177.5192,17552.5192,,0
Inactive Ballots by Exhausted Choices,67,,289,356,,298,654,,0.0000,654.0000,,1015.0000,1669.0000,,1081.0000,2750.0000,,1905.0000,4655.0000,,3397.0000,8052.0000,,3060.0000,11112.0000,,6263.0000,17375.0000,,0.0000,17375.0000,,0
Inactive Ballots by Repeated Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots Total,67,,289,356,,298,654,,0.0000,654.0000,,1015.0000,1669.0000,,1081.0000,2750.0000,,1905.0000,4655.0000,,3397.0000,8052.0000,,3060.0000,11112.0000,,6263.0000,17375.0000,,177.5192,17552.5192,,0
Inactive Ballots Total,67,,289,356,,298,654,,0.0000,654.0000,,1015.0000,1669.0000,,1081.0000,2750.0000,,1905.0000,4655.0000,,3397.0000,8052.0000,,3060.0000,11112.0000,,6263.0000,17375.0000,,0.0000,17375.0000,,0
Residual surplus,0,,,0,,,0,,,0,,,0,,,0,,,0,,,0,,,0,,,0,,,0.4808,,
Final Round Surplus,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,177.5192,,
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@
"threshold" : "14866"
}, {
"inactiveBallots" : {
"exhaustedChoices" : "17552.5192",
"exhaustedChoices" : "17375.0000",
"finalRoundSurplus" : "177.5192",
"overvotes" : "0",
"repeatedRankings" : "0",
"skippedRankings" : "0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ MARY LYNN MCPHERSON,3373,5.67%,5,3378,5.68%,101,3479,5.85%,202,3681,6.19%,-3681,
ISHMAEL ISRAEL,3305,5.55%,5,3310,5.56%,64,3374,5.67%,-3374,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
CASPER HILL,1280,2.15%,4,1284,2.15%,-1284,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Undeclared Write-ins,342,0.57%,-342,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Active Ballots,59463,,,59174,,,58876,,,57926,,,56924,,,55274,,,52027,,,49968,,,47268,,,45439.0000,,
Active Ballots,59463,,,59174,,,58876,,,57926,,,56924,,,55274,,,52027,,,49968,,,47268,,,47266.2600,,
Current Round Threshold,19821,,,19821,,,19821,,,19821,,,19821,,,19821,,,19821,,,19821,,,19821,,,19821,,
Inactive Ballots by Overvotes,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Skipped Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Exhausted Choices,67,,289,356,,298,654,,950,1604,,1002,2606,,1650,4256,,3247,7503,,2059,9562,,2700,12262,,1827.2600,14089.2600,,0
Inactive Ballots by Exhausted Choices,67,,289,356,,298,654,,950,1604,,1002,2606,,1650,4256,,3247,7503,,2059,9562,,2700,12262,,0,12262,,0
Inactive Ballots by Repeated Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots Total,67,,289,356,,298,654,,950,1604,,1002,2606,,1650,4256,,3247,7503,,2059,9562,,2700,12262,,1827.2600,14089.2600,,0
Inactive Ballots Total,67,,289,356,,298,654,,950,1604,,1002,2606,,1650,4256,,3247,7503,,2059,9562,,2700,12262,,0,12262,,0
Residual surplus,0,,,0,,,0,,,0,,,0,,,0,,,0,,,0,,,0,,,1.7400,,
Final Round Surplus,,,,,,,,,,,,,,,,,,,,,,,,,,,,1827.2600,,
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@
"threshold" : "19821"
}, {
"inactiveBallots" : {
"exhaustedChoices" : "14089.2600",
"exhaustedChoices" : "12262",
"finalRoundSurplus" : "1827.2600",
"overvotes" : "0",
"repeatedRankings" : "0",
"skippedRankings" : "0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ E,5,20.83%,0,5,20.83%,0.0000,5.0000,20.83%,0.0000,5.0000,20.83%,-5.0000,0,0.0%,0
B,4,16.66%,0,4,16.66%,0,4,16.66%,2.0000,6.0000,25.0%,2.0000,8.0000,33.33%,-1.6000,6.4000,26.66%,0
F,3,12.5%,0,3,12.5%,0.0000,3.0000,12.5%,-3.0000,0,0.0%,0,0,0.0%,0,0,0.0%,0
D,2,8.33%,-2,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Active Ballots,24,,,23,,,23.0000,,,23.0000,,,23.0000,,,19.6000,,
Active Ballots,24,,,23,,,23.0000,,,23.0000,,,23.0000,,,23.0000,,
Inactive Ballots by Overvotes,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Skipped Rankings,1,,1,2,,0,2,,0,2,,0.0000,2.0000,,0.6000,2.6000,,0
Inactive Ballots by Exhausted Choices,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,2.8000,2.8000,,0
Inactive Ballots by Skipped Rankings,1,,1,2,,0,2,,0,2,,0.0000,2.0000,,0.0000,2.0000,,0
Inactive Ballots by Exhausted Choices,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,0.0000,0.0000,,0
Inactive Ballots by Repeated Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots Total,1,,1,2,,0,2,,0,2,,0.0000,2.0000,,3.4000,5.4000,,0
Inactive Ballots Total,1,,1,2,,0,2,,0,2,,0.0000,2.0000,,0.0000,2.0000,,0
Final Round Surplus,,,,,,,,,,,,,,,,3.4000,,

*Elect/Eliminate decisions are from the full contest. All other results on this report are at the precinct level.
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ E,1,20.0%,0,1,20.0%,0.0000,1.0000,20.0%,0.0000,1.0000,20.0%,-1.0000,0,0.0%,0,0,0
B,1,20.0%,0,1,20.0%,0,1,20.0%,0,1,20.0%,1,2,40.0%,-0.4000,1.6000,32.0%,0
F,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
D,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Active Ballots,5,,,5,,,5.0000,,,5.0000,,,5.0000,,,4.4000,,
Active Ballots,5,,,5,,,5.0000,,,5.0000,,,5.0000,,,5.0000,,
Inactive Ballots by Overvotes,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Skipped Rankings,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,0.0000,0.0000,,0
Inactive Ballots by Exhausted Choices,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,0.6000,0.6000,,0
Inactive Ballots by Exhausted Choices,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,0.0000,0.0000,,0
Inactive Ballots by Repeated Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots Total,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,0.6000,0.6000,,0
Inactive Ballots Total,0,,0,0,,0,0,,0,0,,0.0000,0.0000,,0.0000,0.0000,,0
Final Round Surplus,,,,,,,,,,,,,,,,0.6000,,

*Elect/Eliminate decisions are from the full contest. All other results on this report are at the precinct level.
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
"threshold" : "8"
}, {
"inactiveBallots" : {
"exhaustedChoices" : "0.6000",
"exhaustedChoices" : "0.0000",
"finalRoundSurplus" : "0.6000",
"overvotes" : "0",
"repeatedRankings" : "0",
"skippedRankings" : "0.0000"
Expand Down
Loading

0 comments on commit 599f098

Please sign in to comment.