Added ability to calculate election result

This commit is contained in:
Jim Martens 2023-07-13 23:23:54 +02:00
parent da22c792da
commit a8d4d47f70
15 changed files with 329 additions and 62 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,7 +1,6 @@
package de.twomartens.wahlrecht.mapper.v1;
import de.twomartens.wahlrecht.model.dto.v1.ElectedCandidate;
import de.twomartens.wahlrecht.model.dto.v1.NominationId;
import de.twomartens.wahlrecht.model.internal.ElectedResult;
import java.util.Collection;
import java.util.Map;
@ -11,6 +10,7 @@ import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.ReportingPolicy;
import org.springframework.lang.NonNull;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
@ -19,16 +19,21 @@ public interface ElectedResultMapper {
de.twomartens.wahlrecht.model.dto.v1.ElectedResult mapToExternal(ElectedResult result);
Map<NominationId, Collection<ElectedCandidate>> mapToExternal(
Map<de.twomartens.wahlrecht.model.internal.NominationId,
Collection<de.twomartens.wahlrecht.model.internal.ElectedCandidate>> value);
default Map<String, Collection<ElectedCandidate>> mapToExternal(
@NonNull Map<de.twomartens.wahlrecht.model.internal.VotingResult,
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<de.twomartens.wahlrecht.model.internal.ElectedCandidate> value);
default Map<de.twomartens.wahlrecht.model.internal.NominationId,
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) {
return value.entrySet().stream()
.collect(Collectors.toMap(entry -> entry.getKey().getNominationId(), Entry::getValue));

View File

@ -9,13 +9,17 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
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 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.db.VotingResult> results);
@ -36,6 +40,10 @@ public interface ElectionResultMapper {
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<de.twomartens.wahlrecht.model.dto.v1.VotingResult> results);

View File

