Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/main/java/network/brightspots/rcv/StreamingCvrReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import java.io.File;
import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javafx.util.Pair;
import javax.xml.parsers.ParserConfigurationException;
Expand Down Expand Up @@ -60,6 +62,8 @@ final class StreamingCvrReader extends BaseCvrReader {
private final Integer batchColumnIndex;
// 0-based column index of currentPrecinct name (if present)
private final Integer precinctColumnIndex;
// 0-based column index of Ballot Style (if present)
private final Integer ballotStyleColumnIndex;
// optional delimiter for cells that contain multiple candidates
private final String overvoteDelimiter;
private final String overvoteLabel;
Expand All @@ -82,8 +86,12 @@ final class StreamingCvrReader extends BaseCvrReader {
private List<CastVoteRecord> cvrList;
// last rankings cell observed for CVR in progress
private int lastRankSeen;
// has this CVR had any non-blank candidate cells?
private boolean hasSeenAnyNonBlankCandidateCells;
// flag indicating data issues during parsing
private boolean encounteredDataErrors = false;
// Does this ballot style have any empty rankings?
private Map<String, Boolean> ballotStyleHasEmptyRankings = new HashMap<>();

StreamingCvrReader(ContestConfig config, RawContestConfig.CvrSource source) {
super(config, source);
Expand All @@ -104,6 +112,7 @@ final class StreamingCvrReader extends BaseCvrReader {
!isNullOrBlank(source.getPrecinctColumnIndex())
? Integer.parseInt(source.getPrecinctColumnIndex()) - 1
: null;
this.ballotStyleColumnIndex = null; // to be implemented
this.overvoteDelimiter = source.getOvervoteDelimiter();
this.overvoteLabel = source.getOvervoteLabel();
this.skippedRankLabel = source.getSkippedRankLabel();
Expand Down Expand Up @@ -172,6 +181,7 @@ private void beginCvr() {
currentBatch = null;
currentPrecinct = null;
lastRankSeen = 0;
hasSeenAnyNonBlankCandidateCells = false;
}

// complete construction of new CVR object
Expand All @@ -183,6 +193,29 @@ private void endCvr() {
String computedCastVoteRecordId =
String.format("%s-%d", OutputWriter.sanitizeStringForOutput(excelFileName), cvrIndex);

if (ballotStyleColumnIndex != null) {
String ballotStyle = currentCvrData.size() > ballotStyleColumnIndex
? currentCvrData.get(ballotStyleColumnIndex)
: null;

if (ballotStyleHasEmptyRankings.containsKey(ballotStyle)) {
Boolean hasPreviouslySeenNonBlankCandidateCells =
ballotStyleHasEmptyRankings.get(ballotStyle);
if (hasPreviouslySeenNonBlankCandidateCells != hasSeenAnyNonBlankCandidateCells) {
Logger.severe("Ballot style %s has some cast vote records with votes and some without. "
+ "Cast vote record file: %s", ballotStyle, excelFileName);
encounteredDataErrors = true;
} else {
ballotStyleHasEmptyRankings.put(ballotStyle, hasSeenAnyNonBlankCandidateCells);
}
}
}
if (!hasSeenAnyNonBlankCandidateCells) {
Logger.auditable(
"Skipping CVR with no votes for any candidates: %s", computedCastVoteRecordId);
return;
}

// add precinct ID if needed
if (precinctColumnIndex != null) {
if (currentPrecinct == null) {
Expand Down Expand Up @@ -259,6 +292,7 @@ private void cvrCell(int col, String cellData) {

for (String candidate : candidates) {
candidate = candidate.trim();
hasSeenAnyNonBlankCandidateCells |= !candidate.isBlank();
if (candidates.length > 1 && (candidate.isBlank() || candidate.equals(skippedRankLabel))) {
Logger.severe(
"If a cell contains multiple candidates split by the overvote delimiter, "
Expand Down
8 changes: 7 additions & 1 deletion src/test/java/network/brightspots/rcv/TabulatorTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,12 @@ void tabulateByPrecinctWithoutPrecincts() {
@DisplayName("halting error when CVRs have a ranking larger than the max-configured value")
void maxRankingValidationFails() {
runTabulationTest("max_ranking_enforcement",
TabulatorSession.CastVoteRecordGenericParseException.class.toString());
TabulatorSession.CastVoteRecordGenericParseException.class.toString());
}

@Test
@DisplayName("ES&S correctly ignores empty CVRs in multi-contest CVR")
void essMultiContest() {
runTabulationTest("ess_multi_contest");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"tabulatorVersion" : "TEST",
"outputSettings" : {
"contestName" : "ES&S Multi-Contest",
"outputDirectory" : "output",
"contestDate" : "",
"contestJurisdiction" : "",
"contestOffice" : "",
"tabulateByBatch" : false,
"tabulateByPrecinct" : false,
"generateCdfJson" : false
},
"cvrFileSources" : [ {
"filePath" : "ess_multi_contest_cvr.xlsx",
"contestId" : "",
"firstVoteColumnIndex" : "9",
"firstVoteRowIndex" : "2",
"idColumnIndex" : "4",
"batchColumnIndex" : "5",
"precinctColumnIndex" : "7",
"overvoteDelimiter" : "",
"provider" : "ess",
"overvoteLabel" : "overvote",
"skippedRankLabel" : "undervote",
"undeclaredWriteInLabel" : "Write-in",
"treatBlankAsUndeclaredWriteIn" : false
} ],
"candidates" : [ {
"name" : "Mickey Mouse",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "George Washington",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "Luke Skywalker",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "Zelda",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "Abraham Lincoln",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "Michael Jackson",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "Indiana Jones",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "Bugs Bunny",
"excluded" : false,
"aliases" : [ ]
}, {
"name" : "John Oliver",
"excluded" : false,
"aliases" : [ ]
} ],
"rules" : {
"tiebreakMode" : "useCandidateOrder",
"overvoteRule" : "exhaustImmediately",
"winnerElectionMode" : "singleWinnerMajority",
"randomSeed" : "",
"numberOfWinners" : "1",
"multiSeatBottomsUpPercentageThreshold" : "",
"decimalPlacesForVoteArithmetic" : "4",
"maxSkippedRanksAllowed" : "1",
"maxRankingsAllowed" : "5",
"nonIntegerWinningThreshold" : false,
"doesFirstRoundDetermineThreshold" : false,
"hareQuota" : false,
"batchElimination" : false,
"cutoffElimination" : false,
"continueUntilTwoCandidatesRemain" : false,
"stopTabulationEarlyAfterRound" : "",
"exhaustOnDuplicateCandidate" : false,
"rulesDescription" : "",
"treatBlankAsUndeclaredWriteIn" : false
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Contest Information
Generated By,RCTab 2.0.2
CSV Format Version,1
Type of Election,Single-Winner
Contest,ES&S Multi-Contest
Jurisdiction,
Office,
Date,
Winner(s),Bugs Bunny
Final Threshold,18

Contest Summary
Number to be Elected,1
Number of Candidates,10
Total Number of Ballots,44
Number of Undervotes (No Rankings),0

Rounds,Round 1 Votes,% of vote,transfer,Round 2 Votes,% of vote,transfer,Round 3 Votes,% of vote,transfer,Round 4 Votes,% of vote,transfer,Round 5 Votes,% of vote,transfer,Round 6 Votes,% of vote,transfer,Round 7 Votes,% of vote,transfer,Round 8 Votes,% of vote,transfer,Round 9 Votes,% of vote,transfer
Eliminated,George Washington*,,,Undeclared Write-ins,,,Abraham Lincoln*,,,Zelda,,,Mickey Mouse,,,Luke Skywalker,,,Indiana Jones,,,John Oliver,,,,,
Elected,,,,,,,,,,,,,,,,,,,,,,,,,Bugs Bunny,,
Michael Jackson,10,24.39%,0,10,24.39%,0,10,24.39%,2,12,29.26%,0,12,29.26%,2,14,34.14%,0,14,34.14%,0,14,34.14%,0,14,41.17%,0
Bugs Bunny,7,17.07%,0,7,17.07%,0,7,17.07%,0,7,17.07%,0,7,17.07%,0,7,17.07%,3,10,24.39%,7,17,41.46%,3,20,58.82%,0
Luke Skywalker,6,14.63%,0,6,14.63%,0,6,14.63%,0,6,14.63%,0,6,14.63%,0,6,14.63%,-6,0,0.0%,0,0,0.0%,0,0,0.0%,0
Indiana Jones,5,12.19%,0,5,12.19%,0,5,12.19%,0,5,12.19%,2,7,17.07%,0,7,17.07%,0,7,17.07%,-7,0,0.0%,0,0,0.0%,0
John Oliver,5,12.19%,0,5,12.19%,0,5,12.19%,0,5,12.19%,0,5,12.19%,2,7,17.07%,3,10,24.39%,0,10,24.39%,-10,0,0.0%,0
Mickey Mouse,4,9.75%,0,4,9.75%,0,4,9.75%,0,4,9.75%,0,4,9.75%,-4,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Zelda,2,4.87%,0,2,4.87%,0,2,4.87%,0,2,4.87%,-2,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
Abraham Lincoln,2,4.87%,0,2,4.87%,0,2,4.87%,-2,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0,0,0.0%,0
George Washington,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,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,41,,,41,,,41,,,41,,,41,,,41,,,41,,,41,,,34,,
Current Round Threshold,21,,,21,,,21,,,21,,,21,,,21,,,21,,,21,,,18,,
Inactive Ballots by Overvotes,3,,0,3,,0,3,,0,3,,0,3,,0,3,,0,3,,0,3,,0,3,,0
Inactive Ballots by Skipped Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots by Exhausted Choices,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,7,7,,0
Inactive Ballots by Repeated Rankings,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0,0,,0
Inactive Ballots Total,3,,0,3,,0,3,,0,3,,0,3,,0,3,,0,3,,0,3,,7,10,,0

*Tie resolved in accordance with election law
Loading
Loading