diff --git a/src/election.rs b/src/election.rs index 59c1d08..98fdf4c 100644 --- a/src/election.rs +++ b/src/election.rs @@ -97,6 +97,10 @@ pub struct CountState<'a, N> { pub num_elected: usize, pub num_excluded: usize, + + pub kind: Option<&'a str>, + pub title: String, + pub logs: Vec, } impl<'a, N: Number> CountState<'a, N> { @@ -109,6 +113,9 @@ impl<'a, N: Number> CountState<'a, N> { quota: N::new(), num_elected: 0, num_excluded: 0, + kind: None, + title: String::new(), + logs: Vec::new(), }; for candidate in election.candidates.iter() { @@ -127,12 +134,17 @@ impl<'a, N: Number> CountState<'a, N> { } } +#[allow(dead_code)] pub enum CountStateOrRef<'a, N> { - State(CountState<'a, N>), + State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints Ref(&'a CountState<'a, N>), } impl<'a, N> CountStateOrRef<'a, N> { + pub fn from(state: &'a CountState) -> Self { + return Self::Ref(state); + } + pub fn as_ref(&self) -> &CountState { match self { CountStateOrRef::State(state) => &state, @@ -142,8 +154,9 @@ impl<'a, N> CountStateOrRef<'a, N> { } pub struct StageResult<'a, N> { - pub title: &'a str, - pub logs: Vec<&'a str>, + pub kind: Option<&'a str>, + pub title: &'a String, + pub logs: &'a Vec, pub state: CountStateOrRef<'a, N>, } diff --git a/src/main.rs b/src/main.rs index ee1668e..f0b3c7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,10 @@ fn print_candidates<'a, N: 'a + Number, I: Iterator(stage_num: usize, result: &StageResult, cmd_opts: &STV) { // Print stage details - println!("{}. {}", stage_num, result.title); + match result.kind { + None => { println!("{}. {}", stage_num, result.title); } + Some(kind) => { println!("{}. {} {}", stage_num, kind, result.title); } + }; println!("{}", result.logs.join(" ")); let state = result.state.as_ref(); @@ -106,9 +109,17 @@ fn print_stage(stage_num: usize, result: &StageResult, cmd_opts: & println!(""); } +fn make_and_print_result(stage_num: usize, state: &CountState, cmd_opts: &STV) { + let result = StageResult { + kind: state.kind, + title: &state.title, + logs: &state.logs, + state: CountStateOrRef::from(&state), + }; + print_stage(stage_num, &result, &cmd_opts); +} + fn main() { - let should_clone_state = false; - // Read arguments let opts: Opts = Opts::parse(); let Command::STV(cmd_opts) = opts.command; @@ -127,17 +138,11 @@ fn main() { stv::elect_meeting_quota(&mut state); // Display - // TODO: Add logs during count - let result = StageResult { - title: "First preferences", - logs: vec!["First preferences distributed."], - state: if should_clone_state { CountStateOrRef::State(state.clone()) } else { CountStateOrRef::Ref(&state) }, - }; - print_stage(1, &result, &cmd_opts); - let mut stage_num = 1; + make_and_print_result(stage_num, &state, &cmd_opts); loop { + state.logs.clear(); state.step_all(); stage_num += 1; @@ -149,48 +154,28 @@ fn main() { // Continue exclusions if stv::continue_exclusion(&mut state) { stv::elect_meeting_quota(&mut state); - let result = StageResult { - title: "Exclusion", - logs: vec!["Continuing exclusion."], - state: if should_clone_state { CountStateOrRef::State(state.clone()) } else { CountStateOrRef::Ref(&state) }, - }; - print_stage(stage_num, &result, &cmd_opts); + make_and_print_result(stage_num, &state, &cmd_opts); continue; } // Distribute surpluses if stv::distribute_surpluses(&mut state) { stv::elect_meeting_quota(&mut state); - let result = StageResult { - title: "Surplus", - logs: vec!["Surplus distributed."], - state: if should_clone_state { CountStateOrRef::State(state.clone()) } else { CountStateOrRef::Ref(&state) }, - }; - print_stage(stage_num, &result, &cmd_opts); + make_and_print_result(stage_num, &state, &cmd_opts); continue; } // Attempt bulk election if stv::bulk_elect(&mut state) { stv::elect_meeting_quota(&mut state); - let result = StageResult { - title: "Bulk election", - logs: vec!["Bulk election."], - state: if should_clone_state { CountStateOrRef::State(state.clone()) } else { CountStateOrRef::Ref(&state) }, - }; - print_stage(stage_num, &result, &cmd_opts); + make_and_print_result(stage_num, &state, &cmd_opts); continue; } // Exclude lowest hopeful if stv::exclude_hopefuls(&mut state) { stv::elect_meeting_quota(&mut state); - let result = StageResult { - title: "Exclusion", - logs: vec!["Candidate excluded."], - state: if should_clone_state { CountStateOrRef::State(state.clone()) } else { CountStateOrRef::Ref(&state) }, - }; - print_stage(stage_num, &result, &cmd_opts); + make_and_print_result(stage_num, &state, &cmd_opts); continue; } diff --git a/src/stv/mod.rs b/src/stv/mod.rs index cf53a6a..b963f3d 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -109,11 +109,18 @@ pub fn distribute_first_preferences(state: &mut CountState) { let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); state.exhausted.transfer(&result.exhausted.num_votes); + + state.kind = None; + state.title = "First preferences".to_string(); + state.logs.push("First preferences distributed.".to_string()); } pub fn calculate_quota(state: &mut CountState) { + let mut log = String::new(); + // Calculate the total vote state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); + log.push_str(format!("{:.2} usable votes, so the quota is ", state.quota).as_str()); // TODO: Different quotas state.quota /= N::from(state.election.seats + 1); @@ -121,6 +128,9 @@ pub fn calculate_quota(state: &mut CountState) { // TODO: Different rounding rules state.quota += N::one(); state.quota.floor_mut(); + log.push_str(format!("{:.2}.", state.quota).as_str()); + + state.logs.push(log); } fn meets_quota(quota: &N, count_card: &CountCard) -> bool { @@ -139,11 +149,12 @@ pub fn elect_meeting_quota(state: &mut CountState) { cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); // Declare elected in descending order of votes - for (_, count_card) in cands_meeting_quota.into_iter().rev() { + for (candidate, count_card) in cands_meeting_quota.into_iter().rev() { // TODO: Log count_card.state = CandidateState::ELECTED; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; + state.logs.push(format!("{} meets the quota and is elected.", candidate.name)); } } } @@ -189,7 +200,12 @@ where // Transfer candidate votes // Unweighted inclusive Gregory // TODO: Other methods - //let transfer_value = surplus.clone() / &result.total_ballots; + let transfer_value = surplus.clone() / &result.total_ballots; + + state.kind = Some("Surplus of"); + state.title = String::from(&elected_candidate.name); + state.logs.push(format!("Surplus of {} distributed at value {:.2}.", elected_candidate.name, transfer_value)); + let mut checksum = N::new(); for (candidate, entry) in result.candidates.into_iter() { @@ -234,6 +250,9 @@ where pub fn bulk_elect(state: &mut CountState) -> bool { if state.election.candidates.len() - state.num_excluded <= state.election.seats { + state.kind = None; + state.title = "Bulk election".to_string(); + // Bulk elect all remaining candidates let mut hopefuls: Vec<(&&Candidate, &mut CountCard)> = state.candidates.iter_mut() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) @@ -242,10 +261,12 @@ pub fn bulk_elect(state: &mut CountState) -> bool { // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); - for (_, count_card) in hopefuls.into_iter() { + for (candidate, count_card) in hopefuls.into_iter() { count_card.state = CandidateState::ELECTED; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; + + state.logs.push(format!("{} elected to fill remaining vacancies.", candidate.name)); } return true; @@ -264,6 +285,11 @@ pub fn exclude_hopefuls(state: &mut CountState) -> bool { // Exclude lowest ranked candidate let excluded_candidate = hopefuls.first().unwrap().0; + + state.kind = Some("Exclusion of"); + state.title = String::from(&excluded_candidate.name); + state.logs.push(format!("No surpluses to distribute, so {} is excluded.", excluded_candidate.name)); + exclude_candidate(state, excluded_candidate); return true; @@ -277,6 +303,11 @@ pub fn continue_exclusion(state: &mut CountState) -> bool { if excluded_with_votes.len() > 0 { excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); let excluded_candidate = excluded_with_votes.first().unwrap().0; + + state.kind = Some("Exclusion of"); + state.title = String::from(&excluded_candidate.name); + state.logs.push(format!("Continuing exclusion of {}.", excluded_candidate.name)); + exclude_candidate(state, excluded_candidate); return true; }