diff --git a/buildSrc/src/main/groovy/twomartens.java.gradle b/buildSrc/src/main/groovy/twomartens.java.gradle index 9b61efa..6b5f55a 100644 --- a/buildSrc/src/main/groovy/twomartens.java.gradle +++ b/buildSrc/src/main/groovy/twomartens.java.gradle @@ -25,7 +25,7 @@ configurations { } test { - systemProperty 'junit.jupiter.execution.parallel.enabled', true + systemProperty 'junit.jupiter.execution.parallel.enabled', false systemProperty 'junit.jupiter.execution.parallel.mode.default', "concurrent" useJUnitPlatform() maxHeapSize = "4g" diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/model/Candidate.java b/module-server/src/main/java/de/twomartens/wahlrecht/model/Candidate.java index 5392991..75cf245 100644 --- a/module-server/src/main/java/de/twomartens/wahlrecht/model/Candidate.java +++ b/module-server/src/main/java/de/twomartens/wahlrecht/model/Candidate.java @@ -1,16 +1,5 @@ package de.twomartens.wahlrecht.model; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; +public record Candidate(String name, String profession) { -@RequiredArgsConstructor -@Getter -@Setter -@ToString -public class Candidate { - private final String name; - private final String profession; - private Elected elected = Elected.NOT_ELECTED; } diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedCandidate.java b/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedCandidate.java new file mode 100644 index 0000000..c27d5bd --- /dev/null +++ b/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedCandidate.java @@ -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(); + } +} diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedResult.java b/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedResult.java index f2d4e59..33b2b43 100644 --- a/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedResult.java +++ b/module-server/src/main/java/de/twomartens/wahlrecht/model/ElectedResult.java @@ -6,7 +6,7 @@ import java.util.Map; import lombok.Builder; @Builder -public record ElectedResult(Map> electedCandidates, +public record ElectedResult(Map> electedCandidates, Deque usedElectionNumbers) { } diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/service/CalculationService.java b/module-server/src/main/java/de/twomartens/wahlrecht/service/CalculationService.java index 189a338..aebf413 100644 --- a/module-server/src/main/java/de/twomartens/wahlrecht/service/CalculationService.java +++ b/module-server/src/main/java/de/twomartens/wahlrecht/service/CalculationService.java @@ -3,6 +3,7 @@ package de.twomartens.wahlrecht.service; import de.twomartens.wahlrecht.model.Candidate; import de.twomartens.wahlrecht.model.Constituency; import de.twomartens.wahlrecht.model.Elected; +import de.twomartens.wahlrecht.model.ElectedCandidate; import de.twomartens.wahlrecht.model.ElectedResult; import de.twomartens.wahlrecht.model.Election; import de.twomartens.wahlrecht.model.Nomination; @@ -11,12 +12,14 @@ import de.twomartens.wahlrecht.model.VotingResult; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; @@ -39,7 +42,7 @@ public class CalculationService { nominations, numberOfSeats, initialElectionNumber); return ElectedResult.builder() - .electedCandidates(findElectedCandidates(assignedSeatsPerNomination)) + .electedCandidates(findElectedCandidates(assignedSeatsPerNomination, Collections.emptyMap())) .usedElectionNumbers(electionNumberHistory) .build(); } @@ -75,43 +78,51 @@ public class CalculationService { .build(); } - public ElectedResult calculateElectedOverallCandidates(Map seatsPerNomination) { + public ElectedResult calculateElectedOverallCandidates(Map seatsPerNomination, + Map> electedCandidates) { electionNumberHistory.clear(); return ElectedResult.builder() - .electedCandidates(findElectedCandidates(seatsPerNomination)) + .electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates)) .usedElectionNumbers(electionNumberHistory) .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() <= nomination.getVotingResult().getTotalVotes(); } @NonNull - private Map> findElectedCandidates( - @NonNull Map assignedSeatsPerNomination) { + private Map> findElectedCandidates( + @NonNull Map assignedSeatsPerNomination, + Map> alreadyElectedCandidates) { - Map> electedCandidates = new HashMap<>(); + Map> electedCandidates = new HashMap<>(); assignedSeatsPerNomination.entrySet().stream() .filter(entry -> entry.getValue() > 0) .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; } @NonNull - private Collection findCandidates(@NonNull Nomination nomination, int numberOfSeats) { + private Collection findCandidates(@NonNull Nomination nomination, + int numberOfSeats, Collection alreadyElectedCandidates) { List individualVotesOrder = nomination.getVotingResult().getVotesPerPosition() .entrySet().stream() .sorted(Comparator.comparing(Entry::getValue).reversed()) .map(Entry::getKey) .toList(); - Collection elected = new ArrayList<>(); + Collection elected = new ArrayList<>(); int seatsByVoteOrder = numberOfSeats; + Map electedCandidateMap = alreadyElectedCandidates.stream() + .collect(Collectors.toMap(ElectedCandidate::candidate, Function.identity())); + if (nomination.supportsVotesOnNomination()) { int seatsByNomination = calculateSeatsByNominationOrder(nomination, numberOfSeats); seatsByVoteOrder = numberOfSeats - seatsByNomination; @@ -119,9 +130,11 @@ public class CalculationService { int electedByNominationOrder = 0; for (int i = 0; i < candidates.size() && electedByNominationOrder < seatsByNomination; i++) { Candidate candidate = candidates.get(i); - if (candidate.getElected() == Elected.NOT_ELECTED) { - candidate.setElected(Elected.OVERALL_NOMINATION_ORDER); - elected.add(candidate); + ElectedCandidate electedCandidate = electedCandidateMap.get(candidate); + if (electedCandidate == null) { + electedCandidate = new ElectedCandidate(candidate, Elected.OVERALL_NOMINATION_ORDER); + elected.add(electedCandidate); + electedCandidateMap.put(candidate, electedCandidate); electedByNominationOrder++; } } @@ -131,11 +144,12 @@ public class CalculationService { for (int i = 0; i < individualVotesOrder.size() && electedByVoteOrder < seatsByVoteOrder; i++) { Integer position = individualVotesOrder.get(i); Candidate candidate = nomination.getCandidate(position); - if (candidate.getElected() == Elected.NOT_ELECTED) { - candidate.setElected(nomination.supportsVotesOnNomination() + ElectedCandidate electedCandidate = electedCandidateMap.get(candidate); + if (electedCandidate == null) { + electedCandidate = new ElectedCandidate(candidate, nomination.supportsVotesOnNomination() ? Elected.OVERALL_VOTE_ORDER : Elected.CONSTITUENCY); - elected.add(candidate); + elected.add(electedCandidate); electedByVoteOrder++; } } diff --git a/module-server/src/test/java/de/twomartens/wahlrecht/service/CalculationServiceTest.java b/module-server/src/test/java/de/twomartens/wahlrecht/service/CalculationServiceTest.java index 78a0903..09ec463 100644 --- a/module-server/src/test/java/de/twomartens/wahlrecht/service/CalculationServiceTest.java +++ b/module-server/src/test/java/de/twomartens/wahlrecht/service/CalculationServiceTest.java @@ -3,6 +3,7 @@ package de.twomartens.wahlrecht.service; import de.twomartens.wahlrecht.model.Candidate; import de.twomartens.wahlrecht.model.Constituency; import de.twomartens.wahlrecht.model.Elected; +import de.twomartens.wahlrecht.model.ElectedCandidate; import de.twomartens.wahlrecht.model.ElectedResult; import de.twomartens.wahlrecht.model.Election; import de.twomartens.wahlrecht.model.Nomination; @@ -140,76 +141,76 @@ class CalculationServiceTest { FDP_BEZIRK, 3, AFD_BEZIRK, 3 ); - setUpConstituencyResults(); + Map> electedCandidates = setUpConstituencyResults(); - ElectedResult result = service.calculateElectedOverallCandidates(seatsPerNomination); + ElectedResult result = service.calculateElectedOverallCandidates(seatsPerNomination, electedCandidates); Assertions.assertThat(result.electedCandidates()) .anySatisfy((nomination, candidates) -> { Assertions.assertThat(nomination).isEqualTo(SPD_BEZIRK); Assertions.assertThat(candidates) - .anyMatch(candidate -> candidate.getName().equals(GOTTLIEB_GABOR) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(RIEGEL_ANN_KATHRIN) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER); + .anyMatch(candidate -> candidate.name().equals(GOTTLIEB_GABOR) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(RIEGEL_ANN_KATHRIN) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER); }) .anySatisfy((nomination, candidates) -> { Assertions.assertThat(nomination).isEqualTo(CDU_BEZIRK); Assertions.assertThat(candidates) - .anyMatch(candidate -> candidate.getName().equals(HOEFLICH_JUTTA) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(DR_LANGHEIN_A_W_HEINRICH) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER); + .anyMatch(candidate -> candidate.name().equals(HOEFLICH_JUTTA) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(DR_LANGHEIN_A_W_HEINRICH) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER); }) .anySatisfy((nomination, candidates) -> { Assertions.assertThat(nomination).isEqualTo(LINKE_BEZIRK); Assertions.assertThat(candidates) - .anyMatch(candidate -> candidate.getName().equals(PAGELS_MANUELA) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(WIEGMANN_ROLAND) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(KLEINERT_MIKEY) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER); + .anyMatch(candidate -> candidate.name().equals(PAGELS_MANUELA) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(WIEGMANN_ROLAND) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(KLEINERT_MIKEY) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER); }) .anySatisfy((nomination, candidates) -> { Assertions.assertThat(nomination).isEqualTo(FDP_BEZIRK); Assertions.assertThat(candidates) - .anyMatch(candidate -> candidate.getName().equals(KRUEGER_KLAUS) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(SCHWANKE_BENJAMIN) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(MUELLER_SOENKSEN_BURKHARDT) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER); + .anyMatch(candidate -> candidate.name().equals(KRUEGER_KLAUS) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(SCHWANKE_BENJAMIN) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(MUELLER_SOENKSEN_BURKHARDT) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER); }) .anySatisfy((nomination, candidates) -> { Assertions.assertThat(nomination).isEqualTo(AFD_BEZIRK); Assertions.assertThat(candidates) - .anyMatch(candidate -> candidate.getName().equals(SCHOEMER_DIRK) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(ZIMMERMANN_ELKE) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(PILLATZKE_JOERG) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER); + .anyMatch(candidate -> candidate.name().equals(SCHOEMER_DIRK) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(ZIMMERMANN_ELKE) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(PILLATZKE_JOERG) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER); }) .anySatisfy((nomination, candidates) -> { Assertions.assertThat(nomination).isEqualTo(GRUENE_BEZIRK); Assertions.assertThat(candidates) - .anyMatch(candidate -> candidate.getName().equals(KUHLMANN_DIETMAR) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(KLEIN_ROBERT) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(ERK_ARAMAK) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(MARTENS_JIM) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(DR_FISCHER_JOST_LEONHARDT) - && candidate.getElected() == Elected.OVERALL_NOMINATION_ORDER) - .anyMatch(candidate -> candidate.getName().equals(KUELL_GABRIELA) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER) - .anyMatch(candidate -> candidate.getName().equals(BOHNY_CARL_MARIA) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER) - .anyMatch(candidate -> candidate.getName().equals(DORSCH_SEBASTIAN) - && candidate.getElected() == Elected.OVERALL_VOTE_ORDER); + .anyMatch(candidate -> candidate.name().equals(KUHLMANN_DIETMAR) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(KLEIN_ROBERT) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(ERK_ARAMAK) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(MARTENS_JIM) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(DR_FISCHER_JOST_LEONHARDT) + && candidate.elected() == Elected.OVERALL_NOMINATION_ORDER) + .anyMatch(candidate -> candidate.name().equals(KUELL_GABRIELA) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER) + .anyMatch(candidate -> candidate.name().equals(BOHNY_CARL_MARIA) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER) + .anyMatch(candidate -> candidate.name().equals(DORSCH_SEBASTIAN) + && candidate.elected() == Elected.OVERALL_VOTE_ORDER); }); } @@ -436,18 +437,20 @@ class CalculationServiceTest { .build()); } - void setUpConstituencyResults() { - GRUENE_BEZIRK.getCandidate(1).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(2).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(3).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(5).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(6).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(7).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(11).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(15).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(16).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(20).setElected(Elected.CONSTITUENCY); - GRUENE_BEZIRK.getCandidate(22).setElected(Elected.CONSTITUENCY); + private Map> setUpConstituencyResults() { + return Map.of(GRUENE_BEZIRK, List.of( + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(5), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(6), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(7), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(11), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(15), Elected.CONSTITUENCY), + new ElectedCandidate(GRUENE_BEZIRK.getCandidate(16), 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) {