From 45dbccc76659d0ece0483bd81f7c4e42e86b4a41 Mon Sep 17 00:00:00 2001 From: Armin Samii Date: Wed, 11 Mar 2026 14:58:29 -0400 Subject: [PATCH 1/2] Proof of concept: each ignored ballot style must be 100% ignored --- .../brightspots/rcv/StreamingCvrReader.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/network/brightspots/rcv/StreamingCvrReader.java b/src/main/java/network/brightspots/rcv/StreamingCvrReader.java index a3595f20..9c524413 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; @@ -84,6 +88,8 @@ final class StreamingCvrReader extends BaseCvrReader { private int lastRankSeen; // 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); @@ -104,6 +110,7 @@ final class StreamingCvrReader extends BaseCvrReader { !isNullOrBlank(source.getPrecinctColumnIndex()) ? Integer.parseInt(source.getPrecinctColumnIndex()) - 1 : null; + this.ballotStyleColumnIndex = /** AS A TEST **/ 8; this.overvoteDelimiter = source.getOvervoteDelimiter(); this.overvoteLabel = source.getOvervoteLabel(); this.skippedRankLabel = source.getSkippedRankLabel(); @@ -183,9 +190,25 @@ private void endCvr() { String computedCastVoteRecordId = String.format("%s-%d", OutputWriter.sanitizeStringForOutput(excelFileName), cvrIndex); - boolean areAllCandidatesEmpty = currentRankings.stream().allMatch( + boolean areAllCurrentRankingsEmpty = currentRankings.stream().allMatch( ranking -> isNullOrBlank(ranking.getValue())); - if (areAllCandidatesEmpty) { + if (ballotStyleColumnIndex != null) { + String ballotStyle = currentCvrData.size() > ballotStyleColumnIndex + ? currentCvrData.get(ballotStyleColumnIndex) + : null; + + if (ballotStyleHasEmptyRankings.containsKey(ballotStyle)) { + Boolean hasPreviousEmptyRankings = ballotStyleHasEmptyRankings.get(ballotStyle); + if (hasPreviousEmptyRankings != areAllCurrentRankingsEmpty) { + 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, areAllCurrentRankingsEmpty); + } + } + } + if (areAllCurrentRankingsEmpty) { Logger.auditable( "Skipping cast vote record with no votes for any candidates: %s", computedCastVoteRecordId); return; From b4e6620b79fc51942cf4e95cb673936ad81ccd34 Mon Sep 17 00:00:00 2001 From: Armin Samii Date: Tue, 17 Mar 2026 15:51:58 -0400 Subject: [PATCH 2/2] fix checkstyle --- .../java/network/brightspots/rcv/StreamingCvrReader.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/network/brightspots/rcv/StreamingCvrReader.java b/src/main/java/network/brightspots/rcv/StreamingCvrReader.java index 7d8d7149..dc43463b 100644 --- a/src/main/java/network/brightspots/rcv/StreamingCvrReader.java +++ b/src/main/java/network/brightspots/rcv/StreamingCvrReader.java @@ -112,7 +112,7 @@ final class StreamingCvrReader extends BaseCvrReader { !isNullOrBlank(source.getPrecinctColumnIndex()) ? Integer.parseInt(source.getPrecinctColumnIndex()) - 1 : null; - this.ballotStyleColumnIndex = /** AS A TEST **/ null; + this.ballotStyleColumnIndex = null; // to be implemented this.overvoteDelimiter = source.getOvervoteDelimiter(); this.overvoteLabel = source.getOvervoteLabel(); this.skippedRankLabel = source.getSkippedRankLabel(); @@ -199,7 +199,8 @@ private void endCvr() { : null; if (ballotStyleHasEmptyRankings.containsKey(ballotStyle)) { - Boolean hasPreviouslySeenNonBlankCandidateCells = ballotStyleHasEmptyRankings.get(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);