Added calculation of constituency results
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
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,9 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
public record Constituency(String name, int numberOfSeats) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
public enum Elected {
|
||||||
|
NOT_ELECTED,
|
||||||
|
CONSTITUENCY,
|
||||||
|
OVERALL
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
public record ElectedResult(Map<Nomination, Collection<Candidate>> electedCandidates,
|
||||||
|
Deque<Double> usedElectionNumbers) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public record Election(String name, LocalDate day, VotingThreshold votingThreshold,
|
||||||
|
int totalNumberOfSeats,
|
||||||
|
Collection<Constituency> constituencies) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.ToString.Include;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ToString(onlyExplicitlyIncluded = true)
|
||||||
|
public class Nomination {
|
||||||
|
private final List<Candidate> candidates = new ArrayList<>();
|
||||||
|
private final Map<Candidate, Integer> candidateToPosition = new HashMap<>();
|
||||||
|
private final Map<Integer, Candidate> positionToCandidate = new HashMap<>();
|
||||||
|
@Include
|
||||||
|
private final String name;
|
||||||
|
@Getter
|
||||||
|
private final boolean supportVotesOnNomination;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private VotingResult votingResult;
|
||||||
|
|
||||||
|
private void addCandidate(Candidate candidate) {
|
||||||
|
candidates.add(candidate);
|
||||||
|
candidateToPosition.put(candidate, candidates.size());
|
||||||
|
positionToCandidate.put(candidates.size(), candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCandidates(Candidate... candidates) {
|
||||||
|
Arrays.stream(candidates).forEach(this::addCandidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Candidate getCandidate(int position) {
|
||||||
|
return positionToCandidate.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPosition(Candidate candidate) {
|
||||||
|
return candidateToPosition.get(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public class Party {
|
||||||
|
private final String name;
|
||||||
|
private Nomination overallNomination;
|
||||||
|
private Map<Constituency, Nomination> constituencyNominations;
|
||||||
|
private boolean passesVotingThreshold;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class VotingResult {
|
||||||
|
|
||||||
|
private final int votesOnNomination;
|
||||||
|
private final Map<Integer, Integer> votesPerPosition;
|
||||||
|
private Integer totalVotes = null;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
public VotingResult(int votesOnNomination, Map<Integer, Integer> votesPerPosition) {
|
||||||
|
this.votesOnNomination = votesOnNomination;
|
||||||
|
this.votesPerPosition = votesPerPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalVotes() {
|
||||||
|
if (totalVotes == null) {
|
||||||
|
totalVotes = votesOnNomination + votesPerPosition.values().stream()
|
||||||
|
.reduce(0, Integer::sum);
|
||||||
|
}
|
||||||
|
return totalVotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.twomartens.wahlrecht.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public enum VotingThreshold {
|
||||||
|
NONE(0.00),
|
||||||
|
THREE(0.03),
|
||||||
|
FIVE(0.05);
|
||||||
|
|
||||||
|
private final double multiplier;
|
||||||
|
}
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
package de.twomartens.wahlrecht.service;
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.Candidate;
|
||||||
|
import de.twomartens.wahlrecht.model.Constituency;
|
||||||
|
import de.twomartens.wahlrecht.model.ElectedResult;
|
||||||
|
import de.twomartens.wahlrecht.model.Nomination;
|
||||||
|
import de.twomartens.wahlrecht.model.VotingResult;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
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.stream.Collectors;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ConstituencyCalculationService {
|
||||||
|
private final Deque<Double> electionNumberHistory = new ArrayDeque<>();
|
||||||
|
|
||||||
|
public ElectedResult calculateConstituency(@NonNull Constituency constituency,
|
||||||
|
@NonNull Collection<Nomination> nominations) {
|
||||||
|
electionNumberHistory.clear();
|
||||||
|
int totalVotes = nominations.stream()
|
||||||
|
.map(Nomination::getVotingResult)
|
||||||
|
.map(VotingResult::getTotalVotes)
|
||||||
|
.reduce(0, Integer::sum);
|
||||||
|
int numberOfSeats = constituency.numberOfSeats();
|
||||||
|
double initialElectionNumber = totalVotes / (double) numberOfSeats;
|
||||||
|
|
||||||
|
Map<Nomination, Long> assignedSeatsPerNomination = calculateAssignedSeatsPerNomination(
|
||||||
|
nominations, numberOfSeats, initialElectionNumber);
|
||||||
|
|
||||||
|
return ElectedResult.builder()
|
||||||
|
.electedCandidates(findElectedCandidates(assignedSeatsPerNomination))
|
||||||
|
.usedElectionNumbers(electionNumberHistory)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Map<Nomination, Collection<Candidate>> findElectedCandidates(
|
||||||
|
@NonNull Map<Nomination, Long> assignedSeatsPerNomination) {
|
||||||
|
|
||||||
|
Map<Nomination, Collection<Candidate>> electedCandidates = new HashMap<>();
|
||||||
|
assignedSeatsPerNomination.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue() > 0)
|
||||||
|
.forEach(entry ->
|
||||||
|
electedCandidates.put(entry.getKey(), findCandidates(entry.getKey(), entry.getValue())));
|
||||||
|
|
||||||
|
return electedCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Collection<Candidate> findCandidates(@NonNull Nomination nomination, long numberOfSeats) {
|
||||||
|
List<Integer> positions = nomination.getVotingResult().getVotesPerPosition().entrySet().stream()
|
||||||
|
.sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed())
|
||||||
|
.map(Entry::getKey)
|
||||||
|
.toList();
|
||||||
|
Collection<Candidate> elected = new ArrayList<>();
|
||||||
|
for (int i = 0; i < numberOfSeats; i++) {
|
||||||
|
Integer position = positions.get(i);
|
||||||
|
elected.add(nomination.getCandidate(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
return elected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Nomination, Long> calculateAssignedSeatsPerNomination(
|
||||||
|
@NonNull Collection<Nomination> nominations,
|
||||||
|
int numberOfSeats, Double initialElectionNumber) {
|
||||||
|
|
||||||
|
double electionNumber = initialElectionNumber;
|
||||||
|
long assignedSeats;
|
||||||
|
Map<Nomination, Long> assignedSeatsPerNomination;
|
||||||
|
|
||||||
|
do {
|
||||||
|
electionNumberHistory.add(electionNumber);
|
||||||
|
Map<Nomination, Double> seatsPerNomination = new HashMap<>();
|
||||||
|
for (Nomination nomination : nominations) {
|
||||||
|
double seatNumber = calculateAssignedSeatNumber(electionNumber,
|
||||||
|
nomination.getVotingResult().getTotalVotes());
|
||||||
|
seatsPerNomination.put(nomination, seatNumber);
|
||||||
|
}
|
||||||
|
assignedSeatsPerNomination = seatsPerNomination.entrySet().stream()
|
||||||
|
.map(entry -> Map.entry(entry.getKey(), Math.round(entry.getValue())))
|
||||||
|
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
||||||
|
assignedSeats = assignedSeatsPerNomination.values().stream().reduce(0L, Long::sum);
|
||||||
|
|
||||||
|
if (assignedSeats < numberOfSeats) {
|
||||||
|
// election number was too big, decrease
|
||||||
|
electionNumber = calculateLowerElectionNumber(initialElectionNumber, seatsPerNomination,
|
||||||
|
assignedSeatsPerNomination);
|
||||||
|
} else if (assignedSeats > numberOfSeats) {
|
||||||
|
// election number was too small, increase
|
||||||
|
electionNumber = calculateHigherElectionNumber(initialElectionNumber, seatsPerNomination,
|
||||||
|
assignedSeatsPerNomination);
|
||||||
|
}
|
||||||
|
} while (assignedSeats != numberOfSeats);
|
||||||
|
|
||||||
|
|
||||||
|
return assignedSeatsPerNomination;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double calculateHigherElectionNumber(Double initialElectionNumber,
|
||||||
|
@NonNull Map<Nomination, Double> seatsPerNomination,
|
||||||
|
Map<Nomination, Long> assignedSeatsPerNomination) {
|
||||||
|
double electionNumber;
|
||||||
|
electionNumber = seatsPerNomination.entrySet().stream()
|
||||||
|
.map(entry -> Map.entry(entry.getKey(),
|
||||||
|
entry.getValue() - assignedSeatsPerNomination.get(entry.getKey())))
|
||||||
|
.min(Comparator.comparing(Entry<Nomination, Double>::getValue))
|
||||||
|
.map(entry -> entry.getKey().getVotingResult().getTotalVotes()
|
||||||
|
/ (assignedSeatsPerNomination.get(entry.getKey()) - 0.5))
|
||||||
|
.orElse(initialElectionNumber);
|
||||||
|
return electionNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double calculateLowerElectionNumber(Double initialElectionNumber,
|
||||||
|
@NonNull Map<Nomination, Double> seatsPerNomination,
|
||||||
|
Map<Nomination, Long> assignedSeatsPerNomination) {
|
||||||
|
double electionNumber;
|
||||||
|
electionNumber = seatsPerNomination.entrySet().stream()
|
||||||
|
.map(entry -> Map.entry(entry.getKey(),
|
||||||
|
entry.getValue() - assignedSeatsPerNomination.get(entry.getKey())))
|
||||||
|
.max(Comparator.comparing(Entry<Nomination, Double>::getValue))
|
||||||
|
.map(entry -> entry.getKey().getVotingResult().getTotalVotes()
|
||||||
|
/ (assignedSeatsPerNomination.get(entry.getKey()) + 0.5))
|
||||||
|
.orElse(initialElectionNumber);
|
||||||
|
return electionNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private double calculateAssignedSeatNumber(double electionNumber, int totalVotesOfNomination) {
|
||||||
|
return totalVotesOfNomination / electionNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
package de.twomartens.wahlrecht.service;
|
||||||
|
|
||||||
|
import de.twomartens.wahlrecht.model.Candidate;
|
||||||
|
import de.twomartens.wahlrecht.model.Constituency;
|
||||||
|
import de.twomartens.wahlrecht.model.ElectedResult;
|
||||||
|
import de.twomartens.wahlrecht.model.Nomination;
|
||||||
|
import de.twomartens.wahlrecht.model.VotingResult;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ConstituencyCalculationServiceTest {
|
||||||
|
private static final Nomination SPD_NIENDORF = buildNomination("SPD-Niendorf");
|
||||||
|
private static final Nomination CDU_NIENDORF = buildNomination("CDU-Niendorf");
|
||||||
|
private static final Nomination FDP_NIENDORF = buildNomination("FDP-Niendorf");
|
||||||
|
private static final Nomination LINKE_NIENDORF = buildNomination("LINKE-Niendorf");
|
||||||
|
private static final Nomination GRUENE_NIENDORF = buildNomination("GRUENE-Niendorf");
|
||||||
|
private static final Nomination AFD_NIENDORF = buildNomination("AFD-Niendorf");
|
||||||
|
|
||||||
|
private ConstituencyCalculationService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setService() {
|
||||||
|
service = new ConstituencyCalculationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCalculateConstituencyCorrectly() {
|
||||||
|
Constituency constituency = new Constituency("Niendorf", 5);
|
||||||
|
Collection<Nomination> nominations = List.of(
|
||||||
|
SPD_NIENDORF,
|
||||||
|
CDU_NIENDORF,
|
||||||
|
LINKE_NIENDORF,
|
||||||
|
FDP_NIENDORF,
|
||||||
|
GRUENE_NIENDORF,
|
||||||
|
AFD_NIENDORF
|
||||||
|
);
|
||||||
|
setUpCandidates();
|
||||||
|
setUpResults();
|
||||||
|
|
||||||
|
ElectedResult elected = service.calculateConstituency(constituency, nominations);
|
||||||
|
|
||||||
|
Assertions.assertThat(elected.electedCandidates())
|
||||||
|
.containsKey(SPD_NIENDORF)
|
||||||
|
.containsKey(CDU_NIENDORF)
|
||||||
|
.containsKey(GRUENE_NIENDORF)
|
||||||
|
.doesNotContainKey(LINKE_NIENDORF)
|
||||||
|
.doesNotContainKey(AFD_NIENDORF)
|
||||||
|
.doesNotContainKey(FDP_NIENDORF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpCandidates() {
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Schwarzarius, Ines", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Urbanski, Torge", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Fischbach-Pyttel, Carola", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Bäcker, Guido", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Porr, Ulrike", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Petersen, Johannes", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Kröger, Manuela-Andrea", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Steppat, Wolfgang", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Canbay, Berfin", ""));
|
||||||
|
SPD_NIENDORF.addCandidates(new Candidate("Schumacher, Wolfgang", ""));
|
||||||
|
|
||||||
|
CDU_NIENDORF.addCandidates(new Candidate("Kuhn, Rüdiger", ""));
|
||||||
|
CDU_NIENDORF.addCandidates(new Candidate("Ahrens, Mareike", ""));
|
||||||
|
CDU_NIENDORF.addCandidates(new Candidate("Wysocki, Michael", ""));
|
||||||
|
CDU_NIENDORF.addCandidates(new Candidate("Dönselmann, Malte", ""));
|
||||||
|
CDU_NIENDORF.addCandidates(new Candidate("Seehaber, Daniel", ""));
|
||||||
|
|
||||||
|
LINKE_NIENDORF.addCandidates(new Candidate("Dr. Khenkhar, Malik", ""));
|
||||||
|
LINKE_NIENDORF.addCandidates(new Candidate("Radom, Claudia", ""));
|
||||||
|
LINKE_NIENDORF.addCandidates(new Candidate("Kara, Başak", ""));
|
||||||
|
|
||||||
|
FDP_NIENDORF.addCandidates(new Candidate("Häffs, Marvin", ""));
|
||||||
|
FDP_NIENDORF.addCandidates(new Candidate("Bahmer, Larissa", ""));
|
||||||
|
FDP_NIENDORF.addCandidates(new Candidate("Patzer, Heinrich-Otto", ""));
|
||||||
|
|
||||||
|
GRUENE_NIENDORF.addCandidates(new Candidate("Demirhan, Sina", ""));
|
||||||
|
GRUENE_NIENDORF.addCandidates(new Candidate("Schmidt, Lutz", "Redakteur"));
|
||||||
|
|
||||||
|
AFD_NIENDORF.addCandidates(new Candidate("Hanssen, Wolfgang", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUpResults() {
|
||||||
|
SPD_NIENDORF.setVotingResult(VotingResult.builder()
|
||||||
|
.votesOnNomination(0)
|
||||||
|
.votesPerPosition(Map.of(
|
||||||
|
1, 11882,
|
||||||
|
2, 7617,
|
||||||
|
3, 1946,
|
||||||
|
4, 1743,
|
||||||
|
5, 1363,
|
||||||
|
6, 762,
|
||||||
|
7, 895,
|
||||||
|
8, 1598,
|
||||||
|
9, 1786,
|
||||||
|
10, 781
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
CDU_NIENDORF.setVotingResult(VotingResult.builder()
|
||||||
|
.votesOnNomination(0)
|
||||||
|
.votesPerPosition(Map.of(
|
||||||
|
1, 10733,
|
||||||
|
2, 4550,
|
||||||
|
3, 2310,
|
||||||
|
4, 2333,
|
||||||
|
5, 2565
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
LINKE_NIENDORF.setVotingResult(VotingResult.builder()
|
||||||
|
.votesOnNomination(0)
|
||||||
|
.votesPerPosition(Map.of(
|
||||||
|
1, 3782,
|
||||||
|
2, 1584,
|
||||||
|
3, 1185
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
FDP_NIENDORF.setVotingResult(VotingResult.builder()
|
||||||
|
.votesOnNomination(0)
|
||||||
|
.votesPerPosition(Map.of(
|
||||||
|
1, 2498,
|
||||||
|
2, 3200,
|
||||||
|
3, 992
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
GRUENE_NIENDORF.setVotingResult(VotingResult.builder()
|
||||||
|
.votesOnNomination(0)
|
||||||
|
.votesPerPosition(Map.of(
|
||||||
|
1, 14838,
|
||||||
|
2, 12501
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
AFD_NIENDORF.setVotingResult(VotingResult.builder()
|
||||||
|
.votesOnNomination(0)
|
||||||
|
.votesPerPosition(Map.of(
|
||||||
|
1, 6616
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Nomination buildNomination(String name) {
|
||||||
|
return new Nomination(name, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user