Added ability to calculate election result

This commit is contained in:
2023-07-13 23:23:54 +02:00
parent da22c792da
commit a8d4d47f70
15 changed files with 329 additions and 62 deletions
@@ -0,0 +1,58 @@
package de.twomartens.wahlrecht.controller.v1;
import de.twomartens.wahlrecht.mapper.v1.ElectedCandidatesMapper;
import de.twomartens.wahlrecht.mapper.v1.ElectionResultMapper;
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates;
import de.twomartens.wahlrecht.model.dto.v1.ElectionResult;
import de.twomartens.wahlrecht.service.CalculationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/wahlrecht/v1")
@Tag(name = "Calculations", description = "all requests relating to calculations")
public class CalculationController {
private final ElectedCandidatesMapper electedCandidatesMapper = Mappers.getMapper(
ElectedCandidatesMapper.class);
private final ElectionResultMapper electionResultMapper = Mappers.getMapper(
ElectionResultMapper.class);
private final CalculationService service;
@Operation(
summary = "Calculates a provided election result",
description = "This request does not store any result and is idempotent.",
responses = {
@ApiResponse(responseCode = "200",
description = "Returns all elected candidates",
content = {@Content(
mediaType = "application/json",
schema = @Schema(implementation = ElectedCandidates.class))}
)
}
)
@PostMapping(value = "/calculate", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ElectedCandidates> calculateResult(
@RequestBody ElectionResult electionResult
) {
ElectedCandidates result = electedCandidatesMapper.mapToExternal(
service.determineElectedCandidates(electionResultMapper.mapToInternal(electionResult)));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(result);
}
}
@@ -0,0 +1,28 @@
package de.twomartens.wahlrecht.mapper.v1;
import de.twomartens.wahlrecht.model.internal.ElectedCandidate;
import de.twomartens.wahlrecht.model.internal.ElectedCandidates;
import de.twomartens.wahlrecht.model.internal.VotingResult;
import java.util.Collection;
import java.util.Map;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ElectedCandidatesMapper {
ElectedResultMapper ELECTED_RESULT_MAPPER = Mappers.getMapper(ElectedResultMapper.class);
de.twomartens.wahlrecht.model.dto.v1.ElectedCandidates mapToExternal(ElectedCandidates candidate);
default Map<String, Collection<de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate>> mapToExternal(
Map<VotingResult, Collection<ElectedCandidate>> value) {
return ELECTED_RESULT_MAPPER.mapToExternal(value);
}
}
@@ -1,7 +1,6 @@
package de.twomartens.wahlrecht.mapper.v1; package de.twomartens.wahlrecht.mapper.v1;
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate; import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate;
import de.twomartens.wahlrecht.model.dto.v1.NominationId;
import de.twomartens.wahlrecht.model.internal.ElectedResult; import de.twomartens.wahlrecht.model.internal.ElectedResult;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -11,6 +10,7 @@ import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.NullValueCheckStrategy; import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.ReportingPolicy; import org.mapstruct.ReportingPolicy;
import org.springframework.lang.NonNull;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, @Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
@@ -19,16 +19,21 @@ public interface ElectedResultMapper {
de.twomartens.wahlrecht.model.dto.v1.ElectedResult mapToExternal(ElectedResult result); de.twomartens.wahlrecht.model.dto.v1.ElectedResult mapToExternal(ElectedResult result);
Map<NominationId, Collection<ElectedCandidate>> mapToExternal( default Map<String, Collection<ElectedCandidate>> mapToExternal(
Map<de.twomartens.wahlrecht.model.internal.NominationId, @NonNull Map<de.twomartens.wahlrecht.model.internal.VotingResult,
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value); Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value) {
return value.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey().getNominationId().partyAbbreviation(),
entry -> mapToExternal(entry.getValue())));
}
Collection<ElectedCandidate> mapToExternal( Collection<ElectedCandidate> mapToExternal(
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate> value); Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate> value);
default Map<de.twomartens.wahlrecht.model.internal.NominationId, default Map<de.twomartens.wahlrecht.model.internal.NominationId,
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> map( Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> map(
Map<de.twomartens.wahlrecht.model.internal.VotingResult, @NonNull Map<de.twomartens.wahlrecht.model.internal.VotingResult,
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value) { Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value) {
return value.entrySet().stream() return value.entrySet().stream()
.collect(Collectors.toMap(entry -> entry.getKey().getNominationId(), Entry::getValue)); .collect(Collectors.toMap(entry -> entry.getKey().getNominationId(), Entry::getValue));
@@ -9,13 +9,17 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.NullValueCheckStrategy; import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.ReportingPolicy; import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, @Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
unmappedTargetPolicy = ReportingPolicy.IGNORE) unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ElectionResultMapper { public interface ElectionResultMapper {
de.twomartens.wahlrecht.model.dto.v1.ElectionResult mapToExternal(de.twomartens.wahlrecht.model.db.ElectionResult result); VotingResultMapper VOTING_RESULT_MAPPER = Mappers.getMapper(VotingResultMapper.class);
de.twomartens.wahlrecht.model.dto.v1.ElectionResult mapToExternal(
de.twomartens.wahlrecht.model.db.ElectionResult result);
Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> mapToExternal( Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> mapToExternal(
Collection<de.twomartens.wahlrecht.model.db.VotingResult> results); Collection<de.twomartens.wahlrecht.model.db.VotingResult> results);
@@ -36,6 +40,10 @@ public interface ElectionResultMapper {
ElectionResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.ElectionResult result); ElectionResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.ElectionResult result);
default VotingResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.VotingResult result) {
return VOTING_RESULT_MAPPER.mapToInternal(result);
}
Collection<VotingResult> mapToInternal( Collection<VotingResult> mapToInternal(
Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> results); Collection<de.twomartens.wahlrecht.model.dto.v1.VotingResult> results);
@@ -6,13 +6,14 @@ import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.NullValueCheckStrategy; import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.ReportingPolicy; import org.mapstruct.ReportingPolicy;
import org.springframework.lang.NonNull;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, @Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
unmappedTargetPolicy = ReportingPolicy.IGNORE) unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface VotingResultMapper { public interface VotingResultMapper {
default de.twomartens.wahlrecht.model.dto.v1.VotingResult mapToExternal(VotingResult result) { default de.twomartens.wahlrecht.model.dto.v1.VotingResult mapToExternal(@NonNull VotingResult result) {
return new de.twomartens.wahlrecht.model.dto.v1.VotingResult( return new de.twomartens.wahlrecht.model.dto.v1.VotingResult(
result.getNominationId().electionName(), result.getNominationId().electionName(),
result.getNominationId().partyAbbreviation(), result.getNominationId().partyAbbreviation(),
@@ -23,7 +24,7 @@ public interface VotingResultMapper {
); );
} }
default VotingResult mapToInternal(de.twomartens.wahlrecht.model.dto.v1.VotingResult result) { default VotingResult mapToInternal(@NonNull de.twomartens.wahlrecht.model.dto.v1.VotingResult result) {
return new VotingResult( return new VotingResult(
new NominationId(result.electionName(), result.partyAbbreviation(), new NominationId(result.electionName(), result.partyAbbreviation(),
result.nominationName()), result.nominationName()),
@@ -1,12 +1,4 @@
package de.twomartens.wahlrecht.model.dto.v1; package de.twomartens.wahlrecht.model.dto.v1;
public record ElectedCandidate(Candidate candidate, Elected elected) { public record ElectedCandidate(Candidate candidate, Elected elected) {
public String name() {
return candidate.name();
}
public String profession() {
return candidate.profession();
}
} }
@@ -0,0 +1,10 @@
package de.twomartens.wahlrecht.model.dto.v1;
import java.util.LinkedList;
import java.util.Map;
public record ElectedCandidates(ElectedResult overallResult,
Map<Integer, ElectedResult> constituencyResults,
LinkedList<Double> electionNumbersForSeatAllocation) {
}
@@ -1,13 +1,12 @@
package de.twomartens.wahlrecht.model.dto.v1; package de.twomartens.wahlrecht.model.dto.v1;
import de.twomartens.wahlrecht.model.internal.NominationId;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import lombok.Builder; import lombok.Builder;
@Builder @Builder
public record ElectedResult(Map<NominationId, Collection<ElectedCandidate>> electedCandidates, public record ElectedResult(Map<String, Collection<ElectedCandidate>> electedCandidates,
LinkedList<Double> usedElectionNumbers) { LinkedList<Double> usedElectionNumbers) {
} }
@@ -0,0 +1,5 @@
package de.twomartens.wahlrecht.model.internal;
public record ConstituencyId(String electionName, Integer number) {
}
@@ -0,0 +1,12 @@
package de.twomartens.wahlrecht.model.internal;
import java.util.LinkedList;
import java.util.Map;
import lombok.Builder;
@Builder
public record ElectedCandidates(ElectedResult overallResult,
Map<Integer, ElectedResult> constituencyResults,
LinkedList<Double> electionNumbersForSeatAllocation) {
}
@@ -4,8 +4,10 @@ import de.twomartens.wahlrecht.model.internal.Candidate;
import de.twomartens.wahlrecht.model.internal.Constituency; import de.twomartens.wahlrecht.model.internal.Constituency;
import de.twomartens.wahlrecht.model.internal.Elected; import de.twomartens.wahlrecht.model.internal.Elected;
import de.twomartens.wahlrecht.model.internal.ElectedCandidate; import de.twomartens.wahlrecht.model.internal.ElectedCandidate;
import de.twomartens.wahlrecht.model.internal.ElectedCandidates;
import de.twomartens.wahlrecht.model.internal.ElectedResult; import de.twomartens.wahlrecht.model.internal.ElectedResult;
import de.twomartens.wahlrecht.model.internal.Election; import de.twomartens.wahlrecht.model.internal.Election;
import de.twomartens.wahlrecht.model.internal.ElectionResult;
import de.twomartens.wahlrecht.model.internal.Nomination; import de.twomartens.wahlrecht.model.internal.Nomination;
import de.twomartens.wahlrecht.model.internal.NominationId; import de.twomartens.wahlrecht.model.internal.NominationId;
import de.twomartens.wahlrecht.model.internal.SeatResult; import de.twomartens.wahlrecht.model.internal.SeatResult;
@@ -22,22 +24,69 @@ import java.util.Map.Entry;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
@Slf4j
public class CalculationService { public class CalculationService {
private final NominationService nominationService; private final NominationService nominationService;
private final ElectionService electionService;
private final LinkedList<Double> electionNumberHistory = new LinkedList<>(); private final LinkedList<Double> electionNumberHistory = new LinkedList<>();
private StopWatch stopWatch;
public ElectedCandidates determineElectedCandidates(@NonNull ElectionResult electionResult) {
log.info("Calculate election result for election {}", electionResult.electionName());
stopWatch = new StopWatch();
stopWatch.start("determineElectedCandidates");
Election election = electionService.getElectionInternal(electionResult.electionName());
SeatResult seatResult = calculateOverallSeatDistribution(election,
electionResult.overallResults());
Map<Integer, ElectedResult> constituencyResults = new HashMap<>();
for (Constituency constituency : election.constituencies()) {
ElectedResult electedResult = calculateConstituency(constituency,
electionResult.constituencyResults().get(constituency.number()));
constituencyResults.put(constituency.number(), electedResult);
}
Map<String, Collection<ElectedCandidate>> electedCandidatesMap = constituencyResults
.entrySet().stream()
.flatMap(entry -> entry.getValue().electedCandidates().entrySet().stream())
.collect(Collectors.groupingBy(entry -> entry.getKey().getNominationId().partyAbbreviation(),
Collectors.flatMapping(entry -> entry.getValue().stream(),
Collectors.toCollection(ArrayList::new))));
Map<VotingResult, Integer> remainingSeatsPerParty = seatResult.seatsPerResult()
.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(),
entry.getValue() - electedCandidatesMap.getOrDefault(
entry.getKey().getNominationId().partyAbbreviation(),
Collections.emptyList()).size()))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
ElectedResult overallElectedCandidates = calculateElectedOverallCandidates(
remainingSeatsPerParty,
electedCandidatesMap
);
stopWatch.stop();
log.debug("determineCandidates took {} seconds", stopWatch.getTotalTimeSeconds());
return ElectedCandidates.builder()
.overallResult(overallElectedCandidates)
.constituencyResults(constituencyResults)
.electionNumbersForSeatAllocation(seatResult.usedElectionNumbers())
.build();
}
public ElectedResult calculateConstituency(@NonNull Constituency constituency, public ElectedResult calculateConstituency(@NonNull Constituency constituency,
@NonNull Collection<VotingResult> votingResults) { @NonNull Collection<VotingResult> votingResults) {
electionNumberHistory.clear(); electionNumberHistory.clear();
log.info("Calculate constituency {}", constituency.name());
int totalVotes = votingResults.stream() int totalVotes = votingResults.stream()
.map(VotingResult::getTotalVotes) .map(VotingResult::getTotalVotes)
.reduce(0, Integer::sum); .reduce(0, Integer::sum);
@@ -49,12 +98,13 @@ public class CalculationService {
return ElectedResult.builder() return ElectedResult.builder()
.electedCandidates(findElectedCandidates(assignedSeatsPerResult, Collections.emptyMap())) .electedCandidates(findElectedCandidates(assignedSeatsPerResult, Collections.emptyMap()))
.usedElectionNumbers(electionNumberHistory) .usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build(); .build();
} }
public SeatResult calculateOverallSeatDistribution(@NonNull Election election, public SeatResult calculateOverallSeatDistribution(@NonNull Election election,
@NonNull Collection<VotingResult> votingResults) { @NonNull Collection<VotingResult> votingResults) {
log.info("Calculate overall seat distribution for election {}", election.name());
electionNumberHistory.clear(); electionNumberHistory.clear();
int totalVotes = votingResults.stream() int totalVotes = votingResults.stream()
.map(VotingResult::getTotalVotes) .map(VotingResult::getTotalVotes)
@@ -64,8 +114,12 @@ public class CalculationService {
int totalIgnoredVotes = 0; int totalIgnoredVotes = 0;
for (VotingResult votingResult : votingResults) { for (VotingResult votingResult : votingResults) {
if (passesVotingThreshold(election, totalVotes, votingResult)) { if (passesVotingThreshold(election, totalVotes, votingResult)) {
log.info("Party passes voting threshold: {}",
votingResult.getNominationId().partyAbbreviation());
validVotingResults.add(votingResult); validVotingResults.add(votingResult);
} else { } else {
log.info("Party fails voting threshold: {}",
votingResult.getNominationId().partyAbbreviation());
totalIgnoredVotes += votingResult.getTotalVotes(); totalIgnoredVotes += votingResult.getTotalVotes();
} }
} }
@@ -79,18 +133,25 @@ public class CalculationService {
return SeatResult.builder() return SeatResult.builder()
.seatsPerResult(assignedSeatsPerResult) .seatsPerResult(assignedSeatsPerResult)
.usedElectionNumbers(electionNumberHistory) .usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build(); .build();
} }
/**
* Finds all elected candidates for given voting results
*
* @param seatsPerNomination Seats to allocate per voting result
* @param electedCandidates Already elected candidates per party
*/
public ElectedResult calculateElectedOverallCandidates( public ElectedResult calculateElectedOverallCandidates(
Map<VotingResult, Integer> seatsPerNomination, Map<VotingResult, Integer> seatsPerNomination,
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates) { Map<String, Collection<ElectedCandidate>> electedCandidates) {
log.info("Calculate overall elected candidates");
electionNumberHistory.clear(); electionNumberHistory.clear();
return ElectedResult.builder() return ElectedResult.builder()
.electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates)) .electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates))
.usedElectionNumbers(electionNumberHistory) .usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build(); .build();
} }
@@ -100,10 +161,16 @@ public class CalculationService {
<= votingResult.getTotalVotes(); <= votingResult.getTotalVotes();
} }
/**
* Finds all elected candidates for given voting results.
*
* @param assignedSeatsPerNomination Map of seats to allocate for a voting result
* @param alreadyElectedCandidates Map of already elected candidates per party
*/
@NonNull @NonNull
private Map<VotingResult, Collection<ElectedCandidate>> findElectedCandidates( private Map<VotingResult, Collection<ElectedCandidate>> findElectedCandidates(
@NonNull Map<VotingResult, Integer> assignedSeatsPerNomination, @NonNull Map<VotingResult, Integer> assignedSeatsPerNomination,
Map<VotingResult, Collection<ElectedCandidate>> alreadyElectedCandidates) { Map<String, Collection<ElectedCandidate>> alreadyElectedCandidates) {
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates = new HashMap<>(); Map<VotingResult, Collection<ElectedCandidate>> electedCandidates = new HashMap<>();
for (Entry<VotingResult, Integer> entry : assignedSeatsPerNomination.entrySet()) { for (Entry<VotingResult, Integer> entry : assignedSeatsPerNomination.entrySet()) {
@@ -111,7 +178,9 @@ public class CalculationService {
Collection<ElectedCandidate> candidates = findCandidates( Collection<ElectedCandidate> candidates = findCandidates(
entry.getKey(), entry.getKey(),
entry.getValue(), entry.getValue(),
alreadyElectedCandidates.getOrDefault(entry.getKey(), Collections.emptyList()) alreadyElectedCandidates.getOrDefault(
entry.getKey().getNominationId().partyAbbreviation(),
Collections.emptyList())
); );
electedCandidates.put(entry.getKey(), candidates); electedCandidates.put(entry.getKey(), candidates);
} }
@@ -120,9 +189,18 @@ public class CalculationService {
return electedCandidates; return electedCandidates;
} }
/**
* Finds elected candidates for given voting result.
*
* @param votingResult Result to search candidates for
* @param numberOfSeats Number of seats to allocate
* @param alreadyElectedCandidates Candidates already elected previously
*/
@NonNull @NonNull
private Collection<ElectedCandidate> findCandidates(@NonNull VotingResult votingResult, private Collection<ElectedCandidate> findCandidates(@NonNull VotingResult votingResult,
int numberOfSeats, Collection<ElectedCandidate> alreadyElectedCandidates) { int numberOfSeats, @NonNull Collection<ElectedCandidate> alreadyElectedCandidates) {
log.info("Find elected candidates on nomination: {}",
votingResult.getNominationId().name());
List<Integer> individualVotesOrder = votingResult.getVotesPerPosition() List<Integer> individualVotesOrder = votingResult.getVotesPerPosition()
.entrySet().stream() .entrySet().stream()
.sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed()) .sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed())
@@ -184,6 +262,7 @@ public class CalculationService {
double electionNumber = initialElectionNumber; double electionNumber = initialElectionNumber;
long assignedSeats; long assignedSeats;
Map<VotingResult, Integer> assignedSeatsPerVotingResult; Map<VotingResult, Integer> assignedSeatsPerVotingResult;
log.debug("Calculate assigned seats with initial election number {}", electionNumber);
do { do {
electionNumberHistory.add(electionNumber); electionNumberHistory.add(electionNumber);
@@ -202,10 +281,12 @@ public class CalculationService {
// election number was too big, decrease // election number was too big, decrease
electionNumber = calculateLowerElectionNumber(initialElectionNumber, seatsPerVotingResult, electionNumber = calculateLowerElectionNumber(initialElectionNumber, seatsPerVotingResult,
assignedSeatsPerVotingResult); assignedSeatsPerVotingResult);
log.debug("Calculated lower election number {}", electionNumber);
} else if (assignedSeats > numberOfSeats) { } else if (assignedSeats > numberOfSeats) {
// election number was too small, increase // election number was too small, increase
electionNumber = calculateHigherElectionNumber(initialElectionNumber, seatsPerVotingResult, electionNumber = calculateHigherElectionNumber(initialElectionNumber, seatsPerVotingResult,
assignedSeatsPerVotingResult); assignedSeatsPerVotingResult);
log.debug("Calculated higher election number {}", electionNumber);
} }
} while (assignedSeats != numberOfSeats); } while (assignedSeats != numberOfSeats);
@@ -216,12 +297,10 @@ public class CalculationService {
@NonNull Map<VotingResult, Double> seatsPerVotingResult, @NonNull Map<VotingResult, Double> seatsPerVotingResult,
Map<VotingResult, Integer> assignedSeatsPerVotingResult) { Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
double electionNumber; double electionNumber;
electionNumber = seatsPerVotingResult.entrySet().stream() electionNumber = seatsPerVotingResult.keySet().stream()
.map(entry -> Map.entry(entry.getKey(), .map(votingResult -> votingResult.getTotalVotes()
entry.getValue() - assignedSeatsPerVotingResult.get(entry.getKey()))) / (assignedSeatsPerVotingResult.get(votingResult) - 0.5))
.min(Comparator.comparing(Entry<VotingResult, Double>::getValue)) .min(Comparator.comparing(Double::doubleValue))
.map(entry -> entry.getKey().getTotalVotes()
/ (assignedSeatsPerVotingResult.get(entry.getKey()) - 0.5))
.orElse(initialElectionNumber); .orElse(initialElectionNumber);
return electionNumber; return electionNumber;
} }
@@ -230,12 +309,10 @@ public class CalculationService {
@NonNull Map<VotingResult, Double> seatsPerVotingResult, @NonNull Map<VotingResult, Double> seatsPerVotingResult,
Map<VotingResult, Integer> assignedSeatsPerVotingResult) { Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
double electionNumber; double electionNumber;
electionNumber = seatsPerVotingResult.entrySet().stream() electionNumber = seatsPerVotingResult.keySet().stream()
.map(entry -> Map.entry(entry.getKey(), .map(votingResult -> votingResult.getTotalVotes()
entry.getValue() - assignedSeatsPerVotingResult.get(entry.getKey()))) / (assignedSeatsPerVotingResult.get(votingResult) + 0.5))
.max(Comparator.comparing(Entry<VotingResult, Double>::getValue)) .max(Comparator.comparing(Double::doubleValue))
.map(entry -> entry.getKey().getTotalVotes()
/ (assignedSeatsPerVotingResult.get(entry.getKey()) + 0.5))
.orElse(initialElectionNumber); .orElse(initialElectionNumber);
return electionNumber; return electionNumber;
} }
@@ -1,10 +1,15 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.mapper.v1.ConstituencyMapper;
import de.twomartens.wahlrecht.model.db.Constituency; import de.twomartens.wahlrecht.model.db.Constituency;
import de.twomartens.wahlrecht.model.internal.ConstituencyId;
import de.twomartens.wahlrecht.repository.ConstituencyRepository; import de.twomartens.wahlrecht.repository.ConstituencyRepository;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.mapstruct.factory.Mappers;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -13,21 +18,52 @@ import org.springframework.stereotype.Service;
public class ConstituencyService { public class ConstituencyService {
private final ConstituencyRepository repository; private final ConstituencyRepository repository;
private final ConstituencyMapper mapper = Mappers.getMapper(ConstituencyMapper.class);
private Map<ConstituencyId, Constituency> constituencies;
public Collection<Constituency> getConstituencies() { public Collection<Constituency> getConstituencies() {
return repository.findAll(); return repository.findAll();
} }
public Constituency storeConstituency(@NonNull Constituency constituency) { public Constituency getConstituency(ConstituencyId constituencyId) {
Optional<Constituency> foundOptional = repository return constituencies.get(constituencyId);
.findByElectionNameAndNumber(constituency.getElectionName(), constituency.getNumber()); }
if (foundOptional.isPresent()) { public de.twomartens.wahlrecht.model.internal.Constituency getConstituencyInternal(
Constituency found = foundOptional.get(); ConstituencyId constituencyId) {
found.setNumberOfSeats(constituency.getNumberOfSeats()); return mapper.mapToInternal(getConstituency(constituencyId));
found.setName(constituency.getName()); }
constituency = found;
public Constituency storeConstituency(@NonNull Constituency constituency) {
if (constituencies == null) {
fetchConstituencies();
} }
return repository.save(constituency);
ConstituencyId constituencyId = new ConstituencyId(constituency.getElectionName(),
constituency.getNumber());
Constituency existing = constituencies.get(constituencyId);
if (constituency.equals(existing)) {
return existing;
}
if (existing != null) {
existing.setNumberOfSeats(constituency.getNumberOfSeats());
existing.setName(constituency.getName());
constituency = existing;
}
Constituency stored = repository.save(constituency);
constituencies.put(constituencyId, stored);
return stored;
}
private void fetchConstituencies() {
constituencies = repository.findAll().stream()
.collect(Collectors.toMap(
constituency -> new ConstituencyId(constituency.getElectionName(),
constituency.getNumber()),
Function.identity()));
} }
} }
@@ -1,12 +1,16 @@
package de.twomartens.wahlrecht.service; package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.mapper.v1.ElectionMapper;
import de.twomartens.wahlrecht.model.db.Constituency; import de.twomartens.wahlrecht.model.db.Constituency;
import de.twomartens.wahlrecht.model.db.Election; import de.twomartens.wahlrecht.model.db.Election;
import de.twomartens.wahlrecht.repository.ElectionRepository; import de.twomartens.wahlrecht.repository.ElectionRepository;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.mapstruct.factory.Mappers;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -16,22 +20,43 @@ public class ElectionService {
private final ElectionRepository electionRepository; private final ElectionRepository electionRepository;
private final ConstituencyService constituencyService; private final ConstituencyService constituencyService;
private final ElectionMapper electionMapper = Mappers.getMapper(ElectionMapper.class);
private Map<String, Election> elections;
public Collection<Election> getElections() { public Collection<Election> getElections() {
return electionRepository.findAll(); return electionRepository.findAll();
} }
public Election getElection(String electionName) {
if (elections == null) {
fetchElections();
}
return elections.get(electionName);
}
public de.twomartens.wahlrecht.model.internal.Election getElectionInternal(String electionName) {
return electionMapper.mapToInternal(getElection(electionName));
}
public boolean storeElection(@NonNull Election election) { public boolean storeElection(@NonNull Election election) {
if (elections == null) {
fetchElections();
}
boolean createdNew = true; boolean createdNew = true;
String electionName = election.getName(); String electionName = election.getName();
Optional<Election> existingElectionOptional = electionRepository.findByName(electionName); Election existing = elections.get(electionName);
if (election.equals(existing)) {
return false;
}
Collection<Constituency> constituencies = new ArrayList<>(); Collection<Constituency> constituencies = new ArrayList<>();
election.getConstituencies() election.getConstituencies()
.forEach(constituency -> constituencies.add( .forEach(constituency -> constituencies.add(
constituencyService.storeConstituency(constituency))); constituencyService.storeConstituency(constituency)));
if (existingElectionOptional.isPresent()) { if (existing != null) {
Election existing = existingElectionOptional.get();
existing.setDay(election.getDay()); existing.setDay(election.getDay());
existing.setTotalNumberOfSeats(election.getTotalNumberOfSeats()); existing.setTotalNumberOfSeats(election.getTotalNumberOfSeats());
existing.setVotingThreshold(election.getVotingThreshold()); existing.setVotingThreshold(election.getVotingThreshold());
@@ -40,8 +65,14 @@ public class ElectionService {
} }
election.setConstituencies(constituencies); election.setConstituencies(constituencies);
electionRepository.save(election); Election stored = electionRepository.save(election);
elections.put(electionName, stored);
return createdNew; return createdNew;
} }
private void fetchElections() {
elections = electionRepository.findAll().stream()
.collect(Collectors.toMap(Election::getName, Function.identity()));
}
} }
@@ -8,7 +8,6 @@ import java.util.Collection;
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.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -35,13 +34,16 @@ public class PartyService {
public PartyInElection getPartyByElectionNameAndAbbreviation(String electionName, public PartyInElection getPartyByElectionNameAndAbbreviation(String electionName,
String abbreviation) { String abbreviation) {
Optional<PartyInElection> optionalParty = partyRepository.findByAbbreviationAndElectionName( if (parties == null) {
abbreviation, electionName); fetchParties();
if (optionalParty.isEmpty()) { }
PartyId partyId = new PartyId(electionName, abbreviation);
PartyInElection party = parties.get(partyId);
if (party == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"no party found for %s and %s".formatted(electionName, abbreviation)); "no party found for %s and %s".formatted(electionName, abbreviation));
} }
return optionalParty.get(); return party;
} }
public boolean storeParty(PartyInElection partyInElection) { public boolean storeParty(PartyInElection partyInElection) {
@@ -116,6 +116,9 @@ class CalculationServiceTest {
@Mock @Mock
private NominationService nominationService; private NominationService nominationService;
@Mock
private ElectionService electionService;
@BeforeAll @BeforeAll
static void setUp() { static void setUp() {
setUpCandidatesConstituency(); setUpCandidatesConstituency();
@@ -250,7 +253,7 @@ class CalculationServiceTest {
@BeforeEach @BeforeEach
void setService() { void setService() {
service = new CalculationService(nominationService); service = new CalculationService(nominationService, electionService);
} }
@Test @Test
@@ -316,7 +319,7 @@ class CalculationServiceTest {
FDP_BEZIRK_RESULT, 3, FDP_BEZIRK_RESULT, 3,
AFD_BEZIRK_RESULT, 3 AFD_BEZIRK_RESULT, 3
); );
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates = setUpConstituencyResults(); Map<String, Collection<ElectedCandidate>> electedCandidates = setUpConstituencyResults();
when(nominationService.getNominationInternal(SPD_BEZIRK.getId())).thenReturn(SPD_BEZIRK); when(nominationService.getNominationInternal(SPD_BEZIRK.getId())).thenReturn(SPD_BEZIRK);
when(nominationService.getNominationInternal(CDU_BEZIRK.getId())).thenReturn(CDU_BEZIRK); when(nominationService.getNominationInternal(CDU_BEZIRK.getId())).thenReturn(CDU_BEZIRK);
when(nominationService.getNominationInternal(FDP_BEZIRK.getId())).thenReturn(FDP_BEZIRK); when(nominationService.getNominationInternal(FDP_BEZIRK.getId())).thenReturn(FDP_BEZIRK);
@@ -397,8 +400,8 @@ class CalculationServiceTest {
} }
@NonNull @NonNull
private Map<VotingResult, Collection<ElectedCandidate>> setUpConstituencyResults() { private Map<String, Collection<ElectedCandidate>> setUpConstituencyResults() {
return Map.of(GRUENE_BEZIRK_RESULT, List.of( return Map.of(GRUENE_BEZIRK_RESULT.getNominationId().partyAbbreviation(), List.of(
new ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY), new ElectedCandidate(GRUENE_BEZIRK.getCandidate(1), Elected.CONSTITUENCY),
new ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY), new ElectedCandidate(GRUENE_BEZIRK.getCandidate(2), Elected.CONSTITUENCY),
new ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY), new ElectedCandidate(GRUENE_BEZIRK.getCandidate(3), Elected.CONSTITUENCY),