@ -6,13 +6,14 @@ import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.ReportingPolicy;
import org.springframework.lang.NonNull;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
unmappedTargetPolicy = ReportingPolicy.IGNORE)
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(
result.getNominationId().electionName(),
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(
new NominationId(result.electionName(), result.partyAbbreviation(),
result.nominationName()),

View File

@ -1,12 +1,4 @@
package de.twomartens.wahlrecht.model.dto.v1;
public record ElectedCandidate(Candidate candidate, Elected elected) {
public String name() {
return candidate.name();
}
public String profession() {
return candidate.profession();
}
}

View File

@ -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) {
}

View File

@ -1,13 +1,12 @@
package de.twomartens.wahlrecht.model.dto.v1;
import de.twomartens.wahlrecht.model.internal.NominationId;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import lombok.Builder;
@Builder
public record ElectedResult(Map<NominationId, Collection<ElectedCandidate>> electedCandidates,
public record ElectedResult(Map<String, Collection<ElectedCandidate>> electedCandidates,
LinkedList<Double> usedElectionNumbers) {
}

View File

@ -0,0 +1,5 @@
package de.twomartens.wahlrecht.model.internal;
public record ConstituencyId(String electionName, Integer number) {
}

View File

@ -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) {
}

View File

@ -4,8 +4,10 @@ import de.twomartens.wahlrecht.model.internal.Candidate;
import de.twomartens.wahlrecht.model.internal.Constituency;
import de.twomartens.wahlrecht.model.internal.Elected;
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.Election;
import de.twomartens.wahlrecht.model.internal.ElectionResult;
import de.twomartens.wahlrecht.model.internal.Nomination;
import de.twomartens.wahlrecht.model.internal.NominationId;
import de.twomartens.wahlrecht.model.internal.SeatResult;
@ -22,22 +24,69 @@ import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
@RequiredArgsConstructor
@Service
@Slf4j
public class CalculationService {
private final NominationService nominationService;
private final ElectionService electionService;
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,
@NonNull Collection<VotingResult> votingResults) {
electionNumberHistory.clear();
log.info("Calculate constituency {}", constituency.name());
int totalVotes = votingResults.stream()
.map(VotingResult::getTotalVotes)
.reduce(0, Integer::sum);
@ -49,12 +98,13 @@ public class CalculationService {
return ElectedResult.builder()
.electedCandidates(findElectedCandidates(assignedSeatsPerResult, Collections.emptyMap()))
.usedElectionNumbers(electionNumberHistory)
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build();
}
public SeatResult calculateOverallSeatDistribution(@NonNull Election election,
@NonNull Collection<VotingResult> votingResults) {
log.info("Calculate overall seat distribution for election {}", election.name());
electionNumberHistory.clear();
int totalVotes = votingResults.stream()
.map(VotingResult::getTotalVotes)
@ -64,8 +114,12 @@ public class CalculationService {
int totalIgnoredVotes = 0;
for (VotingResult votingResult : votingResults) {
if (passesVotingThreshold(election, totalVotes, votingResult)) {
log.info("Party passes voting threshold: {}",
votingResult.getNominationId().partyAbbreviation());
validVotingResults.add(votingResult);
} else {
log.info("Party fails voting threshold: {}",
votingResult.getNominationId().partyAbbreviation());
totalIgnoredVotes += votingResult.getTotalVotes();
}
}
@ -79,18 +133,25 @@ public class CalculationService {
return SeatResult.builder()
.seatsPerResult(assignedSeatsPerResult)
.usedElectionNumbers(electionNumberHistory)
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.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(
Map<VotingResult, Integer> seatsPerNomination,
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates) {
Map<String, Collection<ElectedCandidate>> electedCandidates) {
log.info("Calculate overall elected candidates");
electionNumberHistory.clear();
return ElectedResult.builder()
.electedCandidates(findElectedCandidates(seatsPerNomination, electedCandidates))
.usedElectionNumbers(electionNumberHistory)
.usedElectionNumbers(new LinkedList<>(electionNumberHistory))
.build();
}
@ -100,10 +161,16 @@ public class CalculationService {
<= 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
private Map<VotingResult, Collection<ElectedCandidate>> findElectedCandidates(
@NonNull Map<VotingResult, Integer> assignedSeatsPerNomination,
Map<VotingResult, Collection<ElectedCandidate>> alreadyElectedCandidates) {
Map<String, Collection<ElectedCandidate>> alreadyElectedCandidates) {
Map<VotingResult, Collection<ElectedCandidate>> electedCandidates = new HashMap<>();
for (Entry<VotingResult, Integer> entry : assignedSeatsPerNomination.entrySet()) {
@ -111,7 +178,9 @@ public class CalculationService {
Collection<ElectedCandidate> candidates = findCandidates(
entry.getKey(),
entry.getValue(),
alreadyElectedCandidates.getOrDefault(entry.getKey(), Collections.emptyList())
alreadyElectedCandidates.getOrDefault(
entry.getKey().getNominationId().partyAbbreviation(),
Collections.emptyList())
);
electedCandidates.put(entry.getKey(), candidates);
}
@ -120,9 +189,18 @@ public class CalculationService {
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
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()
.entrySet().stream()
.sorted(Comparator.comparing(Entry<Integer, Integer>::getValue).reversed())
@ -184,6 +262,7 @@ public class CalculationService {
double electionNumber = initialElectionNumber;
long assignedSeats;
Map<VotingResult, Integer> assignedSeatsPerVotingResult;
log.debug("Calculate assigned seats with initial election number {}", electionNumber);
do {
electionNumberHistory.add(electionNumber);
@ -202,10 +281,12 @@ public class CalculationService {
// election number was too big, decrease
electionNumber = calculateLowerElectionNumber(initialElectionNumber, seatsPerVotingResult,
assignedSeatsPerVotingResult);
log.debug("Calculated lower election number {}", electionNumber);
} else if (assignedSeats > numberOfSeats) {
// election number was too small, increase
electionNumber = calculateHigherElectionNumber(initialElectionNumber, seatsPerVotingResult,
assignedSeatsPerVotingResult);
log.debug("Calculated higher election number {}", electionNumber);
}
} while (assignedSeats != numberOfSeats);
@ -216,12 +297,10 @@ public class CalculationService {
@NonNull Map<VotingResult, Double> seatsPerVotingResult,
Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
double electionNumber;
electionNumber = seatsPerVotingResult.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(),
entry.getValue() - assignedSeatsPerVotingResult.get(entry.getKey())))
.min(Comparator.comparing(Entry<VotingResult, Double>::getValue))
.map(entry -> entry.getKey().getTotalVotes()
/ (assignedSeatsPerVotingResult.get(entry.getKey()) - 0.5))
electionNumber = seatsPerVotingResult.keySet().stream()
.map(votingResult -> votingResult.getTotalVotes()
/ (assignedSeatsPerVotingResult.get(votingResult) - 0.5))
.min(Comparator.comparing(Double::doubleValue))
.orElse(initialElectionNumber);
return electionNumber;
}
@ -230,12 +309,10 @@ public class CalculationService {
@NonNull Map<VotingResult, Double> seatsPerVotingResult,
Map<VotingResult, Integer> assignedSeatsPerVotingResult) {
double electionNumber;
electionNumber = seatsPerVotingResult.entrySet().stream()
.map(entry -> Map.entry(entry.getKey(),
entry.getValue() - assignedSeatsPerVotingResult.get(entry.getKey())))
.max(Comparator.comparing(Entry<VotingResult, Double>::getValue))
.map(entry -> entry.getKey().getTotalVotes()
/ (assignedSeatsPerVotingResult.get(entry.getKey()) + 0.5))
electionNumber = seatsPerVotingResult.keySet().stream()
.map(votingResult -> votingResult.getTotalVotes()
/ (assignedSeatsPerVotingResult.get(votingResult) + 0.5))
.max(Comparator.comparing(Double::doubleValue))
.orElse(initialElectionNumber);
return electionNumber;
}

View File

@ -1,10 +1,15 @@
package de.twomartens.wahlrecht.service;
import de.twomartens.wahlrecht.mapper.v1.ConstituencyMapper;
import de.twomartens.wahlrecht.model.db.Constituency;
import de.twomartens.wahlrecht.model.internal.ConstituencyId;
import de.twomartens.wahlrecht.repository.ConstituencyRepository;
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 org.mapstruct.factory.Mappers;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@ -13,21 +18,52 @@ import org.springframework.stereotype.Service;
public class ConstituencyService {
private final ConstituencyRepository repository;
private final ConstituencyMapper mapper = Mappers.getMapper(ConstituencyMapper.class);
private Map<ConstituencyId, Constituency> constituencies;
public Collection<Constituency> getConstituencies() {
return repository.findAll();
}
public Constituency storeConstituency(@NonNull Constituency constituency) {
Optional<Constituency> foundOptional = repository
.findByElectionNameAndNumber(constituency.getElectionName(), constituency.getNumber());
public Constituency getConstituency(ConstituencyId constituencyId) {
return constituencies.get(constituencyId);
}
if (foundOptional.isPresent()) {
Constituency found = foundOptional.get();
found.setNumberOfSeats(constituency.getNumberOfSeats());
found.setName(constituency.getName());
constituency = found;
public de.twomartens.wahlrecht.model.internal.Constituency getConstituencyInternal(
ConstituencyId constituencyId) {
return mapper.mapToInternal(getConstituency(constituencyId));
}
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()));
}
}

View File

@ -1,12 +1,16 @@
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.Election;
import de.twomartens.wahlrecht.repository.ElectionRepository;
import java.util.ArrayList;
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 org.mapstruct.factory.Mappers;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@ -16,22 +20,43 @@ public class ElectionService {
private final ElectionRepository electionRepository;
private final ConstituencyService constituencyService;
private final ElectionMapper electionMapper = Mappers.getMapper(ElectionMapper.class);
private Map<String, Election> elections;
public Collection<Election> getElections() {
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) {
if (elections == null) {
fetchElections();
}
boolean createdNew = true;
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<>();
election.getConstituencies()
.forEach(constituency -> constituencies.add(
constituencyService.storeConstituency(constituency)));
if (existingElectionOptional.isPresent()) {
Election existing = existingElectionOptional.get();
if (existing != null) {
existing.setDay(election.getDay());
existing.setTotalNumberOfSeats(election.getTotalNumberOfSeats());
existing.setVotingThreshold(election.getVotingThreshold());
@ -40,8 +65,14 @@ public class ElectionService {
}
election.setConstituencies(constituencies);
electionRepository.save(election);
Election stored = electionRepository.save(election);
elections.put(electionName, stored);
return createdNew;
}
private void fetchElections() {
elections = electionRepository.findAll().stream()
.collect(Collectors.toMap(Election::getName, Function.identity()));
}
}

View File

@ -8,7 +8,6 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
@ -35,13 +34,16 @@ public class PartyService {
public PartyInElection getPartyByElectionNameAndAbbreviation(String electionName,
String abbreviation) {
Optional<PartyInElection> optionalParty = partyRepository.findByAbbreviationAndElectionName(
abbreviation, electionName);
if (optionalParty.isEmpty()) {
if (parties == null) {
fetchParties();
}
PartyId partyId = new PartyId(electionName, abbreviation);
PartyInElection party = parties.get(partyId);
if (party == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"no party found for %s and %s".formatted(electionName, abbreviation));
}
return optionalParty.get();
return party;
}
public boolean storeParty(PartyInElection partyInElection) {

View File

@ -116,6 +116,9 @@ class CalculationServiceTest {
@Mock
private NominationService nominationService;
@Mock
private ElectionService electionService;
@BeforeAll
static void setUp() {
setUpCandidatesConstituency();
@ -250,7 +253,7 @@ class CalculationServiceTest {
@BeforeEach
void setService() {
service = new CalculationService(nominationService);
service = new CalculationService(nominationService, electionService);
}
@Test
@ -316,7 +319,7 @@ class CalculationServiceTest {
FDP_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(CDU_BEZIRK.getId())).thenReturn(CDU_BEZIRK);
when(nominationService.getNominationInternal(FDP_BEZIRK.getId())).thenReturn(FDP_BEZIRK);
@ -397,8 +400,8 @@ class CalculationServiceTest {
}
@NonNull
private Map<VotingResult, Collection<ElectedCandidate>> setUpConstituencyResults() {
return Map.of(GRUENE_BEZIRK_RESULT, List.of(
private Map<String, Collection<ElectedCandidate>> setUpConstituencyResults() {
return Map.of(GRUENE_BEZIRK_RESULT.getNominationId().partyAbbreviation(), 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),