From e526493b0ee85a1380543f9fdeea982e7a217b18 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 29 May 2021 02:13:47 +1000 Subject: [PATCH] Implement smart logger Combine multiple like log entries into one --- src/election.rs | 7 ++-- src/logger.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 +-- src/stv/mod.rs | 31 ++++++++++++----- 4 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 src/logger.rs diff --git a/src/election.rs b/src/election.rs index 98fdf4c..c65a5bd 100644 --- a/src/election.rs +++ b/src/election.rs @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +use crate::logger::Logger; use crate::numbers::Number; use std::collections::HashMap; @@ -100,7 +101,7 @@ pub struct CountState<'a, N> { pub kind: Option<&'a str>, pub title: String, - pub logs: Vec, + pub logger: Logger<'a>, } impl<'a, N: Number> CountState<'a, N> { @@ -115,7 +116,7 @@ impl<'a, N: Number> CountState<'a, N> { num_excluded: 0, kind: None, title: String::new(), - logs: Vec::new(), + logger: Logger { entries: Vec::new() }, }; for candidate in election.candidates.iter() { @@ -156,7 +157,7 @@ impl<'a, N> CountStateOrRef<'a, N> { pub struct StageResult<'a, N> { pub kind: Option<&'a str>, pub title: &'a String, - pub logs: &'a Vec, + pub logs: Vec, pub state: CountStateOrRef<'a, N>, } diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..b91d368 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,88 @@ +/* OpenTally: Open-source election vote counting + * Copyright © 2021 Lee Yingtong Li (RunasSudo) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#[derive(Clone)] +pub struct Logger<'a> { + pub entries: Vec>, +} + +impl<'a> Logger<'a> { + pub fn log(&mut self, entry: LogEntry<'a>) { + if let LogEntry::Smart(mut smart) = entry { + if self.entries.len() > 0 { + if let LogEntry::Smart(last_smart) = self.entries.last_mut().unwrap() { + if last_smart.template1 == smart.template1 && last_smart.template2 == smart.template2 { + &last_smart.data.append(&mut smart.data); + } else { + self.entries.push(LogEntry::Smart(smart)); + } + } else { + self.entries.push(LogEntry::Smart(smart)); + } + } else { + self.entries.push(LogEntry::Smart(smart)); + } + } else { + self.entries.push(entry); + } + } + + pub fn log_literal(&mut self, literal: String) { + self.log(LogEntry::Literal(literal)); + } + + pub fn log_smart(&mut self, template1: &'a str, template2: &'a str, data: Vec<&'a str>) { + self.log(LogEntry::Smart(SmartLogEntry { + template1: template1, + template2: template2, + data: data, + })); + } + + pub fn render(&self) -> Vec { + return self.entries.iter().map(|e| match e { + LogEntry::Smart(smart) => smart.render(), + LogEntry::Literal(literal) => literal.to_string(), + }).collect(); + } +} + +#[derive(Clone)] +pub enum LogEntry<'a> { + Smart(SmartLogEntry<'a>), + Literal(String) +} + +#[derive(Clone)] +pub struct SmartLogEntry<'a> { + template1: &'a str, + template2: &'a str, + data: Vec<&'a str>, +} + +impl<'a> SmartLogEntry<'a> { + pub fn render(&self) -> String { + if self.data.len() == 0 { + panic!("Attempted to format smart log entry with no data"); + } else if self.data.len() == 1 { + return String::from(self.template1).replace("{}", self.data.first().unwrap()); + } else { + let rendered_list = format!("{} and {}", self.data[0..self.data.len()-1].join(", "), self.data.last().unwrap()); + return String::from(self.template2).replace("{}", &rendered_list); + } + } +} diff --git a/src/main.rs b/src/main.rs index f0b3c7f..d9e97bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ */ mod election; +mod logger; mod numbers; mod stv; @@ -113,7 +114,7 @@ fn make_and_print_result(stage_num: usize, state: &CountState, cmd let result = StageResult { kind: state.kind, title: &state.title, - logs: &state.logs, + logs: state.logger.render(), state: CountStateOrRef::from(&state), }; print_stage(stage_num, &result, &cmd_opts); @@ -142,7 +143,7 @@ fn main() { make_and_print_result(stage_num, &state, &cmd_opts); loop { - state.logs.clear(); + state.logger.entries.clear(); state.step_all(); stage_num += 1; diff --git a/src/stv/mod.rs b/src/stv/mod.rs index b963f3d..d4e1a01 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -112,7 +112,7 @@ pub fn distribute_first_preferences(state: &mut CountState) { state.kind = None; state.title = "First preferences".to_string(); - state.logs.push("First preferences distributed.".to_string()); + state.logger.log_literal("First preferences distributed.".to_string()); } pub fn calculate_quota(state: &mut CountState) { @@ -130,7 +130,7 @@ pub fn calculate_quota(state: &mut CountState) { state.quota.floor_mut(); log.push_str(format!("{:.2}.", state.quota).as_str()); - state.logs.push(log); + state.logger.log_literal(log); } fn meets_quota(quota: &N, count_card: &CountCard) -> bool { @@ -150,11 +150,14 @@ pub fn elect_meeting_quota(state: &mut CountState) { // Declare elected in descending order of votes 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)); + state.logger.log_smart( + "{} meets the quota and is elected.", + "{} meet the quota and are elected.", + vec![&candidate.name] + ); } } } @@ -204,7 +207,7 @@ where 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)); + state.logger.log_literal(format!("Surplus of {} distributed at value {:.2}.", elected_candidate.name, transfer_value)); let mut checksum = N::new(); @@ -266,7 +269,11 @@ pub fn bulk_elect(state: &mut CountState) -> bool { state.num_elected += 1; count_card.order_elected = state.num_elected as isize; - state.logs.push(format!("{} elected to fill remaining vacancies.", candidate.name)); + state.logger.log_smart( + "{} is elected to fill the remaining vacancy.", + "{} are elected to fill the remaining vacancies.", + vec![&candidate.name] + ); } return true; @@ -288,7 +295,11 @@ pub fn exclude_hopefuls(state: &mut CountState) -> bool { 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)); + state.logger.log_smart( + "No surpluses to distribute, so {} is excluded.", + "No surpluses to distribute, so {} are excluded.", + vec![&excluded_candidate.name] + ); exclude_candidate(state, excluded_candidate); @@ -306,7 +317,11 @@ pub fn continue_exclusion(state: &mut CountState) -> bool { state.kind = Some("Exclusion of"); state.title = String::from(&excluded_candidate.name); - state.logs.push(format!("Continuing exclusion of {}.", excluded_candidate.name)); + state.logger.log_smart( + "Continuing exclusion of {}.", + "Continuing exclusion of {}.", + vec![&excluded_candidate.name] + ); exclude_candidate(state, excluded_candidate); return true;