Implementing the July 2015 Community Challenge seemed relevant considering it is election time.
I decided to go with the strategy pattern as this can be implemented in many different ways. I did my own implementation of this first, and then later also added a "PascalElection" strategy, as that code was translated and adapted from a Pascal implementation (not included for review here)
Overview of code:
- ElectionStrategy: Interface for the Strategy Pattern
- Election: Class to hold data about the votes and nominees. Also contains a static method to perform an election.
- ElectionResult: (static class within
Election
) Represents the result of an election. - CandidateState: (static class within
Election
) Represents several states a candidate can be in (my strategy only uses three of them, the PascalElection strategy uses all of them). - SimonElection: My own implementation of the election strategy
- Round: As the STV voting system is an iterative one, this represents a single 'round' in the voting. This can be used to plot a graph over the results. (The code to produce the graphs is not included in this post)
This project is also available on GitHub: Zomis/StackSTV
Example graphs showing election results:
My strategy:
The pascal strategy:
Code
ElectionStrategy:
interface ElectionStrategy {
Election.ElectionResult elect(Election election)
}
Election:
class Election {
final List<Candidate> candidates = new ArrayList<>()
final List<Vote> votes = new ArrayList<>()
int availablePositions
int maxChoices
private Election(int availablePositions) {
this.availablePositions = availablePositions
}
void addVote(Vote vote) {
this.votes << vote
this.maxChoices = Math.max(maxChoices, vote.preferences.length)
}
void addCandidate(String name) {
this.candidates.add(new Candidate(name: name))
}
double calculateQuota(double excess) {
(votes.size() - excess) / (availablePositions + 1)
}
static class ElectionResult {
List<Round> rounds
List<Candidate> candidateResults
List<Candidate> getCandidates(CandidateState state) {
candidateResults.stream()
.filter({it.state == state})
.collect(Collectors.toList())
}
}
ElectionResult elect(ElectionStrategy strategy) {
strategy.elect(this)
}
static enum CandidateState {
HOPEFUL, EXCLUDED, ALMOST, NEWLY_ELECTED, ELECTED
}
@ToString(includeNames = true, includePackage = false)
static class Candidate {
String name
double weighting = 1
double votes
CandidateState state = CandidateState.HOPEFUL
Candidate copy() {
new Candidate(name: name, weighting: weighting, votes: votes, state: state)
}
}
@ToString
static class Vote {
int numVotes
Candidate[] preferences
static Vote fromLine(String line, Election election) {
String[] data = line.split()
Vote vote = new Vote()
vote.numVotes = data[0] as int
int candidateVotes = data.length - 2
vote.preferences = new Candidate[candidateVotes]
for (int i = 0; i < vote.preferences.length; i++) {
int candidate = data[i + 1] as int
if (candidate > 0) {
vote.preferences[i] = election.candidates.get(candidate - 1)
}
}
vote
}
void distribute(Round round) {
double remaining = numVotes
int choiceIndex = 0
preferences.eachWithIndex { Candidate entry, int i ->
if (entry) {
double myScore = remaining * entry.weighting
entry.votes += myScore
remaining -= myScore
round.usedVotes[choiceIndex++] += myScore
}
}
round.excess += remaining
}
}
static final ElectionResult fromURL(URL url, ElectionStrategy strategy) {
BufferedReader reader = url.newReader()
String[] head = reader.readLine().split()
int candidates = head[0] as int
Election stv = new Election(head[1] as int)
for (int i = 0; i < candidates; i++) {
stv.addCandidate("Candidate $i") // use a temporary name at first. real names are at the end of the file
}
String line = reader.readLine();
while (line != '0') {
Vote vote = Vote.fromLine(line, stv)
stv.addVote(vote)
line = reader.readLine();
}
for (int i = 0; i < candidates; i++) {
String name = reader.readLine()
stv.candidates.get(i).name = name
}
stv.elect(strategy)
}
}
SimonElection:
class SimonElection implements ElectionStrategy {
@Override
Election.ElectionResult elect(Election election) {
List<Round> rounds = new ArrayList<>()
int electedCount = 0
int roundsCount = 0
double previousExcess = 0
while (electedCount < election.availablePositions) {
Round round = new Round(roundsCount, election.maxChoices)
rounds << round
double roundQuota = election.calculateQuota(previousExcess)
roundsCount++
round.quota = roundQuota
election.candidates*.votes = 0
election.votes*.distribute(round)
List<Election.Candidate> elected = election.candidates.stream()
.filter({candidate -> candidate.votes > roundQuota})
.collect(Collectors.toList())
elected.each {
if (it.state != Election.CandidateState.ELECTED) {
electedCount++
}
it.state = Election.CandidateState.ELECTED
it.weighting *= roundQuota / it.votes
}
if (elected.isEmpty()) {
Election.Candidate loser = election.candidates.stream()
.filter({it.state == Election.CandidateState.HOPEFUL})
.min(Comparator.comparingDouble({it.votes})).get()
loser.state = Election.CandidateState.EXCLUDED
loser.weighting = 0
}
round.candidates = election.candidates.collect {it.copy()}
previousExcess = round.excess
}
new Election.ElectionResult(rounds: rounds, candidateResults: election.candidates)
}
}
Round:
@ToString
class Round {
int round
List<Election.Candidate> candidates = new ArrayList<>()
double quota
double[] usedVotes
double excess
Round(int round, int maxChoices) {
this.round = round
this.usedVotes = new double[maxChoices]
}
}
Running the code
Tests are available on the GitHub repository, I am currently only running the code in a test, using election data from the most recent Stack Overflow election
Primary Concerns
I'm mostly interested in the way I'm using Groovy, and what Groovy things I could do instead of, or in addition to(?), using Java 8. Should I use some Java stuff instead of some Groovy stuff, or should I use more Groovy instead of Java?
Any other comments welcome.