Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
db09066
tests: added some tests to reveal business cases
nergal-perm Mar 12, 2022
dd86c30
duplicated-code: incrementing candidate votes counter
nergal-perm May 28, 2022
57a1cf7
obscured-intent: make adding new votes counter use 0 as default value
nergal-perm May 29, 2022
a4d879c
duplicated-code: increment votes counter unconditionally
nergal-perm May 29, 2022
3ec9db5
duplicated-code: generalize collection type for votes w/o district
nergal-perm May 29, 2022
57997a7
duplicated-code: extract method for adding an unofficial candidate
nergal-perm May 29, 2022
cbd76a2
duplicated-code: extract method for vote count
nergal-perm May 29, 2022
b397b0f
duplicated-code: extract method for counting total votes
nergal-perm May 30, 2022
6468f3f
duplicated-code: extract method for counting official candidates votes
nergal-perm May 30, 2022
88aa61a
primitive-obsession: use Fraction to represent resulting percents
nergal-perm May 30, 2022
faa6936
primitive-obsession: use Fraction to represent blank and null votes
nergal-perm May 30, 2022
90740be
duplicated-code: extract votes categorizing method
nergal-perm May 30, 2022
982573e
primitive-obsession: store electors in a dedicated collection class
nergal-perm May 31, 2022
0198492
primitive-obsession: store candidates in a dedicated collection class
nergal-perm May 31, 2022
d5b6733
primitive-obsession: use dedicated Candidates collection
nergal-perm May 31, 2022
2c5d18b
no-smell: move vote type definition to Vote class
nergal-perm May 31, 2022
1e2d0f9
no-smell: move all vote type counters to Votes collection
nergal-perm May 31, 2022
57a26b2
primitive-obsession: move votes without district counting to Votes
nergal-perm May 31, 2022
2d2fa34
primitive-obsession: move votes with district counting to Votes
nergal-perm May 31, 2022
7a7f479
primitive-obsession: remove the Map of votes by district
nergal-perm May 31, 2022
3b6e444
duplicated-code: use the dedicated object for storing the result
nergal-perm Jun 1, 2022
20c290a
switch-statements: replace conditional result counting with strategy
nergal-perm Jun 1, 2022
9de9d10
no-smell: cleaning unused methods and variables, formatting
nergal-perm Jun 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions java/src/main/java/org/elections/Candidate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.elections;

public class Candidate {
private final String name;
private final boolean isOfficial;

public Candidate(String name, boolean isOfficial) {
this.name = name;
this.isOfficial = isOfficial;
}

public static Candidate official(String name) {
return new Candidate(name, true);
}

public static Candidate unofficial(String name) {
return new Candidate(name, false);
}

public String name() {
return this.name;
}

public boolean isOfficial() {
return this.isOfficial;
}

public boolean hasName(String candidateName) {
return this.name.equals(candidateName);
}

public boolean isBlank() {
return this.name.trim().isEmpty();
}
}
47 changes: 47 additions & 0 deletions java/src/main/java/org/elections/Candidates.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.elections;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class Candidates {
private final List<Candidate> candidates = new ArrayList<>();

public void addOfficial(String name) {
candidates.add(Candidate.official(name));
}

public void addUnofficial(String name) {
candidates.add(Candidate.unofficial(name));
}

public List<String> officialCandidates() {
return candidates.stream()
.filter(Candidate::isOfficial)
.map(Candidate::name)
.collect(Collectors.toUnmodifiableList());
}

public long officialCandidatesCount() {
return candidates.stream().filter(Candidate::isOfficial).count();
}

public boolean isUnregistered(String candidate) {
return candidates.stream().noneMatch(sameNameAs(candidate));
}

private Predicate<Candidate> sameNameAs(String candidate) {
return c -> candidate.equals(c.name());
}

public String get(int index) {
return candidates.get(index).name();
}

public Candidate findByName(String candidateName) {
return candidates.stream()
.filter(candidate -> candidate.hasName(candidateName))
.findFirst().orElse(null);
}
}
165 changes: 32 additions & 133 deletions java/src/main/java/org/elections/Elections.java
Original file line number Diff line number Diff line change
@@ -1,149 +1,48 @@
package org.elections;

