forked from cdos-rla/colorado-rla
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
232 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,18 @@ | |
* | ||
* @author Daniel M. Zimmerman <[email protected]> | ||
* @version 1.0.0 | ||
* Additions by Vanessa Teague for IRV and STV parsing. | ||
* This parser can cope with 3 different kinds of contests, defined by their headers. | ||
* - plurality single- and multi- winner contests, which are parsed directly and saved, | ||
* - IRV (ranked single-winner) contests, which are parsed with some extra complexity for dealing with | ||
* their special candidate headers (names and ranks), and saved, | ||
* - STV (ranked multi-winner) contests, which by request are accepted but then dropped completely. | ||
* These are ephemerally parsed into the Contest data structures, which are then used to parse each | ||
* CVR, but no CountyContestResult is added to my_results, no Contest is persisted, and none of the | ||
* choices are added into the CVRContestInfo structures attached to the CVR. This processing is | ||
* intended to replicate the situation in which the STV contest had simply been omitted from the | ||
* CSV file. However, parsing errors (invalid headers, missing headers, etc) will still cause an | ||
* error message. | ||
*/ | ||
@SuppressWarnings({"PMD.GodClass", "PMD.CyclomaticComplexity", "PMD.ExcessiveImports", | ||
"PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity"}) | ||
|
@@ -165,6 +177,11 @@ public class DominionCVRExportParser { | |
*/ | ||
private static final String IRV_WINNERS_ALLOWED = "(Number of positions="; | ||
|
||
/** | ||
* Indicator (-1) to show that there are no votes allowed. This is used to identify STV contests. | ||
*/ | ||
private static final int STV_NO_VOTES = -1; | ||
|
||
/** | ||
* The parser to be used. | ||
*/ | ||
|
@@ -334,19 +351,26 @@ private void updateContestStructures(final CSVRecord the_line, | |
final int pluralityVotesAllowed = extractPositiveInteger(c, PLURALITY_VOTE_FOR, ")"); | ||
final int irvVotesAllowed = extractPositiveInteger(c, IRV_VOTE_FOR, ")"); | ||
final int irvWinners = extractPositiveInteger(c, IRV_WINNERS_ALLOWED, ","); | ||
|
||
// If winners and allowed votes are as expected for plurality, this is a plurality contest. | ||
if(pluralityVotesAllowed > 0 && irvVotesAllowed == -1 && irvWinners == -1) { | ||
String contestName = c.substring(0, c.indexOf(PLURALITY_VOTE_FOR)).strip(); | ||
final boolean isPlurality = pluralityVotesAllowed > 0 && irvVotesAllowed == -1 && irvWinners == -1; | ||
final boolean isIRV = pluralityVotesAllowed == -1 && irvVotesAllowed > 0 && irvWinners == 1; | ||
// If it looks like IRV but has more than one winner, it's an STV contest. | ||
final boolean isSTV = pluralityVotesAllowed == -1 && irvVotesAllowed > 0 && irvWinners > 1; | ||
|
||
// Counterintuitively, we also code the STV contest as a 'plurality' contest, which works | ||
// fine with candidate names of the form Alice(1), Alice(2), etc. | ||
// STV_NO_VOTES = -1 in the_votes_allowed to encode that it's an STV contest. | ||
if(isPlurality || isSTV) { | ||
String contestName | ||
= c.substring(0, c.indexOf(isPlurality ? PLURALITY_VOTE_FOR : IRV_VOTE_FOR)).strip(); | ||
the_names.add(contestName); | ||
the_contest_types.put(contestName, ContestType.PLURALITY); | ||
the_choice_counts.put(contestName, count); | ||
the_votes_allowed.put(contestName, pluralityVotesAllowed); | ||
the_votes_allowed.put(contestName, isPlurality ? pluralityVotesAllowed : STV_NO_VOTES); | ||
|
||
// If winners and allowed votes are as expected for IRV, this is an IRV contest. | ||
} else if(pluralityVotesAllowed == -1 && irvVotesAllowed > 0 && irvWinners == 1 | ||
// We expect the count to be the real number of choices times the number of ranks. | ||
&& count % irvVotesAllowed == 0) { | ||
// We expect the count to be the real number of choices times the number of ranks. | ||
} else if(isIRV && count % irvVotesAllowed == 0) { | ||
String contestName = c.substring(0, c.indexOf(IRV_WINNERS_ALLOWED)).strip(); | ||
the_names.add(contestName); | ||
the_contest_types.put(contestName, ContestType.IRV); | ||
|
@@ -355,9 +379,6 @@ private void updateContestStructures(final CSVRecord the_line, | |
the_choice_counts.put(contestName, count / irvVotesAllowed); | ||
the_votes_allowed.put(contestName, irvVotesAllowed); | ||
|
||
// TODO Clarify whether we should accept, though not audit, an STV contest, as below, | ||
// See issue https://github.com/DemocracyDevelopers/colorado-rla/issues/107 | ||
// } else if (pluralityVotesAllowed == -1 && irvVotesAllowed > 0 && irvWinners > 1) { | ||
} else { | ||
// The header didn't have the keywords we expected. | ||
final String msg = "Could not parse header: "; | ||
|
@@ -450,6 +471,7 @@ private Result addContests(final CSVRecord choiceLine, | |
choice = new IRVPreference(choiceLine.get(index)).candidateName; | ||
} else { | ||
// Plurality - just use the candidate/choice name as is. | ||
// Also for STV - it will just keep the names followed by rank. | ||
choice = choiceLine.get(index).strip(); | ||
} | ||
final String explanation = explanationLine.get(index).trim(); | ||
|
@@ -467,15 +489,16 @@ private Result addContests(final CSVRecord choiceLine, | |
|
||
// Winners allowed is always 1 for IRV, but is assumed to be equal to votesAllowed for | ||
// plurality, because the Dominion format doesn't give us that separately. | ||
// For STV, it doesn't matter because the contest will be dropped. | ||
final int winnersAllowed = isIRV ? 1 : votesAllowed.get(contestName); | ||
|
||
final Contest c = new Contest(contestName, my_county, contestTypes.get(contestName).toString(), | ||
choices, votesAllowed.get(contestName), winnersAllowed, contest_count); | ||
|
||
LOGGER.debug(String.format("[addContests: county=%s, contest=%s", my_county.name(), c)); | ||
|
||
// If we've just finished a plurality contest header, index is already at the right place | ||
// for the next contest. | ||
// If we've just finished a plurality or STV contest header, index is already at the right | ||
// place for the next contest. | ||
// If we've just finished the 1st-preference IRV choices, e.g. "Alice(1), Bob(1), Chuan(1)", | ||
// index will now be pointed at the beginning of the IRV 2nd preferences, e.g. | ||
// "Alice(2), Bob(2), Chuan(2)", so we have to advance it to the next contest. There is a | ||
|
@@ -484,10 +507,13 @@ private Result addContests(final CSVRecord choiceLine, | |
index = index + (isIRV ? choices.size() * (votesAllowed.get(contestName) - 1) : 0); | ||
contest_count = contest_count + 1; | ||
try { | ||
Persistence.saveOrUpdate(c); | ||
final CountyContestResult r = CountyContestResultQueries.matching(my_county, c); | ||
// Don't make a CountyContestResult for, and don't persist, an STV contest. | ||
if(votesAllowed.get(contestName) != STV_NO_VOTES) { | ||
Persistence.saveOrUpdate(c); | ||
final CountyContestResult r = CountyContestResultQueries.matching(my_county, c); | ||
my_results.add(r); | ||
} | ||
my_contests.add(c); | ||
my_results.add(r); | ||
} catch (PersistenceException pe) { | ||
result.success = false; | ||
result.errorMessage = StringUtils.abbreviate(DBExceptionUtil.getConstraintFailureReason(pe), 250); | ||
|
@@ -505,34 +531,6 @@ private Result addContests(final CSVRecord choiceLine, | |
return result ; | ||
} | ||
|
||
/** | ||
* Checks whether a contest choice is "Write-in" or various other spellings. Note this is not the | ||
* same as _being_ a write-in candidate - we are looking for the fictitious candidate who | ||
* indicates the beginning of the write-ins. If it is a plurality contest, we look for expected | ||
* write-in strings such as "Write-in", "WRITEIN", "write_in", etc. | ||
* If it is an IRV contest, we parse it as candidate_name(rank) and check whether candidate_name | ||
* matches an expected write-in string. | ||
* @param choice the candidate name (possibly with a rank in parentheses, if IRV). | ||
* @param contestType PLURALITY or IRV. | ||
* @return true if it matches some variation on "Write-in", allowing '-', '_', empty space or | ||
* whitespace between (any capitalization of) "Write" and any capitalization of "in". | ||
* @throws IRVParsingException if the contestType is IRV but the choice cannot be parsed as | ||
* name(rank). | ||
*/ | ||
private boolean nameIsWriteIn(String choice, ContestType contestType) throws IRVParsingException { | ||
// The upper case matches "WRITE" [something] "IN", where the something can be 0 or more of | ||
// '-', '_', space or tab. | ||
final String writeInRegexp = "WRITE[-_ \t]*IN"; | ||
|
||
// If it's IRV, ignore the rank in parentheses and just check the name. | ||
if(contestType == ContestType.IRV) { | ||
return (new IRVPreference(choice).candidateName.toUpperCase()).matches(writeInRegexp); | ||
} else { | ||
// If it's plurality, just match the name directly. | ||
return choice.toUpperCase().matches(writeInRegexp); | ||
} | ||
} | ||
|
||
/** | ||
* Checks to see if the set of parsed CVRs needs flushing, and does so | ||
* if necessary. | ||
|
@@ -682,7 +680,9 @@ private CastVoteRecord extractCVR(final CSVRecord the_line) { | |
irvInterpretation.logMessage(CVR_NUMBER_HEADER, IMPRINTED_ID_HEADER))); | ||
} | ||
contest_info.add(new CVRContestInfo(co, null, null, orderedChoices)); | ||
} else { | ||
|
||
} else if(co.votesAllowed() != STV_NO_VOTES) { | ||
// Don't store an STV contest (indicated by STV_NO_VOTES in votesAllowed). | ||
// Store plurality vote. | ||
contest_info.add(new CVRContestInfo(co, null, null, votes)); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
...src/test/resources/CSVs/Tiny-IRV-Examples/STVPlusThreeCandidatesTenVotesPlusPlurality.csv
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
Tiny IRV Example Contest 1,5.10.11.24,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | ||
,,,,,,,"STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","STVExample1 (Number of positions=2, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)","TinyExample1 (Number of positions=1, Number of ranks=3)",PluralityExample1 (Vote For=1),PluralityExample1 (Vote For=1),PluralityExample1 (Vote For=1),PluralityExample2 (Vote For=2),PluralityExample2 (Vote For=2),PluralityExample2 (Vote For=2) | ||
,,,,,,,Jaya(1),Kun(1),Lou(1),Jaya(2),Kun(2),Lou(2),Jaya(3),Kun(3),Lou(3),Alice(1),Bob(1),Chuan(1),Alice(2),Bob(2),Chuan(2),Alice(3),Bob(3),Chuan(3),Diego,Eli,Farhad,Gertrude,Ho,Imogen | ||
CvrNumber,TabulatorNum,BatchId,RecordId,ImprintedId,PrecinctPortion,BallotType,,,,,,,,,,,,,,,,,,,,,,,, | ||
1,1,1,1,1-1-1,Precinct 1,Ballot 1 - Type 1,1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,1,0,1 | ||
2,1,1,2,1-1-2,Precinct 1,Ballot 1 - Type 1,1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,1,0,0,1 | ||
3,1,1,3,1-1-3,Precinct 1,Ballot 1 - Type 1,1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,1,1,1,0 | ||
4,1,1,4,1-1-4,Precinct 1,Ballot 1 - Type 1,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1,0,0,0,1,1,1,0 | ||
5,1,1,5,1-1-5,Precinct 1,Ballot 1 - Type 1,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,0,1,1,0 | ||
6,1,1,6,1-1-6,Precinct 1,Ballot 1 - Type 1,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,0,1,1,0 | ||
7,1,1,7,1-1-7,Precinct 1,Ballot 1 - Type 1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,1,0,0,1,1,0 | ||
8,1,1,8,1-1-8,Precinct 1,Ballot 1 - Type 1,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,1,0 | ||
9,1,1,9,1-1-9,Precinct 1,Ballot 1 - Type 1,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,1,0 | ||
10,1,1,10,1-1-10,Precinct 1,Ballot 1 - Type 1,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,1,0 |
Oops, something went wrong.