Skip to content
This repository has been archived by the owner on Nov 3, 2020. It is now read-only.

Commit

Permalink
refactor: clarify the tally report types
Browse files Browse the repository at this point in the history
This changes the types for the candidate and yesno tallies from arrays into objects. For candidate contests, it splits the former single array that contained both write-ins and ballot candidates into separate properties on an object. For yesno contests, instead of a 2-element tuple we now have an object with "yes" and "no" properties.

I think this is simpler and requires less casting. It also adds a few assertions for situations which shouldn't happen but otherwise might silently fail.
  • Loading branch information
eventualbuddha committed Oct 3, 2019
1 parent db02f8b commit c3e94c8
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 32 deletions.
41 changes: 20 additions & 21 deletions src/AppRoot.tsx
Expand Up @@ -34,11 +34,10 @@ import {
VxPrintOnly,
Tally,
YesNoVote,
TallyCount,
CandidateVote,
CandidateVoteTally,
WriteInCandidateTally,
getAppMode,
YesNoVoteTally,
} from './config/types'
import BallotContext from './contexts/ballotContext'
import electionSample from './data/electionSample.json'
Expand Down Expand Up @@ -766,44 +765,44 @@ class AppRoot extends React.Component<RouteComponentProps, State> {
const contestIndex = election.contests.findIndex(
c => c.id === contestId
)
const contestTally = tally[contestIndex]
// throw error if `contestIndex < 0` ?
const contest = election.contests[contestIndex]
/* istanbul ignore else */
if (contest.type === 'yesno') {
const yesnoContestTally = contestTally as YesNoVoteTally
const vote = votes[contestId] as YesNoVote
const yesNoVoteIndex = {
yes: 0,
no: 1,
}
;(tally[contestIndex][yesNoVoteIndex[vote]] as TallyCount)++
yesnoContestTally[vote]++
} else if (contest.type === 'candidate') {
const candidateContestTally = contestTally as CandidateVoteTally
const vote = votes[contestId] as CandidateVote
vote.forEach(candidate => {
if (candidate.isWriteIn) {
const tallyContestWriteIns = (tally[
contestIndex
] as CandidateVoteTally).filter(
c => typeof c !== 'number'
) as WriteInCandidateTally[]
const writeInIndex = tallyContestWriteIns.findIndex(
const tallyContestWriteIns = candidateContestTally.writeIns
const writeIn = tallyContestWriteIns.find(
c => c.name === candidate.name
)
if (writeInIndex === -1) {
const newWriteIn: WriteInCandidateTally = {
if (typeof writeIn === 'undefined') {
tallyContestWriteIns.push({
name: candidate.name,
tally: 1,
}
;(tally[contestIndex] as CandidateVoteTally).push(newWriteIn)
})
} else {
;(tally[contestIndex][
contest.candidates.length + writeInIndex
] as WriteInCandidateTally).tally++
writeIn.tally++
}
} else {
const candidateIndex = contest.candidates.findIndex(
c => c.id === candidate.id
)
;(tally[contestIndex][candidateIndex] as TallyCount)++
if (
candidateIndex < 0 ||
candidateIndex >= candidateContestTally.candidates.length
) {
throw new Error(
`unable to find a candidate with id: ${candidate.id}`
)
}
candidateContestTally.candidates[candidateIndex]++
}
})
}
Expand Down
23 changes: 17 additions & 6 deletions src/components/PrecinctTallyReport.tsx
@@ -1,6 +1,12 @@
import React from 'react'
import styled from 'styled-components'
import { Election, Tally, Precinct } from '../config/types'
import {
Election,
Tally,
Precinct,
CandidateVoteTally,
YesNoVoteTally,
} from '../config/types'

import Table, { TD } from './Table'
import Prose from './Prose'
Expand Down Expand Up @@ -54,14 +60,14 @@ const PrecinctTallyReport = ({
return (
<Report>
<h1>
<NoWrap>{precinct.name}</NoWrap> <NoWrap>{election.title}</NoWrap>{' '}
<NoWrap>{precinct.name}</NoWrap> <NoWrap>{election.title}</NoWrap>
<NoWrap>Tally Report</NoWrap>
</h1>
<p>
This report should be <strong>{reportPurpose}</strong>.
</p>
<p>
{isPollsOpen ? 'Polls Closed' : 'Polls Opened'} and report printed at:{' '}
{isPollsOpen ? 'Polls Closed' : 'Polls Opened'} and report printed at:
<strong>{currentDateTime}</strong>
</p>
<p>
Expand All @@ -86,7 +92,8 @@ const PrecinctTallyReport = ({
<td>{candidate.name}</td>
<TD narrow textAlign="right">
{isContestInPrecinct
? tally[contestIndex][candidateIndex]
? (tally[contestIndex] as CandidateVoteTally)
.candidates[candidateIndex]
: 'X'}
</TD>
</tr>
Expand All @@ -97,13 +104,17 @@ const PrecinctTallyReport = ({
<tr>
<td>Yes</td>
<TD narrow textAlign="right">
{isContestInPrecinct ? tally[contestIndex][0] : 'X'}
{isContestInPrecinct
? (tally[contestIndex] as YesNoVoteTally).yes
: 'X'}
</TD>
</tr>
<tr>
<td>No</td>
<TD narrow textAlign="right">
{isContestInPrecinct ? tally[contestIndex][1] : 'X'}
{isContestInPrecinct
? (tally[contestIndex] as YesNoVoteTally).no
: 'X'}
</TD>
</tr>
</React.Fragment>
Expand Down
10 changes: 8 additions & 2 deletions src/config/types.ts
Expand Up @@ -129,8 +129,14 @@ export interface WriteInCandidateTally {
tally: number
}
export type TallyCount = number
export type CandidateVoteTally = (TallyCount | WriteInCandidateTally)[]
export type YesNoVoteTally = TallyCount[]
export interface CandidateVoteTally {
candidates: TallyCount[]
writeIns: WriteInCandidateTally[]
}
export interface YesNoVoteTally {
yes: TallyCount
no: TallyCount
}
export type Tally = (CandidateVoteTally | YesNoVoteTally)[]

// Ballot
Expand Down
13 changes: 10 additions & 3 deletions src/utils/election.ts
@@ -1,3 +1,4 @@
import { Contest } from '@votingworks/ballot-encoder'
import { BallotStyle, Election, Tally } from '../config/types'

export const getContests = ({
Expand Down Expand Up @@ -47,11 +48,17 @@ export const getZeroTally = (election: Election): Tally =>
election.contests.map(contest => {
/* istanbul ignore else */
if (contest.type === 'yesno') {
return [0, 0]
return { yes: 0, no: 0 }
} else if (contest.type === 'candidate') {
return contest.candidates.map(() => 0)
return {
candidates: contest.candidates.map(() => 0),
writeIns: [],
}
} else {
return []
// `as Contest` is needed because TS knows 'yesno' and 'candidate' are the
// only valid values and so infers `contest` is type `never`, and we want
// to fail loudly in this situation.
throw new Error(`unexpected contest type: ${(contest as Contest).type}`)
}
})

Expand Down

0 comments on commit c3e94c8

Please sign in to comment.