import java.text.DecimalFormat;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class Elections {
List<String> candidates = new ArrayList<>();
List<String> officialCandidates = new ArrayList<>();
ArrayList<Integer> votesWithoutDistricts = new ArrayList<>();
Map<String, ArrayList<Integer>> votesWithDistricts;
private Map<String, List<String>> list;
private boolean withDistrict;
private final Electors electors;
private final Candidates candidates;
private final Votes votes;
private final VotingResultStrategy voting;

public Elections(Map<String, List<String>> list, boolean withDistrict) {
this.list = list;
this.withDistrict = withDistrict;

votesWithDistricts = new HashMap<>();
votesWithDistricts.put("District 1", new ArrayList<>());
votesWithDistricts.put("District 2", new ArrayList<>());
votesWithDistricts.put("District 3", new ArrayList<>());
electors = Electors.fromMapByDistrict(list);
candidates = new Candidates();
votes = new Votes();

List<String> districts = new ArrayList<>();
districts.add("District 1");
districts.add("District 2");
districts.add("District 3");

voting = withDistrict
? new WithDistrictVotingResult(candidates, electors, districts)
:new NoDistrictVotingResult(candidates, electors);
}

public void addCandidate(String candidate) {
officialCandidates.add(candidate);
candidates.add(candidate);
votesWithoutDistricts.add(0);
votesWithDistricts.get("District 1").add(0);
votesWithDistricts.get("District 2").add(0);
votesWithDistricts.get("District 3").add(0);
public void addOfficialCandidate(String candidate) {
candidates.addOfficial(candidate);
}

public void voteFor(String elector, String candidate, String electorDistrict) {
if (!withDistrict) {
if (candidates.contains(candidate)) {
int index = candidates.indexOf(candidate);
votesWithoutDistricts.set(index, votesWithoutDistricts.get(index) + 1);
} else {
candidates.add(candidate);
votesWithoutDistricts.add(1);
}
} else {
if (votesWithDistricts.containsKey(electorDistrict)) {
ArrayList<Integer> districtVotes = votesWithDistricts.get(electorDistrict);
if (candidates.contains(candidate)) {
int index = candidates.indexOf(candidate);
districtVotes.set(index, districtVotes.get(index) + 1);
} else {
candidates.add(candidate);
votesWithDistricts.forEach((district, votes) -> {
votes.add(0);
});
districtVotes.set(candidates.size() - 1, districtVotes.get(candidates.size() - 1) + 1);
}
}
public void voteFor(String electorName, String candidateName, String electorDistrict) {
if (candidates.isUnregistered(candidateName)) {
candidates.addUnofficial(candidateName);
}

votes.registerVote(electorDistrict,
electors.findByName(electorName),
candidates.findByName(candidateName));
}

public Map<String, String> results() {
Map<String, String> results = new HashMap<>();
Integer nbVotes = 0;
Integer nullVotes = 0;
Integer blankVotes = 0;
int nbValidVotes = 0;

if (!withDistrict) {
nbVotes = votesWithoutDistricts.stream().reduce(0, Integer::sum);
for (int i = 0; i < officialCandidates.size(); i++) {
int index = candidates.indexOf(officialCandidates.get(i));
nbValidVotes += votesWithoutDistricts.get(index);
}

for (int i = 0; i < votesWithoutDistricts.size(); i++) {
Float candidatResult = ((float)votesWithoutDistricts.get(i) * 100) / nbValidVotes;
String candidate = candidates.get(i);
if (officialCandidates.contains(candidate)) {
results.put(candidate, String.format(Locale.FRENCH, "%.2f%%", candidatResult));
} else {
if (candidates.get(i).isEmpty()) {
blankVotes += votesWithoutDistricts.get(i);
} else {
nullVotes += votesWithoutDistricts.get(i);
}
}
}
} else {
for (Map.Entry<String, ArrayList<Integer>> entry : votesWithDistricts.entrySet()) {
ArrayList<Integer> districtVotes = entry.getValue();
nbVotes += districtVotes.stream().reduce(0, Integer::sum);
}

for (int i = 0; i < officialCandidates.size(); i++) {
int index = candidates.indexOf(officialCandidates.get(i));
for (Map.Entry<String, ArrayList<Integer>> entry : votesWithDistricts.entrySet()) {
ArrayList<Integer> districtVotes = entry.getValue();
nbValidVotes += districtVotes.get(index);
}
}

Map<String, Integer> officialCandidatesResult = new HashMap<>();
for (int i = 0; i < officialCandidates.size(); i++) {
officialCandidatesResult.put(candidates.get(i), 0);
}
for (Map.Entry<String, ArrayList<Integer>> entry : votesWithDistricts.entrySet()) {
ArrayList<Float> districtResult = new ArrayList<>();
ArrayList<Integer> districtVotes = entry.getValue();
for (int i = 0; i < districtVotes.size(); i++) {
float candidateResult = 0;
if (nbValidVotes != 0)
candidateResult = ((float)districtVotes.get(i) * 100) / nbValidVotes;
String candidate = candidates.get(i);
if (officialCandidates.contains(candidate)) {
districtResult.add(candidateResult);
} else {
if (candidates.get(i).isEmpty()) {
blankVotes += districtVotes.get(i);
} else {
nullVotes += districtVotes.get(i);
}
}
}
int districtWinnerIndex = 0;
for (int i = 1; i < districtResult.size(); i++) {
if (districtResult.get(districtWinnerIndex) < districtResult.get(i))
districtWinnerIndex = i;
}
officialCandidatesResult.put(candidates.get(districtWinnerIndex), officialCandidatesResult.get(candidates.get(districtWinnerIndex)) + 1);
}
for (int i = 0; i < officialCandidatesResult.size(); i++) {
Float ratioCandidate = ((float) officialCandidatesResult.get(candidates.get(i))) / officialCandidatesResult.size() * 100;
results.put(candidates.get(i), String.format(Locale.FRENCH, "%.2f%%", ratioCandidate));
}
}

float blankResult = ((float)blankVotes * 100) / nbVotes;
results.put("Blank", String.format(Locale.FRENCH, "%.2f%%", blankResult));

float nullResult = ((float)nullVotes * 100) / nbVotes;
results.put("Null", String.format(Locale.FRENCH, "%.2f%%", nullResult));

int nbElectors = list.values().stream().map(List::size).reduce(0, Integer::sum);
DecimalFormat df = new DecimalFormat();
df.setMaximumFractionDigits(2);
float abstentionResult = 100 - ((float) nbVotes * 100 / nbElectors);
results.put("Abstention", String.format(Locale.FRENCH, "%.2f%%", abstentionResult));

return results;
FormattedResult formattedResult = new FormattedResult(Locale.FRENCH, "%.2f%%");
voting.count(votes).printOn(formattedResult);
return formattedResult.result();
}
}
20 changes: 20 additions & 0 deletions java/src/main/java/org/elections/Elector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.elections;

public class Elector {
private String district;
private String name;

public static Elector withDistrict_Name_(String district, String name) {
return new Elector().setDistrict_Name_(district, name);
}

private Elector setDistrict_Name_(String district, String name) {
this.district = district;
this.name = name;
return this;
}

public boolean hasName(String electorName) {
return this.name.equals(electorName);
}
}
39 changes: 39 additions & 0 deletions java/src/main/java/org/elections/Electors.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.elections;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Electors {
private final List<Elector> electors;

private Electors() {
electors = new ArrayList<>();
}

public static Electors fromMapByDistrict(Map<String, List<String>> electorsByDistrict) {
Electors instance = new Electors();
for (Map.Entry<String, List<String>> district : electorsByDistrict.entrySet()) {
instance.addAll(district.getValue().stream()
.map(el -> Elector.withDistrict_Name_(district.getKey(), el))
.collect(Collectors.toList()));
}
return instance;
}

private void addAll(List<Elector> electorList) {
electors.addAll(electorList);
}

public int size() {
return electors.size();
}

public Elector findByName(String electorName) {
return electors.stream()
.filter(elector -> elector.hasName(electorName))
.findAny()
.orElse(null);
}
}
41 changes: 41 additions & 0 deletions java/src/main/java/org/elections/FormattedResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.elections;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class FormattedResult {
private final Locale locale;
private final String percentFormat;
private final Map<String, String> result;

public FormattedResult(Locale locale, String percentFormat) {
this.locale = locale;
this.percentFormat = percentFormat;
result = new HashMap<>();
}

public void addCandidateResult(String candidate, Fraction candidateResult) {
putFormattedValue(candidate, candidateResult);
}

private void putFormattedValue(String key, Fraction value) {
result.put(key, String.format(locale, percentFormat, value.asPercent()));
}

public void addBlankVotes(Fraction blanks) {
putFormattedValue("Blank", blanks);
}

public void addNullVotes(Fraction nulls) {
putFormattedValue("Null", nulls);
}

public void addAbstention(Fraction abstentionResult) {
putFormattedValue("Abstention", abstentionResult);
}

public Map<String, String> result() {
return new HashMap<>(this.result);
}
}
22 changes: 22 additions & 0 deletions java/src/main/java/org/elections/Fraction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.elections;

public class Fraction {
private final long numerator;
private final long denominator;

public static Fraction withNumeratorDenominator(long numerator, long denominator) {
return new Fraction(numerator, denominator);
}

private Fraction(long numerator, long denominator) {
this.numerator = numerator;
this.denominator = denominator;
}

public float asPercent() {
if (this.denominator==0) {
return 0;
}
return (float) this.numerator * 100 / this.denominator;
}
}
Loading