Added calculation of constituency results

This commit is contained in:
2023-07-02 18:04:03 +02:00
parent 681b738919
commit 30d2c39580
11 changed files with 455 additions and 0 deletions
@@ -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;
}
@@ -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;
}
}