Make CalculationService free of side effects

This commit is contained in:
2023-07-06 11:49:15 +02:00
parent 488818319a
commit 19e33baf1d
6 changed files with 103 additions and 86 deletions
@@ -25,7 +25,7 @@ configurations {
} }
test { test {
systemProperty 'junit.jupiter.execution.parallel.enabled', true systemProperty 'junit.jupiter.execution.parallel.enabled', false
systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent" systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent"
useJUnitPlatform() useJUnitPlatform()
maxHeapSize = "4g" maxHeapSize = "4g"
@@ -1,16 +1,5 @@
package de.twomartens.wahlrecht.model; package de.twomartens.wahlrecht.model;
import lombok.Getter; public record Candidate(String name, String profession) {
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@RequiredArgsConstructor
@Getter
@Setter
@ToString
public class Candidate {
private final String name;
private final String profession;
private Elected elected = Elected.NOT_ELECTED;
} }
@@ -0,0 +1,11 @@
package de.twomartens.wahlrecht.model;
public record ElectedCandidate(Candidate candidate, Elected elected) {
public String name() {
return candidate.name();
}
public String profession() {
return candidate.profession();
}
}
@@ -6,7 +6,7 @@ import java.util.Map;
import lombok.Builder; import lombok.Builder;
@Builder @Builder
public record ElectedResult(Map<Nomination, Collection<Candidate>> electedCandidates, public record ElectedResult(Map<Nomination, Collection<ElectedCandidate>> electedCandidates,
Deque<Double> usedElectionNumbers) { Deque<Double> usedElectionNumbers) {
} }
@@ -3,6 +3,7 @@ package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.model.Candidate; import de.twomartens.wahlrecht.model.Candidate;
import de.twomartens.wahlrecht.model.Constituency; import de.twomartens.wahlrecht.model.Constituency;
import de.twomartens.wahlrecht.model.Elected; import de.twomartens.wahlrecht.model.Elected;
import de.twomartens.wahlrecht.model.ElectedCandidate;
import de.twomartens.wahlrecht.model.ElectedResult; import de.twomartens.wahlrecht.model.ElectedResult;
import de.twomartens.wahlrecht.model.Election; import de.twomartens.wahlrecht.model.Election;
import de.twomartens.wahlrecht.model.Nomination; import de.twomartens.wahlrecht.model.Nomination;
@@ -11,12 +12,14 @@ import de.twomartens.wahlrecht.model.VotingResult;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -39,7 +42,7 @@ public class CalculationService {
nominations, numberOfSeats, initialElectionNumber); nominations, numberOfSeats, initialElectionNumber);
return ElectedResult.builder() return ElectedResult.builder()
.electedCandidates(findElectedCandidates(assignedSeatsPerNomination)) .electedCandidates(findElectedCandidates(assignedSeatsPerNomination, Collections.emptyMap()))
.usedElectionNumbers(electionNumberHistory) .usedElectionNumbers(electionNumberHistory)
.build(); .build();
} }
@@ -75,43 +78,51 @@ public class CalculationService {
.build(); .build();
} }
public ElectedResult calculateElectedOverallCandidates(Map<Nomination, Integer> seatsPerNomination) { public ElectedResult calculateElectedOverallCandidates(Map<Nomination, Integer> seatsPerNomination,
Map<Nomination, Collection<ElectedCandidate>> electedCandidates) {
electionNumberHistory.clear(); electionNumberHistory.clear();
return ElectedResult.builder() return ElectedResult.builder()
.electedCandidates(findElectedCandidates(seatsPerNomination)) .electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates))
.usedElectionNumbers(electionNumberHistory) .usedElectionNumbers(electionNumberHistory)
.build(); .build();
} }
private static boolean passesVotingThreshold(Election election, int totalVotes, Nomination nomination) { private static boolean passesVotingThreshold(@NonNull Election election, int totalVotes,
@NonNull Nomination nomination) {
return totalVotes * election.votingThreshold().getMultiplier() return totalVotes * election.votingThreshold().getMultiplier()
<= nomination.getVotingResult().getTotalVotes(); <= nomination.getVotingResult().getTotalVotes();
} }
@NonNull @NonNull
private Map<Nomination, Collection<Candidate>> findElectedCandidates( private Map<Nomination, Collection<ElectedCandidate>> findElectedCandidates(
@NonNull Map<Nomination, Integer> assignedSeatsPerNomination) { @NonNull Map<Nomination, Integer> assignedSeatsPerNomination,
Map<Nomination, Collection<ElectedCandidate>> alreadyElectedCandidates) {
Map<Nomination, Collection<Candidate>> electedCandidates = new HashMap<>(); Map<Nomination, Collection<ElectedCandidate>> electedCandidates = new HashMap<>();
assignedSeatsPerNomination.entrySet().stream() assignedSeatsPerNomination.entrySet().stream()
.filter(entry -> entry.getValue() > 0) .filter(entry -> entry.getValue() > 0)
.forEach(entry -> .forEach(entry ->
electedCandidates.put(entry.getKey(), findCandidates(entry.getKey(), entry.getValue()))); electedCandidates.put(entry.getKey(), findCandidates(entry.getKey(), entry.getValue(),
alreadyElectedCandidates.getOrDefault(entry.getKey(), Collections.emptyList()))));
return electedCandidates; return electedCandidates;
} }
@NonNull @NonNull
private Collection<Candidate> findCandidates(@NonNull Nomination nomination, int numberOfSeats) { private Collection<ElectedCandidate> findCandidates(@NonNull Nomination nomination,
int numberOfSeats, Collection<ElectedCandidate> alreadyElectedCandidates) {
List<Integer> individualVotesOrder = nomination.getVotingResult().getVotesPerPosition() List<Integer> individualVotesOrder = nomination.getVotingResult().getVotesPerPosition()
.entrySet().stream() .entrySet().stream()
.sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed()) .sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed())
.map(Entry::getKey) .map(Entry::getKey)
.toList(); .toList();
Collection<Candidate> elected = new ArrayList<>(); Collection<ElectedCandidate> elected = new ArrayList<>();
int seatsByVoteOrder = numberOfSeats; int seatsByVoteOrder = numberOfSeats;
Map<Candidate, ElectedCandidate> electedCandidateMap = alreadyElectedCandidates.stream()
.collect(Collectors.toMap(ElectedCandidate::candidate, Function.identity()));
if (nomination.supportsVotesOnNomination()) { if (nomination.supportsVotesOnNomination()) {
int seatsByNomination = calculateSeatsByNominationOrder(nomination, numberOfSeats); int seatsByNomination = calculateSeatsByNominationOrder(nomination, numberOfSeats);
seatsByVoteOrder = numberOfSeats - seatsByNomination; seatsByVoteOrder = numberOfSeats - seatsByNomination;
@@ -119,9 +130,11 @@ public class CalculationService {
int electedByNominationOrder = 0; int electedByNominationOrder = 0;
for (int i = 0; i < candidates.size() && electedByNominationOrder < seatsByNomination; i++) { for (int i = 0; i < candidates.size() && electedByNominationOrder < seatsByNomination; i++) {
Candidate candidate = candidates.get(i); Candidate candidate = candidates.get(i);
if (candidate.getElected() == Elected.NOT_ELECTED) { ElectedCandidate electedCandidate = electedCandidateMap.get(candidate);
candidate.setElected(Elected.OVERALL_NOMINATION_ORDER); if (electedCandidate == null) {
elected.add(candidate); electedCandidate = new ElectedCandidate(candidate, Elected.OVERALL_NOMINATION_ORDER);
elected.add(electedCandidate);
electedCandidateMap.put(candidate, electedCandidate);
electedByNominationOrder++; electedByNominationOrder++;
} }
} }
@@ -131,11 +144,12 @@ public class CalculationService {
for (int i = 0; i < individualVotesOrder.size() && electedByVoteOrder < seatsByVoteOrder; i++) { for (int i = 0; i < individualVotesOrder.size() && electedByVoteOrder < seatsByVoteOrder; i++) {
Integer position = individualVotesOrder.get(i); Integer position = individualVotesOrder.get(i);
Candidate candidate = nomination.getCandidate(position); Candidate candidate = nomination.getCandidate(position);
if (candidate.getElected() == Elected.NOT_ELECTED) { ElectedCandidate electedCandidate = electedCandidateMap.get(candidate);
candidate.setElected(nomination.supportsVotesOnNomination() if (electedCandidate == null) {
electedCandidate = new ElectedCandidate(candidate, nomination.supportsVotesOnNomination()
? Elected.OVERALL_VOTE_ORDER ? Elected.OVERALL_VOTE_ORDER
: Elected.CONSTITUENCY); : Elected.CONSTITUENCY);
elected.add(candidate); elected.add(electedCandidate);
electedByVoteOrder++; electedByVoteOrder++;
} }
} }
@@ -3,6 +3,7 @@ package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.model.Candidate; import de.twomartens.wahlrecht.model.Candidate;
import de.twomartens.wahlrecht.model.Constituency; import de.twomartens.wahlrecht.model.Constituency;
import de.twomartens.wahlrecht.model.Elected; import de.twomartens.wahlrecht.model.Elected;
import de.twomartens.wahlrecht.model.ElectedCandidate;
import de.twomartens.wahlrecht.model.ElectedResult; import de.twomartens.wahlrecht.model.ElectedResult;
import de.twomartens.wahlrecht.model.Election; import de.twomartens.wahlrecht.model.Election;
import de.twomartens.wahlrecht.model.Nomination; import de.twomartens.wahlrecht.model.Nomination;
@@ -140,76 +141,76 @@ class CalculationServiceTest {
FDP_BEZIRK, 3, FDP_BEZIRK, 3,
AFD_BEZIRK, 3 AFD_BEZIRK, 3
); );
setUpConstituencyResults(); Map<Nomination, Collection<ElectedCandidate>> electedCandidates = setUpConstituencyResults();
ElectedResult result = service.calculateElectedOverallCandidates(seatsPerNomination); ElectedResult result = service.calculateElectedOverallCandidates(seatsPerNomination, electedCandidates);
Assertions.assertThat(result.electedCandidates()) Assertions.assertThat(result.electedCandidates())
.anySatisfy((nomination, candidates) -> { .anySatisfy((nomination, candidates) -> {
Assertions.assertThat(nomination).isEqualTo(SPD_BEZIRK); Assertions.assertThat(nomination).isEqualTo(SPD_BEZIRK);
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch(candidate -> candidate.getName().equals(GOTTLIEB_GABOR) .anyMatch(candidate -> candidate.name().equals(GOTTLIEB_GABOR)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(RIEGEL_ANN_KATHRIN) .anyMatch(candidate -> candidate.name().equals(RIEGEL_ANN_KATHRIN)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER); && candidate.elected() == Elected.OVERALL_VOTE_ORDER);
}) })
.anySatisfy((nomination, candidates) -> { .anySatisfy((nomination, candidates) -> {
Assertions.assertThat(nomination).isEqualTo(CDU_BEZIRK); Assertions.assertThat(nomination).isEqualTo(CDU_BEZIRK);
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch(candidate -> candidate.getName().equals(HOEFLICH_JUTTA) .anyMatch(candidate -> candidate.name().equals(HOEFLICH_JUTTA)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(DR_LANGHEIN_A_W_HEINRICH) .anyMatch(candidate -> candidate.name().equals(DR_LANGHEIN_A_W_HEINRICH)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER); && candidate.elected() == Elected.OVERALL_VOTE_ORDER);
}) })
.anySatisfy((nomination, candidates) -> { .anySatisfy((nomination, candidates) -> {
Assertions.assertThat(nomination).isEqualTo(LINKE_BEZIRK); Assertions.assertThat(nomination).isEqualTo(LINKE_BEZIRK);
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch(candidate -> candidate.getName().equals(PAGELS_MANUELA) .anyMatch(candidate -> candidate.name().equals(PAGELS_MANUELA)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(WIEGMANN_ROLAND) .anyMatch(candidate -> candidate.name().equals(WIEGMANN_ROLAND)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(KLEINERT_MIKEY) .anyMatch(candidate -> candidate.name().equals(KLEINERT_MIKEY)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER); && candidate.elected() == Elected.OVERALL_VOTE_ORDER);
}) })
.anySatisfy((nomination, candidates) -> { .anySatisfy((nomination, candidates) -> {
Assertions.assertThat(nomination).isEqualTo(FDP_BEZIRK); Assertions.assertThat(nomination).isEqualTo(FDP_BEZIRK);
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch(candidate -> candidate.getName().equals(KRUEGER_KLAUS) .anyMatch(candidate -> candidate.name().equals(KRUEGER_KLAUS)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(SCHWANKE_BENJAMIN) .anyMatch(candidate -> candidate.name().equals(SCHWANKE_BENJAMIN)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(MUELLER_SOENKSEN_BURKHARDT) .anyMatch(candidate -> candidate.name().equals(MUELLER_SOENKSEN_BURKHARDT)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER); && candidate.elected() == Elected.OVERALL_VOTE_ORDER);
}) })
.anySatisfy((nomination, candidates) -> { .anySatisfy((nomination, candidates) -> {
Assertions.assertThat(nomination).isEqualTo(AFD_BEZIRK); Assertions.assertThat(nomination).isEqualTo(AFD_BEZIRK);
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch(candidate -> candidate.getName().equals(SCHOEMER_DIRK) .anyMatch(candidate -> candidate.name().equals(SCHOEMER_DIRK)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(ZIMMERMANN_ELKE) .anyMatch(candidate -> candidate.name().equals(ZIMMERMANN_ELKE)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(PILLATZKE_JOERG) .anyMatch(candidate -> candidate.name().equals(PILLATZKE_JOERG)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER); && candidate.elected() == Elected.OVERALL_VOTE_ORDER);
}) })
.anySatisfy((nomination, candidates) -> { .anySatisfy((nomination, candidates) -> {
Assertions.assertThat(nomination).isEqualTo(GRUENE_BEZIRK); Assertions.assertThat(nomination).isEqualTo(GRUENE_BEZIRK);
Assertions.assertThat(candidates) Assertions.assertThat(candidates)
.anyMatch(candidate -> candidate.getName().equals(KUHLMANN_DIETMAR) .anyMatch(candidate -> candidate.name().equals(KUHLMANN_DIETMAR)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(KLEIN_ROBERT) .anyMatch(candidate -> candidate.name().equals(KLEIN_ROBERT)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(ERK_ARAMAK) .anyMatch(candidate -> candidate.name().equals(ERK_ARAMAK)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(MARTENS_JIM) .anyMatch(candidate -> candidate.name().equals(MARTENS_JIM)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(DR_FISCHER_JOST_LEONHARDT) .anyMatch(candidate -> candidate.name().equals(DR_FISCHER_JOST_LEONHARDT)
&& candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER)
.anyMatch(candidate -> candidate.getName().equals(KUELL_GABRIELA) .anyMatch(candidate -> candidate.name().equals(KUELL_GABRIELA)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER) && candidate.elected() == Elected.OVERALL_VOTE_ORDER)
.anyMatch(candidate -> candidate.getName().equals(BOHNY_CARL_MARIA) .anyMatch(candidate -> candidate.name().equals(BOHNY_CARL_MARIA)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER) && candidate.elected() == Elected.OVERALL_VOTE_ORDER)
.anyMatch(candidate -> candidate.getName().equals(DORSCH_SEBASTIAN) .anyMatch(candidate -> candidate.name().equals(DORSCH_SEBASTIAN)
&& candidate.getElected() == Elected.OVERALL_VOTE_ORDER); && candidate.elected() == Elected.OVERALL_VOTE_ORDER);
}); });
} }
@@ -436,18 +437,20 @@ class CalculationServiceTest {
.build()); .build());
} }
void setUpConstituencyResults() { private Map<Nomination, Collection<ElectedCandidate>> setUpConstituencyResults() {
GRUENE_BEZIRK.getCandidate(1).setElected(Elected.CONSTITUENCY); return Map.of(GRUENE_BEZIRK, List.of(
GRUENE_BEZIRK.getCandidate(2).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(3).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(5).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(6).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(5), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(7).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(6), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(11).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(7), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(15).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(11), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(16).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(15), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(20).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(16), Elected.CONSTITUENCY),
GRUENE_BEZIRK.getCandidate(22).setElected(Elected.CONSTITUENCY); new ElectedCandidate(GRUENE_BEZIRK.getCandidate(20), Elected.CONSTITUENCY),
new ElectedCandidate(GRUENE_BEZIRK.getCandidate(22), Elected.CONSTITUENCY)
));
} }
private static Nomination buildNomination(String name, boolean supportVotesOnNomination) { private static Nomination buildNomination(String name, boolean supportVotesOnNomination) {