diff --git a/src/main/java/network/brightspots/rcv/StreamingCvrReader.java b/src/main/java/network/brightspots/rcv/StreamingCvrReader.java index 7a35d16e..dc43463b 100644 --- a/src/main/java/network/brightspots/rcv/StreamingCvrReader.java +++ b/src/main/java/network/brightspots/rcv/StreamingCvrReader.java @@ -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; @@ -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; @@ -86,6 +90,8 @@ final class StreamingCvrReader extends BaseCvrReader { private boolean hasSeenAnyNonBlankCandidateCells; // flag indicating data issues during parsing private boolean encounteredDataErrors = false; + // Does this ballot style have any empty rankings? + private Map ballotStyleHasEmptyRankings = new HashMap<>(); StreamingCvrReader(ContestConfig config, RawContestConfig.CvrSource source) { super(config, source); @@ -106,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(); @@ -186,6 +193,23 @@ 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);