diff --git a/src/main/java/network/brightspots/rcv/BaseCvrReader.java b/src/main/java/network/brightspots/rcv/BaseCvrReader.java index 3dd461e8..8e8f82b3 100644 --- a/src/main/java/network/brightspots/rcv/BaseCvrReader.java +++ b/src/main/java/network/brightspots/rcv/BaseCvrReader.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javafx.util.Pair; import network.brightspots.rcv.RawContestConfig.Candidate; import network.brightspots.rcv.RawContestConfig.CvrSource; @@ -64,24 +65,19 @@ public void runAdditionalValidations(List castVoteRecords) } // Some CVRs have a list of candidates in the file. Read that list and return it. - // This will be used in tandem with gatherUnknownCandidates, which only looks for candidates + // This will be used in tandem with gatherUnknownCandidateCounts, which only looks for candidates // that have at least one vote. public List readCandidateListFromCvr(List castVoteRecords) throws IOException { return new ArrayList<>(); } - // Allow any reader-specific postprocessing of the candidate list. - public Candidate postprocessAutoloadedCandidate(Candidate candidate) { - return candidate; - } - // Gather candidate names from the CVR that are not in the config. - Map gatherUnknownCandidates( + public Map gatherUnknownCandidateCounts( List castVoteRecords, boolean includeCandidatesWithZeroVotes) { // First pass: gather all unrecognized candidates and their counts // All CVR Readers have this implemented - Map unrecognizedCandidateCounts = new HashMap<>(); + Map unrecognizedNameCounts = new HashMap<>(); for (CastVoteRecord cvr : castVoteRecords) { for (Pair ranking : cvr.candidateRankings) { for (String candidateName : ranking.getValue()) { @@ -91,7 +87,7 @@ Map gatherUnknownCandidates( continue; } - unrecognizedCandidateCounts.merge(candidateName, 1, Integer::sum); + unrecognizedNameCounts.merge(candidateName, 1, Integer::sum); } } } @@ -115,16 +111,28 @@ Map gatherUnknownCandidates( // Combine the lists for (String candidateName : allCandidates) { - if (!unrecognizedCandidateCounts.containsKey(candidateName) + if (!unrecognizedNameCounts.containsKey(candidateName) && config.getNameForCandidate(candidateName) == null) { - unrecognizedCandidateCounts.put(candidateName, 0); + unrecognizedNameCounts.put(candidateName, 0); } } } + Map unrecognizedCandidateCounts = new HashMap<>(); + for (Map.Entry entry : unrecognizedNameCounts.entrySet()) { + String candidateName = entry.getKey(); + int count = entry.getValue(); + Candidate candidate = new Candidate(candidateName, null, false); + unrecognizedCandidateCounts.put(candidate, count); + } + return unrecognizedCandidateCounts; } + Set gatherUnknownCandidates(List castVoteRecords) { + return gatherUnknownCandidateCounts(castVoteRecords, true).keySet(); + } + boolean usesLastAllowedRanking(List> rankings, String contestId) { if (rankings.isEmpty()) { return false; diff --git a/src/main/java/network/brightspots/rcv/DominionCvrReader.java b/src/main/java/network/brightspots/rcv/DominionCvrReader.java index 5077a5ae..8f9f3381 100644 --- a/src/main/java/network/brightspots/rcv/DominionCvrReader.java +++ b/src/main/java/network/brightspots/rcv/DominionCvrReader.java @@ -101,7 +101,7 @@ private static Map getPrecinctData(String precinctPath) { } // returns a map of Codes to Candidate objects parsed from CandidateManifest.json - private static HashMap getCandidates(String candidatePath) { + private HashMap getCandidates(String candidatePath) { HashMap candidates = new HashMap<>(); try { HashMap json = JsonParser.readFromFile(candidatePath, HashMap.class); @@ -112,6 +112,9 @@ private static HashMap getCandidates(String candidatePath) { Integer id = (Integer) candidateMap.get("Id"); String code = id.toString(); String contestId = candidateMap.get("ContestId").toString(); + if (contestId.equals(config.getContestName())) { + continue; + } Candidate newCandidate = new Candidate(name, code, contestId); candidates.put(code, newCandidate); } @@ -228,20 +231,23 @@ private void validateNamesAreInContest(List castVoteRecords) // The Candidate Autoload looks only at the CVR file(s) and not the manifest files, so // it doesn't know about the mapping between a code and the candidate's name. This function - // addresses that discrepancy. - public RawContestConfig.Candidate postprocessAutoloadedCandidate( - RawContestConfig.Candidate autoloadedCandidate) { - String autoloadedCandidateName = autoloadedCandidate.getName(); - Candidate candidateFromManifest = candidates.get(autoloadedCandidateName); - RawContestConfig.Candidate fixedCandidate; - if (candidateFromManifest == null) { - fixedCandidate = autoloadedCandidate; - } else { - fixedCandidate = new RawContestConfig.Candidate( - candidateFromManifest.name, candidateFromManifest.code, false); + // addresses that discrepancy, while also being much faster than actually reading each ballot. + @Override + public Set gatherUnknownCandidates( + List castVoteRecords) { + Set knownNames = config.getCandidateNames(); + Set> namesFoundInManifest = candidates.entrySet(); + + Set unknownCandidates = new HashSet<>(); + for (Map.Entry entry : namesFoundInManifest) { + Candidate candidate = entry.getValue(); + if (knownNames.contains(candidate.name)) { + continue; + } + unknownCandidates.add(new RawContestConfig.Candidate(candidate.name, entry.getKey(), false)); } - return fixedCandidate; + return unknownCandidates; } // parse the CVR file or files into a List of CastVoteRecords for tabulation diff --git a/src/main/java/network/brightspots/rcv/GuiConfigController.java b/src/main/java/network/brightspots/rcv/GuiConfigController.java index 6deae577..26e914de 100644 --- a/src/main/java/network/brightspots/rcv/GuiConfigController.java +++ b/src/main/java/network/brightspots/rcv/GuiConfigController.java @@ -38,10 +38,8 @@ import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.ResourceBundle; @@ -1722,20 +1720,16 @@ protected Void call() { } if (cvrsSpecified) { // Gather unloaded names from each of the sources and place into the HashSet - HashMap unloadedNames = new HashMap<>(); + HashSet unloadedCandidates = new HashSet<>(); for (CvrSource source : sources) { Provider provider = ContestConfig.getProvider(source); try { List castVoteRecords = new ArrayList<>(); BaseCvrReader reader = provider.constructReader(config, source); reader.readCastVoteRecords(castVoteRecords); - Set unknownCandidates = reader.gatherUnknownCandidates( - castVoteRecords, true).keySet(); - for (String name : unknownCandidates) { - Candidate candidate = new Candidate(name, null, false); - candidate = reader.postprocessAutoloadedCandidate(candidate); - unloadedNames.put(candidate.getName(), candidate); - } + Set unknownCandidates = + reader.gatherUnknownCandidates(castVoteRecords); + unloadedCandidates.addAll(unknownCandidates); } catch (ContestConfig.UnrecognizedProviderException e) { Logger.severe( "Unrecognized provider \"%s\" in source file \"%s\": %s", @@ -1749,14 +1743,14 @@ protected Void call() { // Validate each name and add to the table of candidates int successCount = 0; - for (Map.Entry entry : unloadedNames.entrySet()) { + for (Candidate candidate : unloadedCandidates) { Set validationErrors = - ContestConfig.performBasicCandidateValidation(entry.getValue()); + ContestConfig.performBasicCandidateValidation(candidate); if (validationErrors.isEmpty()) { - tableViewCandidates.getItems().add(entry.getValue()); + tableViewCandidates.getItems().add(candidate); successCount++; } else { - Logger.severe("Failed to load candidate \"%s\"!", entry.getKey()); + Logger.severe("Failed to load candidate \"%s\"!", candidate.getName()); } } diff --git a/src/main/java/network/brightspots/rcv/TabulatorSession.java b/src/main/java/network/brightspots/rcv/TabulatorSession.java index 86bd362f..7b262ffd 100644 --- a/src/main/java/network/brightspots/rcv/TabulatorSession.java +++ b/src/main/java/network/brightspots/rcv/TabulatorSession.java @@ -357,8 +357,8 @@ private LoadedCvrData parseCastVoteRecords( source, reader, sourceIndex, startIndex, castVoteRecords.size() - 1)); // Check for unrecognized candidates - Map unrecognizedCandidateCounts = - reader.gatherUnknownCandidates(castVoteRecords, false); + Map unrecognizedCandidateCounts = + reader.gatherUnknownCandidateCounts(castVoteRecords, false); if (!unrecognizedCandidateCounts.isEmpty()) { throw new UnrecognizedCandidatesException(unrecognizedCandidateCounts); @@ -440,9 +440,9 @@ private LoadedCvrData parseCastVoteRecords( static class UnrecognizedCandidatesException extends Exception { // count of how many times each unrecognized candidate was encountered during CVR parsing - final Map candidateCounts; + final Map candidateCounts; - UnrecognizedCandidatesException(Map candidateCounts) { + UnrecognizedCandidatesException(Map candidateCounts) { this.candidateCounts = candidateCounts; } }