Add logs during count

This commit is contained in:
RunasSudo 2021-05-29 01:22:46 +10:00
parent df69ef456f
commit 91190c7e26
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 70 additions and 41 deletions

View File

@ -97,6 +97,10 @@ pub struct CountState<'a, N> {
pub num_elected: usize, pub num_elected: usize,
pub num_excluded: usize, pub num_excluded: usize,
pub kind: Option<&'a str>,
pub title: String,
pub logs: Vec<String>,
} }
impl<'a, N: Number> CountState<'a, N> { impl<'a, N: Number> CountState<'a, N> {
@ -109,6 +113,9 @@ impl<'a, N: Number> CountState<'a, N> {
quota: N::new(), quota: N::new(),
num_elected: 0, num_elected: 0,
num_excluded: 0, num_excluded: 0,
kind: None,
title: String::new(),
logs: Vec::new(),
}; };
for candidate in election.candidates.iter() { 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> { 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>), Ref(&'a CountState<'a, N>),
} }
impl<'a, N> CountStateOrRef<'a, N> { impl<'a, N> CountStateOrRef<'a, N> {
pub fn from(state: &'a CountState<N>) -> Self {
return Self::Ref(state);
}
pub fn as_ref(&self) -> &CountState<N> { pub fn as_ref(&self) -> &CountState<N> {
match self { match self {
CountStateOrRef::State(state) => &state, CountStateOrRef::State(state) => &state,
@ -142,8 +154,9 @@ impl<'a, N> CountStateOrRef<'a, N> {
} }
pub struct StageResult<'a, N> { pub struct StageResult<'a, N> {
pub title: &'a str, pub kind: Option<&'a str>,
pub logs: Vec<&'a str>, pub title: &'a String,
pub logs: &'a Vec<String>,
pub state: CountStateOrRef<'a, N>, pub state: CountStateOrRef<'a, N>,
} }

View File

@ -76,7 +76,10 @@ fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a Cou
fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) { fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) {
// Print stage details // 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(" ")); println!("{}", result.logs.join(" "));
let state = result.state.as_ref(); let state = result.state.as_ref();
@ -106,9 +109,17 @@ fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &
println!(""); println!("");
} }
fn main() { fn make_and_print_result<N: Number>(stage_num: usize, state: &CountState<N>, cmd_opts: &STV) {
let should_clone_state = false; 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() {
// Read arguments // Read arguments
let opts: Opts = Opts::parse(); let opts: Opts = Opts::parse();
let Command::STV(cmd_opts) = opts.command; let Command::STV(cmd_opts) = opts.command;
@ -127,17 +138,11 @@ fn main() {
stv::elect_meeting_quota(&mut state); stv::elect_meeting_quota(&mut state);
// Display // 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; let mut stage_num = 1;
make_and_print_result(stage_num, &state, &cmd_opts);
loop { loop {
state.logs.clear();
state.step_all(); state.step_all();
stage_num += 1; stage_num += 1;
@ -149,48 +154,28 @@ fn main() {
// Continue exclusions // Continue exclusions
if stv::continue_exclusion(&mut state) { if stv::continue_exclusion(&mut state) {
stv::elect_meeting_quota(&mut state); stv::elect_meeting_quota(&mut state);
let result = StageResult { make_and_print_result(stage_num, &state, &cmd_opts);
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);
continue; continue;
} }
// Distribute surpluses // Distribute surpluses
if stv::distribute_surpluses(&mut state) { if stv::distribute_surpluses(&mut state) {
stv::elect_meeting_quota(&mut state); stv::elect_meeting_quota(&mut state);
let result = StageResult { make_and_print_result(stage_num, &state, &cmd_opts);
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);
continue; continue;
} }
// Attempt bulk election // Attempt bulk election
if stv::bulk_elect(&mut state) { if stv::bulk_elect(&mut state) {
stv::elect_meeting_quota(&mut state); stv::elect_meeting_quota(&mut state);
let result = StageResult { make_and_print_result(stage_num, &state, &cmd_opts);
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);
continue; continue;
} }
// Exclude lowest hopeful // Exclude lowest hopeful
if stv::exclude_hopefuls(&mut state) { if stv::exclude_hopefuls(&mut state) {
stv::elect_meeting_quota(&mut state); stv::elect_meeting_quota(&mut state);
let result = StageResult { make_and_print_result(stage_num, &state, &cmd_opts);
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);
continue; continue;
} }

View File

@ -109,11 +109,18 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
let parcel = result.exhausted.votes as Parcel<N>; let parcel = result.exhausted.votes as Parcel<N>;
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
state.exhausted.transfer(&result.exhausted.num_votes); 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<N: Number>(state: &mut CountState<N>) { pub fn calculate_quota<N: Number>(state: &mut CountState<N>) {
let mut log = String::new();
// Calculate the total vote // Calculate the total vote
state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); 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 // TODO: Different quotas
state.quota /= N::from(state.election.seats + 1); state.quota /= N::from(state.election.seats + 1);
@ -121,6 +128,9 @@ pub fn calculate_quota<N: Number>(state: &mut CountState<N>) {
// TODO: Different rounding rules // TODO: Different rounding rules
state.quota += N::one(); state.quota += N::one();
state.quota.floor_mut(); state.quota.floor_mut();
log.push_str(format!("{:.2}.", state.quota).as_str());
state.logs.push(log);
} }
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>) -> bool { fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>) -> bool {
@ -139,11 +149,12 @@ pub fn elect_meeting_quota<N: Number>(state: &mut CountState<N>) {
cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
// Declare elected in descending order of votes // 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 // TODO: Log
count_card.state = CandidateState::ELECTED; count_card.state = CandidateState::ELECTED;
state.num_elected += 1; state.num_elected += 1;
count_card.order_elected = state.num_elected as isize; 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 // Transfer candidate votes
// Unweighted inclusive Gregory // Unweighted inclusive Gregory
// TODO: Other methods // 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(); let mut checksum = N::new();
for (candidate, entry) in result.candidates.into_iter() { for (candidate, entry) in result.candidates.into_iter() {
@ -234,6 +250,9 @@ where
pub fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool { pub fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
if state.election.candidates.len() - state.num_excluded <= state.election.seats { 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 // Bulk elect all remaining candidates
let mut hopefuls: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut() let mut hopefuls: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
@ -242,10 +261,12 @@ pub fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
// TODO: Handle ties // TODO: Handle ties
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); 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; count_card.state = CandidateState::ELECTED;
state.num_elected += 1; state.num_elected += 1;
count_card.order_elected = state.num_elected as isize; count_card.order_elected = state.num_elected as isize;
state.logs.push(format!("{} elected to fill remaining vacancies.", candidate.name));
} }
return true; return true;
@ -264,6 +285,11 @@ pub fn exclude_hopefuls<N: Number>(state: &mut CountState<N>) -> bool {
// Exclude lowest ranked candidate // Exclude lowest ranked candidate
let excluded_candidate = hopefuls.first().unwrap().0; 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); exclude_candidate(state, excluded_candidate);
return true; return true;
@ -277,6 +303,11 @@ pub fn continue_exclusion<N: Number>(state: &mut CountState<N>) -> bool {
if excluded_with_votes.len() > 0 { 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()); 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; 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); exclude_candidate(state, excluded_candidate);
return true; return true;
} }