diff --git a/src/election.rs b/src/election.rs index 52d511c..febe215 100644 --- a/src/election.rs +++ b/src/election.rs @@ -21,11 +21,13 @@ use crate::numbers::Number; use crate::sharandom::SHARandom; use crate::stv::{QuotaMode, STVOptions}; -#[cfg(not(target_arch = "wasm32"))] -use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}}; - use itertools::Itertools; +#[cfg(not(target_arch = "wasm32"))] +use rkyv::{Archive, Deserialize, Serialize}; +#[cfg(not(target_arch = "wasm32"))] +use crate::numbers::{SerializedNumber, SerializedOptionNumber}; + use std::cmp::max; use std::collections::HashMap; @@ -43,6 +45,11 @@ pub struct Election { pub withdrawn_candidates: Vec, /// [Vec] of [Ballot]s cast in the election pub ballots: Vec>, + /// Total value of [Ballot]s cast in the election + /// + /// Used for [Election::realise_equal_rankings]. + #[cfg_attr(not(target_arch = "wasm32"), with(SerializedOptionNumber))] + pub total_votes: Option, /// Constraints on candidates pub constraints: Option, } @@ -70,6 +77,9 @@ impl Election { /// Convert ballots with equal rankings to strict-preference "minivoters" pub fn realise_equal_rankings(&mut self) { + // Record total_votes so loss by fraction can be calculated + self.total_votes = Some(self.ballots.iter().fold(N::new(), |acc, b| acc + &b.orig_value)); + let mut realised_ballots = Vec::new(); for ballot in self.ballots.iter() { let mut b = ballot.realise_equal_rankings(); @@ -405,7 +415,7 @@ impl<'a, N> Vote<'a, N> { #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Ballot { /// Original value/weight of the ballot - #[cfg_attr(not(target_arch = "wasm32"), with(SerializedNum))] + #[cfg_attr(not(target_arch = "wasm32"), with(SerializedNumber))] pub orig_value: N, /// Indexes of candidates preferenced at each level on the ballot pub preferences: Vec>, @@ -451,34 +461,6 @@ impl Ballot { } } -/// rkyv-serialized representation of [Number] -#[cfg(not(target_arch = "wasm32"))] -pub struct SerializedNum; - -#[cfg(not(target_arch = "wasm32"))] -impl ArchiveWith for SerializedNum { - type Archived = Archived; - type Resolver = Resolver; - - unsafe fn resolve_with(field: &N, pos: usize, resolver: Self::Resolver, out: *mut Self::Archived) { - field.to_string().resolve(pos, resolver, out); - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl SerializeWith for SerializedNum where String: Serialize { - fn serialize_with(field: &N, serializer: &mut S) -> Result { - return field.to_string().serialize(serializer); - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl DeserializeWith, N, D> for SerializedNum where Archived: Deserialize { - fn deserialize_with(field: &Archived, deserializer: &mut D) -> Result { - return Ok(N::parse(&field.deserialize(deserializer)?)); - } -} - /// State of a [Candidate] during a count #[allow(dead_code)] #[derive(PartialEq)] diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index 5fa61c6..1b45933 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -32,6 +32,9 @@ mod rational_num; use num_traits::{NumAssignRef, NumRef}; +#[cfg(not(target_arch = "wasm32"))] +use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}}; + use std::cmp::Ord; use std::fmt; use std::ops; @@ -76,6 +79,81 @@ where } } +/// rkyv-serialized representation of [Number] +#[cfg(not(target_arch = "wasm32"))] +pub struct SerializedNumber; + +/// rkyv-serialized representation of [Option] +#[cfg(not(target_arch = "wasm32"))] +pub struct SerializedOptionNumber; + +#[cfg(not(target_arch = "wasm32"))] +impl ArchiveWith for SerializedNumber { + type Archived = Archived; + type Resolver = Resolver; + + unsafe fn resolve_with(field: &N, pos: usize, resolver: Self::Resolver, out: *mut Self::Archived) { + field.to_string().resolve(pos, resolver, out); + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl ArchiveWith> for SerializedOptionNumber { + type Archived = Archived; + type Resolver = Resolver; + + unsafe fn resolve_with(field: &Option, pos: usize, resolver: Self::Resolver, out: *mut Self::Archived) { + match field { + Some(n) => { + n.to_string().resolve(pos, resolver, out); + } + None => { + String::new().resolve(pos, resolver, out); + } + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl SerializeWith for SerializedNumber where String: Serialize { + fn serialize_with(field: &N, serializer: &mut S) -> Result { + return field.to_string().serialize(serializer); + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl SerializeWith, S> for SerializedOptionNumber where String: Serialize { + fn serialize_with(field: &Option, serializer: &mut S) -> Result { + match field { + Some(n) => { + return n.to_string().serialize(serializer); + } + None => { + return String::new().serialize(serializer); + } + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl DeserializeWith, N, D> for SerializedNumber where Archived: Deserialize { + fn deserialize_with(field: &Archived, deserializer: &mut D) -> Result { + return Ok(N::parse(&field.deserialize(deserializer)?)); + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl DeserializeWith, Option, D> for SerializedOptionNumber where Archived: Deserialize { + fn deserialize_with(field: &Archived, deserializer: &mut D) -> Result, D::Error> { + let s = field.deserialize(deserializer)?; + if s.len() == 0 { + return Ok(None); + } else { + return Ok(Some(N::parse(&s))); + } + } +} + pub use self::fixed::Fixed; pub use self::gfixed::GuardedFixed; pub use self::native::NativeFloat64; diff --git a/src/parser/blt.rs b/src/parser/blt.rs index a58e3d2..8cb1308 100644 --- a/src/parser/blt.rs +++ b/src/parser/blt.rs @@ -372,6 +372,7 @@ impl> BLTParser { candidates: Vec::new(), withdrawn_candidates: Vec::new(), ballots: Vec::new(), + total_votes: None, constraints: None, }, } diff --git a/src/parser/csp.rs b/src/parser/csp.rs index 9761caa..e209d38 100644 --- a/src/parser/csp.rs +++ b/src/parser/csp.rs @@ -107,6 +107,7 @@ pub fn parse_reader(reader: R) -> Election { candidates: candidates, withdrawn_candidates: Vec::new(), ballots: ballots, + total_votes: None, constraints: None, }; } diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index 12d4dd6..d97e909 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -29,6 +29,8 @@ use std::ops; /// Distribute first preference votes according to the Gregory method pub fn distribute_first_preferences(state: &mut CountState, opts: &STVOptions) +where + for<'r> &'r N: ops::Sub<&'r N, Output=N> { let votes = state.election.ballots.iter().map(|b| Vote { ballot: b, @@ -66,6 +68,16 @@ pub fn distribute_first_preferences(state: &mut CountState, opts: state.exhausted.transfer(&result.exhausted.num_ballots); state.exhausted.ballot_transfers += result.exhausted.num_ballots; + // Calculate loss by fraction - if minivoters used + if let Some(orig_total) = &state.election.total_votes { + let mut total_votes = state.candidates.values().fold(N::new(), |acc, cc| acc + &cc.votes); + total_votes += &state.exhausted.votes; + let lbf = orig_total - &total_votes; + + state.loss_fraction.votes = lbf.clone(); + state.loss_fraction.transfers = lbf; + } + state.kind = None; state.title = "First preferences".to_string(); state.logger.log_literal("First preferences distributed.".to_string()); diff --git a/src/stv/meek.rs b/src/stv/meek.rs index fa5fa41..7b090d2 100644 --- a/src/stv/meek.rs +++ b/src/stv/meek.rs @@ -127,6 +127,16 @@ where } state.exhausted.transfers.assign(&state.exhausted.votes); + // Calculate loss by fraction - if minivoters used + if let Some(orig_total) = &state.election.total_votes { + let mut total_votes = state.candidates.values().fold(N::new(), |acc, cc| acc + &cc.votes); + total_votes += &state.exhausted.votes; + let lbf = orig_total - &total_votes; + + state.loss_fraction.votes = lbf.clone(); + state.loss_fraction.transfers = lbf; + } + state.kind = None; state.title = "First preferences".to_string(); state.logger.log_literal("First preferences distributed.".to_string());