From 5a3022d6d3551b2b80b6d0458e389e84f5165488 Mon Sep 17 00:00:00 2001 From: Ellen Date: Thu, 23 Jan 2025 10:23:38 +0100 Subject: [PATCH] Apportionment calculation for <19 seats (#835) --- backend/src/apportionment/fraction.rs | 73 +++-- backend/src/apportionment/mod.rs | 412 +++++++++++++++++--------- 2 files changed, 332 insertions(+), 153 deletions(-) diff --git a/backend/src/apportionment/fraction.rs b/backend/src/apportionment/fraction.rs index 55518afd8..8e7466800 100644 --- a/backend/src/apportionment/fraction.rs +++ b/backend/src/apportionment/fraction.rs @@ -1,10 +1,10 @@ use crate::data_entry::Count; use std::{ - fmt, - fmt::{Debug, Display, Formatter}, - ops::{Div, Mul}, + fmt::{Debug, Display, Formatter, Result}, + ops::{Add, Div, Mul, Sub}, }; +#[derive(Clone, Copy)] pub struct Fraction { numerator: u64, denominator: u64, @@ -32,13 +32,24 @@ impl Fraction { } } -impl Div for Fraction { +impl Add for Fraction { type Output = Self; - fn div(self, other: Self) -> Self { + fn add(self, other: Self) -> Self { Self { - numerator: self.numerator * other.denominator, - denominator: self.denominator * other.numerator, + numerator: self.numerator * other.denominator + other.numerator * self.denominator, + denominator: self.denominator * other.denominator, + } + } +} + +impl Sub for Fraction { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Self { + numerator: self.numerator * other.denominator - other.numerator * self.denominator, + denominator: self.denominator * other.denominator, } } } @@ -54,6 +65,17 @@ impl Mul for Fraction { } } +impl Div for Fraction { + type Output = Self; + + fn div(self, other: Self) -> Self { + Self { + numerator: self.numerator * other.denominator, + denominator: self.denominator * other.numerator, + } + } +} + impl PartialOrd for Fraction { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -78,7 +100,7 @@ impl PartialEq for Fraction { impl Eq for Fraction {} impl Display for Fraction { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { if self.denominator == 0 { return write!(f, "NaN"); } @@ -97,7 +119,7 @@ impl Display for Fraction { } impl Debug for Fraction { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "{}", self) } } @@ -141,21 +163,21 @@ mod tests { } #[test] - fn test_div_whole_number_larger_than_zero() { - let fraction = Fraction::new(11, 5); - let other_fraction = Fraction::new(1, 2); - let divided = fraction / other_fraction; - assert_eq!(divided, Fraction::new(22, 5)); - assert_eq!(divided.to_string(), "4 2/5") + fn test_add() { + let fraction = Fraction::new(1, 3); + let other_fraction = Fraction::new(2, 4); + let added = fraction + other_fraction; + assert_eq!(added, Fraction::new(10, 12)); + assert_eq!(added.to_string(), "10/12") } #[test] - fn test_div_whole_number_smaller_than_zero() { - let fraction = Fraction::new(1, 5); - let other_fraction = Fraction::new(2, 9); - let divided = fraction / other_fraction; - assert_eq!(divided, Fraction::new(9, 10)); - assert_eq!(divided.to_string(), "9/10") + fn test_sub() { + let fraction = Fraction::new(2, 5); + let other_fraction = Fraction::new(1, 4); + let subtracted = fraction - other_fraction; + assert_eq!(subtracted, Fraction::new(3, 20)); + assert_eq!(subtracted.to_string(), "3/20") } #[test] @@ -167,6 +189,15 @@ mod tests { assert_eq!(multiplied.to_string(), "2/45") } + #[test] + fn test_div() { + let fraction = Fraction::new(11, 5); + let other_fraction = Fraction::new(1, 2); + let divided = fraction / other_fraction; + assert_eq!(divided, Fraction::new(22, 5)); + assert_eq!(divided.to_string(), "4 2/5") + } + #[test] fn test_eq() { assert_eq!(Fraction::new(1, 4), Fraction::new(2, 8)); diff --git a/backend/src/apportionment/mod.rs b/backend/src/apportionment/mod.rs index d7950c42c..595e675fb 100644 --- a/backend/src/apportionment/mod.rs +++ b/backend/src/apportionment/mod.rs @@ -1,90 +1,260 @@ -use crate::{apportionment::fraction::Fraction, summary::ElectionSummary}; +use crate::{ + apportionment::fraction::Fraction, data_entry::PoliticalGroupVotes, summary::ElectionSummary, +}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use tracing::{debug, info}; mod fraction; -/// Apportionment - work in progress!! +fn get_number_of_whole_seats_per_pg( + pg_votes: &[PoliticalGroupVotes], + quota: &Fraction, +) -> BTreeMap { + // calculate number of whole seats for each party + pg_votes + .iter() + .fold(BTreeMap::new(), |mut whole_seats, pg| { + let pg_total = Fraction::from_count(pg.total); + let pg_seats = pg_total.divide_and_return_whole_number(quota); + whole_seats.insert(pg.number, pg_seats); + whole_seats + }) +} + +fn get_pg_number_with_largest_average( + pg_votes: &[PoliticalGroupVotes], + whole_seats: &BTreeMap, + rest_seats: &BTreeMap, + remaining_seats: u64, + unique_pgs: Option<&Vec>, +) -> Result { + let averages = pg_votes.iter().fold(BTreeMap::new(), |mut averages, pg| { + let pg_total = Fraction::from_count(pg.total); + let pg_seats_new = whole_seats + .get(&pg.number) + .expect("Political group should have number of whole seats") + + rest_seats.get(&pg.number).unwrap_or(&0) + + 1; + let pg_avg_votes = pg_total / Fraction::from_u64(pg_seats_new); + if unique_pgs.is_none() || unique_pgs.unwrap_or(&vec![]).contains(&pg.number) { + averages.insert(pg.number, pg_avg_votes); + } + averages + }); + + debug!("Averages: {:?}", averages); + + let (&pg_number, &max) = averages + .iter() + .max_by(|(_, a), (_, b)| a.cmp(b)) + .expect("Maximum average should be found"); + debug!("Max: {} (pg_number {})", max, pg_number); + + // if maximum occurs more than once, exit with error if less remaining seats are available than max count + let max_count = averages.iter().filter(|(_, &n)| n == max).count() as u64; + if max_count > remaining_seats { + // TODO: #788 if multiple parties have the same max and not enough remaining seats are available, use drawing of lots + debug!( + "Max count: {} is higher than remaining seats: {}", + max_count, remaining_seats + ); + info!("Drawing of lots is needed but not yet implemented!"); + return Err(ApportionmentError::DrawingOfLotsNotImplemented); + } + Ok(pg_number) +} + +fn get_surplus_per_pg_where_total_votes_meets_the_threshold( + pg_votes: &[PoliticalGroupVotes], + whole_seats: &BTreeMap, + quota: &Fraction, +) -> BTreeMap { + // get parties that have at least 3/4 (0.75) of the quota in total votes, + // and for each party calculate the amount of surplus votes, + // i.e. the number of total votes minus the quota times the number of whole seats + let threshold = Fraction::new(3, 4) * *quota; + debug!("Threshold: {}", threshold); + + pg_votes.iter().fold(BTreeMap::new(), |mut surpluses, pg| { + let pg_total = Fraction::from_count(pg.total); + if pg_total >= threshold { + let pg_whole_seats = Fraction::from_u64( + *whole_seats + .get(&pg.number) + .expect("Political group should have number of whole seats"), + ); + let surplus = pg_total - (*quota * pg_whole_seats); + if surplus > Fraction::new(0, 1) { + surpluses.insert(pg.number, surplus); + } + } + surpluses + }) +} + +fn get_pg_number_with_largest_surplus( + surpluses: &BTreeMap, + remaining_seats: u64, +) -> Result { + debug!("Surpluses: {:?}", surpluses); + + let (&pg_number, &max) = surpluses + .iter() + .max_by(|(_, a), (_, b)| a.cmp(b)) + .expect("Maximum surplus should be found"); + debug!("Max: {} (pg_number {})", max, pg_number); + + // if maximum occurs more than once, exit with error if less remaining seats are available than max count + let max_count = surpluses.iter().filter(|(_, &n)| n == max).count() as u64; + if max_count > remaining_seats { + // TODO: #788 if multiple parties have the same max and not enough remaining seats are available, use drawing of lots + debug!( + "Max count: {} is higher than remaining seats: {}", + max_count, remaining_seats + ); + info!("Drawing of lots is needed but not yet implemented!"); + return Err(ApportionmentError::DrawingOfLotsNotImplemented); + } + Ok(pg_number) +} + +fn allocate_remaining_seats( + pg_votes: &[PoliticalGroupVotes], + whole_seats: &BTreeMap, + quota: &Fraction, + seats: u64, + mut remaining_seats: u64, +) -> Result, ApportionmentError> { + let mut rest_seats = BTreeMap::::new(); + + if seats >= 19 { + info!("Remaining seats calculation for 19 or more seats."); + // using largest averages system ("stelsel grootste gemiddelden") + while remaining_seats > 0 { + info!("======================================================"); + debug!("Remaining seats: {}", remaining_seats); + // assign remaining seat to the party with the largest average + let pg_number = get_pg_number_with_largest_average( + pg_votes, + whole_seats, + &rest_seats, + remaining_seats, + None, + )?; + *rest_seats.entry(pg_number).or_insert(0) += 1; + remaining_seats -= 1; + info!( + "Remaining seat assigned using largest averages system to pg_number: {}", + pg_number + ); + } + } else { + info!("Remaining seats calculation for less than 19 seats."); + // using largest surpluses system ("stelsel grootste overschotten") + let mut surpluses = + get_surplus_per_pg_where_total_votes_meets_the_threshold(pg_votes, whole_seats, quota); + let mut unique_pgs = pg_votes.iter().fold(Vec::new(), |mut unique_pgs, pg| { + unique_pgs.push(pg.number); + unique_pgs + }); + while remaining_seats > 0 { + info!("======================================================"); + debug!("Remaining seats: {}", remaining_seats); + if !surpluses.is_empty() { + // assign remaining seat to the party with the largest surplus and + // remove that party and surplus from the list + let pg_number = get_pg_number_with_largest_surplus(&surpluses, remaining_seats)?; + *rest_seats.entry(pg_number).or_insert(0) += 1; + surpluses.remove(&pg_number); + remaining_seats -= 1; + info!( + "Remaining seat assigned using largest surpluses system to pg_number: {}", + pg_number + ); + } else { + // once there are no parties with surpluses left and more remaining seats exist, + // assign remaining seat to the unique political group with the largest average + // using unique largest averages system ("stelsel grootste gemiddelden") + // if there are still remaining seats after assigning each political group one, + // assign remaining seat to the political group with the largest average + // using largest averages system ("stelsel grootste gemiddelden") + let pg_number = get_pg_number_with_largest_average( + pg_votes, + whole_seats, + &rest_seats, + remaining_seats, + if !unique_pgs.is_empty() { + Some(&unique_pgs) + } else { + None + }, + )?; + *rest_seats.entry(pg_number).or_insert(0) += 1; + remaining_seats -= 1; + if !unique_pgs.is_empty() { + unique_pgs.retain(|&pg_num| pg_num != pg_number); + } + info!( + "Remaining seat assigned using greatest averages system to pg_number: {}", + pg_number + ); + } + } + } + Ok(rest_seats) +} + +/// Apportionment pub fn seat_allocation( seats: u64, totals: &ElectionSummary, ) -> Result, ApportionmentError> { - if seats < 19 { - return Err(ApportionmentError::SeatAllocationLessThan19SeatsNotImplemented); - } - println!("Seat allocation for 19 or more seats."); - println!("Totals {:#?}", totals); + info!("Seat allocation"); + debug!("Totals {:#?}", totals); + info!("Seats: {}", seats); // calculate quota (kiesdeler) as a proper fraction let total_votes = Fraction::from_count(totals.votes_counts.votes_candidates_count); let seats_fraction = Fraction::from_u64(seats); let quota = total_votes / seats_fraction; - println!("Seats: {}", seats); - println!("Quota: {}", quota); + info!("Quota: {}", quota); // TODO: #787 check for lijstuitputting (allocated seats cannot be more than total candidates) - // calculate number of whole seats for each party - let mut whole_seats = vec![]; - for pg in &totals.political_group_votes { - let pg_votes = Fraction::from_count(pg.total); - let pg_seats = pg_votes.divide_and_return_whole_number("a); - whole_seats.push(pg_seats); - } - - let whole_seats_count = whole_seats.iter().sum::(); - println!( + let whole_seats = get_number_of_whole_seats_per_pg(&totals.political_group_votes, "a); + let whole_seats_count = whole_seats.values().sum::(); + info!( "Whole seats: {:?} (total: {})", whole_seats, whole_seats_count ); - let mut remaining_seats = seats - whole_seats_count; - let mut rest_seats = vec![0; totals.political_group_votes.len()]; - // let mut idx_last_remaining_seat: Option = None; + let remaining_seats = seats - whole_seats_count; + let mut rest_seats = BTreeMap::::new(); - // allocate remaining seats (restzetels) - // using greatest average ("stelsel grootste gemiddelden") - while remaining_seats > 0 { - println!("==========================="); - println!("Remaining seats: {}", remaining_seats); - let mut avgs = vec![]; - for (idx, pg) in totals.political_group_votes.iter().enumerate() { - let pg_votes = Fraction::from_count(pg.total); - let pg_seats_new = whole_seats[idx] + rest_seats[idx] + 1; - let pg_avg_votes = pg_votes / Fraction::from_u64(pg_seats_new); - avgs.push(pg_avg_votes); - } - println!("Avgs: {:?}", avgs); - - let (idx, max) = avgs - .iter() - .enumerate() - .max_by(|(_, a), (_, b)| a.cmp(b)) - .expect("Maximum average should be found"); - println!("Max: {} (idx {})", max, idx); - - // if maximum occurs more than once, exit with error if less remaining seats are available than max count - let max_count = avgs.iter().filter(|&a| a == max).count() as u64; - if max_count > remaining_seats { - // TODO: #788 if multiple parties have the same max and not enough remaining seats are available, use drawing of lots - return Err(ApportionmentError::DrawingOfLotsNotImplemented); - } + info!("======================================================"); - rest_seats[idx] += 1; - remaining_seats -= 1; - // idx_last_remaining_seat = Some(idx); + // allocate remaining seats (restzetels) + if remaining_seats > 0 { + rest_seats = allocate_remaining_seats( + &totals.political_group_votes, + &whole_seats, + "a, + seats, + remaining_seats, + )?; } - // TODO: #785 Add check for absolute majority of votes vs seats and adjust last remaining seat assigned accordingly + // TODO: #785 Add check for absolute majority of votes vs seats and adjust last remaining seat allocated accordingly - println!("==========================="); - println!("Whole seats: {:?}", whole_seats); - println!("Remaining seats: {:?}", rest_seats); + info!("======================================================"); + info!("Whole seats: {:?}", whole_seats); + info!("Remaining seats: {:?}", rest_seats); let total_seats = whole_seats .iter() - .zip(rest_seats.iter()) - .map(|(a, b)| a + b) + .map(|(pg_number, seats)| seats + rest_seats.get(pg_number).unwrap_or(&0)) .collect::>(); - println!("Total seats: {:?}", total_seats); + info!("Total seats: {:?}", total_seats); Ok(total_seats) } @@ -92,7 +262,6 @@ pub fn seat_allocation( // TODO: integrate this with the application-wide error.rs once the apportionment functionality is finished #[derive(Serialize, Deserialize, PartialEq, Debug)] pub enum ApportionmentError { - SeatAllocationLessThan19SeatsNotImplemented, DrawingOfLotsNotImplemented, } @@ -100,101 +269,80 @@ pub enum ApportionmentError { mod tests { use crate::{ apportionment::{seat_allocation, ApportionmentError}, - data_entry::{PoliticalGroupVotes, VotersCounts, VotesCounts}, + data_entry::{Count, PoliticalGroupVotes, VotersCounts, VotesCounts}, summary::{ElectionSummary, SummaryDifferencesCounts}, }; use test_log::test; - #[test] - fn test_seat_allocation_less_than_19_seats_with_remaining_seats() { - let totals = ElectionSummary { + fn get_election_summary(pg_votes: Vec) -> ElectionSummary { + let total_votes = pg_votes.iter().sum(); + let mut political_group_votes: Vec = vec![]; + for (index, votes) in pg_votes.iter().enumerate() { + political_group_votes.push(PoliticalGroupVotes::from_test_data_auto( + (index + 1) as u8, + *votes, + &[], + )) + } + ElectionSummary { voters_counts: VotersCounts { - poll_card_count: 1200, + poll_card_count: total_votes, proxy_certificate_count: 0, voter_card_count: 0, - total_admitted_voters_count: 1200, + total_admitted_voters_count: total_votes, }, votes_counts: VotesCounts { - votes_candidates_count: 1200, + votes_candidates_count: total_votes, blank_votes_count: 0, invalid_votes_count: 0, - total_votes_cast_count: 1200, + total_votes_cast_count: total_votes, }, differences_counts: SummaryDifferencesCounts::zero(), recounted_polling_stations: vec![], - political_group_votes: vec![ - PoliticalGroupVotes::from_test_data_auto(1, 600, &[]), - PoliticalGroupVotes::from_test_data_auto(2, 302, &[]), - PoliticalGroupVotes::from_test_data_auto(3, 98, &[]), - PoliticalGroupVotes::from_test_data_auto(4, 99, &[]), - PoliticalGroupVotes::from_test_data_auto(5, 101, &[]), - ], - }; - - let result = seat_allocation(17, &totals); - assert_eq!( - result, - Err(ApportionmentError::SeatAllocationLessThan19SeatsNotImplemented) - ); + political_group_votes, + } } #[test] - fn test_seat_allocation_19_or_more_seats_with_remaining_seats() { - let totals = ElectionSummary { - voters_counts: VotersCounts { - poll_card_count: 1200, - proxy_certificate_count: 0, - voter_card_count: 0, - total_admitted_voters_count: 1200, - }, - votes_counts: VotesCounts { - votes_candidates_count: 1200, - blank_votes_count: 0, - invalid_votes_count: 0, - total_votes_cast_count: 1200, - }, - differences_counts: SummaryDifferencesCounts::zero(), - recounted_polling_stations: vec![], - political_group_votes: vec![ - PoliticalGroupVotes::from_test_data_auto(1, 600, &[]), - PoliticalGroupVotes::from_test_data_auto(2, 302, &[]), - PoliticalGroupVotes::from_test_data_auto(3, 98, &[]), - PoliticalGroupVotes::from_test_data_auto(4, 99, &[]), - PoliticalGroupVotes::from_test_data_auto(5, 101, &[]), - ], - }; + fn test_seat_allocation_less_than_19_seats_with_remaining_seats_assigned_with_surplus_system() { + let totals = get_election_summary(vec![540, 160, 160, 80, 80, 80, 60, 40]); + let result = seat_allocation(15, &totals); + assert_eq!(result, Ok(vec![7, 2, 2, 1, 1, 1, 1, 0])); + } + #[test] + fn test_seat_allocation_less_than_19_seats_with_remaining_seats_assigned_with_surplus_and_averages_system( + ) { + let totals = get_election_summary(vec![540, 160, 160, 80, 80, 80, 55, 45]); + let result = seat_allocation(15, &totals); + assert_eq!(result, Ok(vec![8, 2, 2, 1, 1, 1, 0, 0])); + } + + #[test] + fn test_seat_allocation_less_than_19_seats_with_remaining_seats_assigned_with_surplus_and_averages_system_no_surpluses( + ) { + let totals = get_election_summary(vec![560, 160, 160, 80, 80, 80, 40, 40]); + let result = seat_allocation(15, &totals); + assert_eq!(result, Ok(vec![8, 2, 2, 1, 1, 1, 0, 0])); + } + + #[test] + fn test_seat_allocation_less_than_19_seats_with_drawing_of_lots_error() { + let totals = get_election_summary(vec![500, 140, 140, 140, 140, 140]); + let result = seat_allocation(15, &totals); + assert_eq!(result, Err(ApportionmentError::DrawingOfLotsNotImplemented)); + } + + #[test] + fn test_seat_allocation_19_or_more_seats_with_remaining_seats() { + let totals = get_election_summary(vec![600, 302, 98, 99, 101]); let result = seat_allocation(23, &totals); assert_eq!(result, Ok(vec![12, 6, 1, 2, 2])); } #[test] - fn test_seat_allocation_with_drawing_of_lots_error() { - let totals = ElectionSummary { - voters_counts: VotersCounts { - poll_card_count: 1200, - proxy_certificate_count: 0, - voter_card_count: 0, - total_admitted_voters_count: 1200, - }, - votes_counts: VotesCounts { - votes_candidates_count: 1200, - blank_votes_count: 0, - invalid_votes_count: 0, - total_votes_cast_count: 1200, - }, - differences_counts: SummaryDifferencesCounts::zero(), - recounted_polling_stations: vec![], - political_group_votes: vec![ - PoliticalGroupVotes::from_test_data_auto(1, 500, &[]), - PoliticalGroupVotes::from_test_data_auto(2, 140, &[]), - PoliticalGroupVotes::from_test_data_auto(3, 140, &[]), - PoliticalGroupVotes::from_test_data_auto(4, 140, &[]), - PoliticalGroupVotes::from_test_data_auto(5, 140, &[]), - PoliticalGroupVotes::from_test_data_auto(6, 140, &[]), - ], - }; - + fn test_seat_allocation_19_or_more_seats_with_drawing_of_lots_error() { + let totals = get_election_summary(vec![500, 140, 140, 140, 140, 140]); let result = seat_allocation(23, &totals); assert_eq!(result, Err(ApportionmentError::DrawingOfLotsNotImplemented)); }