Skip to content

Commit

Permalink
alternate approach: load candidate manifest without postprocessing
Browse files Browse the repository at this point in the history
  • Loading branch information
artoonie committed Nov 25, 2024
1 parent adae15b commit 80e4677
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 42 deletions.
30 changes: 19 additions & 11 deletions src/main/java/network/brightspots/rcv/BaseCvrReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,24 +65,19 @@ public void runAdditionalValidations(List<CastVoteRecord> 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<String> readCandidateListFromCvr(List<CastVoteRecord> 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<String, Integer> gatherUnknownCandidates(
public Map<Candidate, Integer> gatherUnknownCandidateCounts(
List<CastVoteRecord> castVoteRecords, boolean includeCandidatesWithZeroVotes) {
// First pass: gather all unrecognized candidates and their counts
// All CVR Readers have this implemented
Map<String, Integer> unrecognizedCandidateCounts = new HashMap<>();
Map<String, Integer> unrecognizedNameCounts = new HashMap<>();
for (CastVoteRecord cvr : castVoteRecords) {
for (Pair<Integer, CandidatesAtRanking> ranking : cvr.candidateRankings) {
for (String candidateName : ranking.getValue()) {
Expand All @@ -91,7 +87,7 @@ Map<String, Integer> gatherUnknownCandidates(
continue;
}

unrecognizedCandidateCounts.merge(candidateName, 1, Integer::sum);
unrecognizedNameCounts.merge(candidateName, 1, Integer::sum);
}
}
}
Expand All @@ -115,16 +111,28 @@ Map<String, Integer> 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<Candidate, Integer> unrecognizedCandidateCounts = new HashMap<>();
for (Map.Entry<String, Integer> 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<Candidate> gatherUnknownCandidates(List<CastVoteRecord> castVoteRecords) {
return gatherUnknownCandidateCounts(castVoteRecords, true).keySet();
}

boolean usesLastAllowedRanking(List<Pair<Integer, String>> rankings, String contestId) {
if (rankings.isEmpty()) {
return false;
Expand Down
32 changes: 19 additions & 13 deletions src/main/java/network/brightspots/rcv/DominionCvrReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private static Map<Integer, String> getPrecinctData(String precinctPath) {
}

// returns a map of Codes to Candidate objects parsed from CandidateManifest.json
private static HashMap<String, Candidate> getCandidates(String candidatePath) {
private HashMap<String, Candidate> getCandidates(String candidatePath) {
HashMap<String, Candidate> candidates = new HashMap<>();
try {
HashMap json = JsonParser.readFromFile(candidatePath, HashMap.class);
Expand All @@ -112,6 +112,9 @@ private static HashMap<String, Candidate> 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);
}
Expand Down Expand Up @@ -228,20 +231,23 @@ private void validateNamesAreInContest(List<CastVoteRecord> 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<RawContestConfig.Candidate> gatherUnknownCandidates(
List<CastVoteRecord> castVoteRecords) {
Set<String> knownNames = config.getCandidateNames();
Set<String> namesFoundInManifest = candidates.keySet();

Set<RawContestConfig.Candidate> unknownCandidates = new HashSet<>();
for (String code : namesFoundInManifest) {
Candidate candidate = candidates.get(code);
if (knownNames.contains(candidate.name)) {
continue;
}
unknownCandidates.add(new RawContestConfig.Candidate(candidate.name, code, false));
}

return fixedCandidate;
return unknownCandidates;
}

// parse the CVR file or files into a List of CastVoteRecords for tabulation
Expand Down
22 changes: 8 additions & 14 deletions src/main/java/network/brightspots/rcv/GuiConfigController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1722,20 +1720,16 @@ protected Void call() {
}
if (cvrsSpecified) {
// Gather unloaded names from each of the sources and place into the HashSet
HashMap<String, Candidate> unloadedNames = new HashMap<>();
HashSet<Candidate> unloadedCandidates = new HashSet<>();
for (CvrSource source : sources) {
Provider provider = ContestConfig.getProvider(source);
try {
List<CastVoteRecord> castVoteRecords = new ArrayList<>();
BaseCvrReader reader = provider.constructReader(config, source);
reader.readCastVoteRecords(castVoteRecords);
Set<String> 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<Candidate> unknownCandidates =
reader.gatherUnknownCandidates(castVoteRecords);
unloadedCandidates.addAll(unknownCandidates);
} catch (ContestConfig.UnrecognizedProviderException e) {
Logger.severe(
"Unrecognized provider \"%s\" in source file \"%s\": %s",
Expand All @@ -1749,14 +1743,14 @@ protected Void call() {

// Validate each name and add to the table of candidates
int successCount = 0;
for (Map.Entry<String, Candidate> entry : unloadedNames.entrySet()) {
for (Candidate candidate : unloadedCandidates) {
Set<ValidationError> 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());
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/network/brightspots/rcv/TabulatorSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ private LoadedCvrData parseCastVoteRecords(
source, reader, sourceIndex, startIndex, castVoteRecords.size() - 1));

// Check for unrecognized candidates
Map<String, Integer> unrecognizedCandidateCounts =
reader.gatherUnknownCandidates(castVoteRecords, false);
Map<RawContestConfig.Candidate, Integer> unrecognizedCandidateCounts =
reader.gatherUnknownCandidateCounts(castVoteRecords, false);

if (!unrecognizedCandidateCounts.isEmpty()) {
throw new UnrecognizedCandidatesException(unrecognizedCandidateCounts);
Expand Down Expand Up @@ -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<String, Integer> candidateCounts;
final Map<RawContestConfig.Candidate, Integer> candidateCounts;

UnrecognizedCandidatesException(Map<String, Integer> candidateCounts) {
UnrecognizedCandidatesException(Map<RawContestConfig.Candidate, Integer> candidateCounts) {
this.candidateCounts = candidateCounts;
}
}
Expand Down

0 comments on commit 80e4677

Please sign in to comment.