From 5e8d58c3b01ad33c14730256c61c12619251f5a4 Mon Sep 17 00:00:00 2001 From: Gustav Eek Date: Thu, 25 May 2023 22:28:41 +0200 Subject: [PATCH] Src. Refactor and migrate all logic into a library Create *src/lib.rs* (and reduce *src/main.rs*) for all logic. TODO Next is further separation into files *main*, *run*, *parse* (or *io*), and *dist* (or *distributions*). --- src/lib.rs | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 331 ++-------------------------------------------------- 2 files changed, 337 insertions(+), 323 deletions(-) create mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c971262 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,329 @@ +//! Copyright (C) 2023 Gustav Eek +//! +//! SPDX-License-Identifier: GPL-3.0-or-later + +use std::io; // bring flush() into scope with `use std::io::Write` +use std::io::prelude::*; // Bring `std::io::BufRead` in scope + +// Statrs, `don't use statrs::prec` and +// `statrs::statistics::Distribution` from examples +use statrs::distribution::{ + Exp, LogNormal, ContinuousCDF}; // Continuous needed for pdf + +use clap::Parser; // CLI Parser, replaces need for use `std::env` +use regex::Regex; + +pub const DEBUG: bool = false; + +fn winnow(mut list: Vec) -> (Conf, Vec ) { + + list.retain(|x| x != ""); // only keep nonempty + + // Search patterns. Find pipe tables as a bullet item + + let bullet = Regex::new(r"^(\s*[\*|-]\s*)").unwrap(); + let number = Regex::new(r"^(\s*)([0-9]+)([).]\s*)").unwrap(); + let prio = Regex::new(r"^\s*[0-9]+\s*%[|\s]*").unwrap(); + + if DEBUG { eprintln!( // debug patterns + "Matches first item: '{}', '{}', '{}'", + match bullet.captures(&list[0]){ + None => "", + Some(x) => x.get(0).unwrap().as_str()}, + match number.captures(&list[0]){ + None => "", + Some(x) => x.get(0).unwrap().as_str()}, + match prio.captures(&list[0]){ + None => "", + Some(x) => x.get(0).unwrap().as_str()});} + + // Use index of the longest relevant group match to create conf + // template. Call this "the rule of index of the longest match". + + let maxi_bullet = list.iter().map( + |y| match bullet.captures(y) { + None => 0, + Some(x) => x.get(1).unwrap().as_str().len(), + }).enumerate().fold( + (0, 0), |max, (ind, val)| if val > max.1 {(ind, val)} else {max}).0; + + let maxi_number = list.iter().map( + |y| match number.captures(y) { + None => 0, + Some(x) => x.get(2).unwrap().as_str().len(), + }).enumerate().fold( + (0, 0), |max, (ind, val)| if val > max.1 {(ind, val)} else {max}).0; + + if DEBUG { eprintln!( // debug longest match + "Longest match: {}, '{}'; {}, '{}'", + maxi_bullet, + match bullet.captures(&list[maxi_bullet]) { + None => "", + Some(x) => x.get(1).unwrap().as_str()}, + maxi_number, + match number.captures(&list[maxi_number]) { + None => "", + Some(x) => x.get(2).unwrap().as_str()})}; + + let conf = Conf { + bullet: match bullet.captures(&list[maxi_bullet]) { + None => "".to_string(), + Some(x) => x.get(1).unwrap().as_str().to_string(), + }, + number: match number.captures(&list[maxi_number]) { + None => "".to_string(), + Some(x) => format!( + "{}{{}}{}", + x.get(1).unwrap().as_str(), + x.get(3).unwrap().as_str()) + }, + }; + + if DEBUG { + eprintln!("Conf: {:?}", conf); + } + + // Remove patterns and trim + + for l in &mut *list { + *l = bullet.replace_all(&l, "").to_string(); + *l = number.replace_all(&l, "").to_string(); + *l = prio.replace_all(&l, "").to_string(); + *l = l.trim().to_owned(); + } + list.retain(|x| x != ""); // again + return (conf, list) +} + +pub fn input() -> (Conf, Vec) { + + let stdin = io::stdin(); + let list: Vec = + stdin.lock().lines() + .map(|x| x.unwrap()) + .collect(); + return winnow(list) +} + +pub fn output(conf: &Conf, prio: &Vec, ranked: &Vec) { + + let digs: usize = (prio.len() as f32).log(10.0).ceil() as usize; + + for (i, (p, l)) in prio.iter().zip(ranked).enumerate() { + + let pre = if conf.number != "" { + conf.number.replace("{}", format!( // Replace template due + "{:digs$}", i + 1).as_str()) // to smt::fmt + } else { + conf.bullet.clone() + }; + let pst = if conf.bullet.contains("|") {" | "} else {"\t"}; + + println!("{}{:2.0} %{}{}", pre, 100.0 * p, pst, l) + } +} + +fn normalize(mut arg: Vec) -> Vec { + + let mut sum = 0.0; + for v in &arg { + sum += v; + } + for i in 0..arg.len() { + arg[i] /= sum; + } + arg +} + +pub fn delta(n: i32) -> Vec { + + const NAME: &str = "Delta"; + const MEAN: f64 = 1.0; // unessential thanks to normalization + let mut prio: Vec = Vec::new(); + if DEBUG { eprint!("{}: ", NAME) } + for i in 1..n + 1 { + prio.push(MEAN); + if DEBUG { + eprint!("i = {}, x = {:.2}; ", i, MEAN); + } + } + if DEBUG { eprintln!("\x08\x08."); } + prio.reverse(); + normalize(prio) +} + +pub fn exp(n: i32) -> Vec { + + const NAME: &str = "Exp"; + const RATE: f64 = 1.0; // rate is unessential thanks to normalization + let dist = Exp::new(RATE).unwrap(); + let mut prio: Vec = Vec::new(); + if DEBUG { eprint!("{}: ", NAME) } + for i in 1..n + 1 { + let f = i as f64 / (n as f64 + 1.0); + let x = dist.inverse_cdf(f); + if DEBUG { + eprint!("i = {}, f = {:.2}, x = {:.2}; ", i, f, x); + } + prio.push(x); + } + if DEBUG { eprintln!("\x08\x08."); } + prio.reverse(); + normalize(prio) +} + +pub fn lognormal(n: i32, std: f64) -> Vec { + + const NAME: &str = "Lognormal"; + const MEAN: f64 = 0.0; // mean is unessential thanks to + // normalization, and std of N + let dist = LogNormal::new(MEAN, std).unwrap(); + let mut prio: Vec = Vec::new(); + if DEBUG { eprint!("{}: ", NAME) } + for i in 1..n + 1 { + let f = i as f64 / (n as f64 + 1.0); + let x = dist.inverse_cdf(f); + if DEBUG { + eprint!("i = {}, f = {:.2}, x = {:.2}; ", i, f, x); + } + prio.push(x); + } + if DEBUG { eprintln!("\x08\x08."); } + prio.reverse(); + normalize(prio) +} + +#[derive(Debug)] + +pub struct Conf { + bullet: String, + number: String, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] + +/// Rank N' Auto, automatic normalized ranking + +pub struct Args { + + /// Ranking distribution + #[arg(short, default_value_t = String::from("lognormal"))] + pub distribution: String, + + /// Skewness of the ranking + #[arg(short)] + pub skewness: Option, +} + + +#[test] + +fn ordinary_input() { + + let mut arg: Vec = Vec::new(); + let mut exp: Vec = Vec::new(); + for s in vec!["", "", "Hej ☕ glade", "", "Amatör", "", ""] { + arg.push(String::from(s)); + } + for s in vec!["Hej ☕ glade", "Amatör"] { + exp.push(String::from(s)); + } + let (_, res) = winnow(arg); + assert_eq!(exp, res); +} + +#[test] + +fn whitespace() { + + let arg: Vec = + "\n\nHej du\t\n glade\nta en\n\n" // Convert &str to String + .split("\n").map(|x| x.to_owned()) // with `.to_owned()` or + .collect(); // `.to_string()` + let exp: Vec = + "Hej du\nglade\nta en" + .split("\n").map(|x| x.to_owned()).collect(); + let (_, res) = winnow(arg); + assert_eq!(exp, res); +} + +#[test] + +fn bullets() { + + let arg: Vec = + "\n * Hej du\t\n - glade\n-\tta en\n\n" + .split("\n").map(|x| x.to_owned()) + .collect(); + let exp: Vec = + "Hej du\nglade\nta en" + .split("\n").map(|x| x.to_owned()).collect(); + let (conf, res) = winnow(arg); + assert_eq!(exp, res); + assert_eq!(conf.bullet, " - ".to_owned()); + assert_eq!(conf.number, "".to_owned()); +} + +#[test] + +fn numbers() { + + let arg: Vec = + "1) Hej du\n 459. glade\n 2)ta en\n" + .split("\n").map(|x| x.to_owned()) + .collect(); + let exp: Vec = + "Hej du\nglade\nta en" + .split("\n").map(|x| x.to_owned()).collect(); + let (conf, res) = winnow(arg); + assert_eq!(exp, res); + assert_eq!(conf.bullet, "".to_owned()); + assert_eq!(conf.number, " {}. ".to_owned()); +} + +#[test] + +fn prioinput() { + + let arg: Vec = + " - 70 % Hej du\n - 30 %\tglade\nta en\n" + .split("\n").map(|x| x.to_owned()) + .collect(); + let exp: Vec = + "Hej du\nglade\nta en" + .split("\n").map(|x| x.to_owned()).collect(); + let (_, res) = winnow(arg); + assert_eq!(exp, res); +} + +#[test] + +fn pipe() { + + let arg: Vec = + "\n| Hej du\n | glade\n| ta en\n" + .split("\n").map(|x| x.to_owned()) + .collect(); + let exp: Vec = + "Hej du\nglade\nta en" + .split("\n").map(|x| x.to_owned()).collect(); + let (conf, res) = winnow(arg); + assert_eq!(exp, res); + assert_eq!(conf.bullet, " | ".to_owned()); + assert_eq!(conf.number, "".to_owned()); +} + +#[test] + +fn bullout() { + + let conf = Conf { + bullet: " * ".to_owned(), + number: "".to_owned()}; + let prio = vec![0.6, 0.3, 0.1]; + let ranked: Vec = + "Hej du\nglade\nta en" + .split("\n").map(|x| x.to_owned()).collect(); + output(&conf, &prio, &ranked); +} diff --git a/src/main.rs b/src/main.rs index 1f90fc4..19f552a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,219 +2,14 @@ //! //! SPDX-License-Identifier: GPL-3.0-or-later -use std::io; // bring flush() into scope with `use std::io::Write` -use std::io::prelude::*; // Bring `std::io::BufRead` in scope +use clap::Parser; -// Statrs, `don't use statrs::prec` and -// `statrs::statistics::Distribution` from examples -use statrs::distribution::{ - Exp, LogNormal, ContinuousCDF}; // Continuous needed for pdf - -use clap::Parser; // CLI Parser, replaces need for use `std::env` -use regex::Regex; - -const DEBUG: bool = false; - -fn winnow(mut list: Vec) -> (Conf, Vec ) { - - list.retain(|x| x != ""); // only keep nonempty - - // Search patterns. Find pipe tables as a bullet item - - let bullet = Regex::new(r"^(\s*[\*|-]\s*)").unwrap(); - let number = Regex::new(r"^(\s*)([0-9]+)([).]\s*)").unwrap(); - let prio = Regex::new(r"^\s*[0-9]+\s*%[|\s]*").unwrap(); - - if DEBUG { eprintln!( // debug patterns - "Matches first item: '{}', '{}', '{}'", - match bullet.captures(&list[0]){ - None => "", - Some(x) => x.get(0).unwrap().as_str()}, - match number.captures(&list[0]){ - None => "", - Some(x) => x.get(0).unwrap().as_str()}, - match prio.captures(&list[0]){ - None => "", - Some(x) => x.get(0).unwrap().as_str()});} - - // Use index of the longest relevant group match to create conf - // template. Call this "the rule of index of the longest match". - - let maxi_bullet = list.iter().map( - |y| match bullet.captures(y) { - None => 0, - Some(x) => x.get(1).unwrap().as_str().len(), - }).enumerate().fold( - (0, 0), |max, (ind, val)| if val > max.1 {(ind, val)} else {max}).0; - - let maxi_number = list.iter().map( - |y| match number.captures(y) { - None => 0, - Some(x) => x.get(2).unwrap().as_str().len(), - }).enumerate().fold( - (0, 0), |max, (ind, val)| if val > max.1 {(ind, val)} else {max}).0; - - if DEBUG { eprintln!( // debug longest match - "Longest match: {}, '{}'; {}, '{}'", - maxi_bullet, - match bullet.captures(&list[maxi_bullet]) { - None => "", - Some(x) => x.get(1).unwrap().as_str()}, - maxi_number, - match number.captures(&list[maxi_number]) { - None => "", - Some(x) => x.get(2).unwrap().as_str()})}; - - let conf = Conf { - bullet: match bullet.captures(&list[maxi_bullet]) { - None => "".to_string(), - Some(x) => x.get(1).unwrap().as_str().to_string(), - }, - number: match number.captures(&list[maxi_number]) { - None => "".to_string(), - Some(x) => format!( - "{}{{}}{}", - x.get(1).unwrap().as_str(), - x.get(3).unwrap().as_str()) - }, - }; - - if DEBUG { - eprintln!("Conf: {:?}", conf); - } - - // Remove patterns and trim - - for l in &mut *list { - *l = bullet.replace_all(&l, "").to_string(); - *l = number.replace_all(&l, "").to_string(); - *l = prio.replace_all(&l, "").to_string(); - *l = l.trim().to_owned(); - } - list.retain(|x| x != ""); // again - return (conf, list) -} - -fn input() -> (Conf, Vec) { - - let stdin = io::stdin(); - let list: Vec = - stdin.lock().lines() - .map(|x| x.unwrap()) - .collect(); - return winnow(list) -} - -fn output(conf: &Conf, prio: &Vec, ranked: &Vec) { - - let digs: usize = (prio.len() as f32).log(10.0).ceil() as usize; - - for (i, (p, l)) in prio.iter().zip(ranked).enumerate() { - - let pre = if conf.number != "" { - conf.number.replace("{}", format!( // Replace template due - "{:digs$}", i + 1).as_str()) // to smt::fmt - } else { - conf.bullet.clone() - }; - let pst = if conf.bullet.contains("|") {" | "} else {"\t"}; - - println!("{}{:2.0} %{}{}", pre, 100.0 * p, pst, l) - } -} - -fn normalize(mut arg: Vec) -> Vec { - - let mut sum = 0.0; - for v in &arg { - sum += v; - } - for i in 0..arg.len() { - arg[i] /= sum; - } - arg -} - -fn delta(n: i32) -> Vec { - - const NAME: &str = "Delta"; - const MEAN: f64 = 1.0; // unessential thanks to normalization - let mut prio: Vec = Vec::new(); - if DEBUG { eprint!("{}: ", NAME) } - for i in 1..n + 1 { - prio.push(MEAN); - if DEBUG { - eprint!("i = {}, x = {:.2}; ", i, MEAN); - } - } - if DEBUG { eprintln!("\x08\x08."); } - prio.reverse(); - normalize(prio) -} - -fn exp(n: i32) -> Vec { - - const NAME: &str = "Exp"; - const RATE: f64 = 1.0; // rate is unessential thanks to normalization - let dist = Exp::new(RATE).unwrap(); - let mut prio: Vec = Vec::new(); - if DEBUG { eprint!("{}: ", NAME) } - for i in 1..n + 1 { - let f = i as f64 / (n as f64 + 1.0); - let x = dist.inverse_cdf(f); - if DEBUG { - eprint!("i = {}, f = {:.2}, x = {:.2}; ", i, f, x); - } - prio.push(x); - } - if DEBUG { eprintln!("\x08\x08."); } - prio.reverse(); - normalize(prio) -} - -fn lognormal(n: i32, std: f64) -> Vec { - - const NAME: &str = "Lognormal"; - const MEAN: f64 = 0.0; // mean is unessential thanks to - // normalization, and std of N - let dist = LogNormal::new(MEAN, std).unwrap(); - let mut prio: Vec = Vec::new(); - if DEBUG { eprint!("{}: ", NAME) } - for i in 1..n + 1 { - let f = i as f64 / (n as f64 + 1.0); - let x = dist.inverse_cdf(f); - if DEBUG { - eprint!("i = {}, f = {:.2}, x = {:.2}; ", i, f, x); - } - prio.push(x); - } - if DEBUG { eprintln!("\x08\x08."); } - prio.reverse(); - normalize(prio) -} - -#[derive(Debug)] - -struct Conf { - bullet: String, - number: String, -} - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] - -/// Rank N' Auto, automatic normalized ranking - -struct Args { - - /// Ranking distribution - #[arg(short, default_value_t = String::from("lognormal"))] - distribution: String, - - /// Skewness of the ranking - #[arg(short)] - skewness: Option, -} +use ranknauto::{ + DEBUG, // flag + input, output, // I/O + delta, exp, lognormal, // distributions + Args, // parsing +}; fn main() { @@ -234,6 +29,7 @@ fn main() { else if a == "moderate" {1.0} else if a == "heavy" {2.0} else {1.0}, + }; if DEBUG { eprintln!("Finally a skewness: {}", skew) } @@ -258,114 +54,3 @@ fn main() { output(&conf, &prio, &ranked); } - -#[test] - -fn ordinary_input() { - - let mut arg: Vec = Vec::new(); - let mut exp: Vec = Vec::new(); - for s in vec!["", "", "Hej ☕ glade", "", "Amatör", "", ""] { - arg.push(String::from(s)); - } - for s in vec!["Hej ☕ glade", "Amatör"] { - exp.push(String::from(s)); - } - let (_, res) = winnow(arg); - assert_eq!(exp, res); -} - -#[test] - -fn whitespace() { - - let arg: Vec = - "\n\nHej du\t\n glade\nta en\n\n" // Convert &str to String - .split("\n").map(|x| x.to_owned()) // with `.to_owned()` or - .collect(); // `.to_string()` - let exp: Vec = - "Hej du\nglade\nta en" - .split("\n").map(|x| x.to_owned()).collect(); - let (_, res) = winnow(arg); - assert_eq!(exp, res); -} - -#[test] - -fn bullets() { - - let arg: Vec = - "\n * Hej du\t\n - glade\n-\tta en\n\n" - .split("\n").map(|x| x.to_owned()) - .collect(); - let exp: Vec = - "Hej du\nglade\nta en" - .split("\n").map(|x| x.to_owned()).collect(); - let (conf, res) = winnow(arg); - assert_eq!(exp, res); - assert_eq!(conf.bullet, " - ".to_owned()); - assert_eq!(conf.number, "".to_owned()); -} - -#[test] - -fn numbers() { - - let arg: Vec = - "1) Hej du\n 459. glade\n 2)ta en\n" - .split("\n").map(|x| x.to_owned()) - .collect(); - let exp: Vec = - "Hej du\nglade\nta en" - .split("\n").map(|x| x.to_owned()).collect(); - let (conf, res) = winnow(arg); - assert_eq!(exp, res); - assert_eq!(conf.bullet, "".to_owned()); - assert_eq!(conf.number, " {}. ".to_owned()); -} - -#[test] - -fn prioinput() { - - let arg: Vec = - " - 70 % Hej du\n - 30 %\tglade\nta en\n" - .split("\n").map(|x| x.to_owned()) - .collect(); - let exp: Vec = - "Hej du\nglade\nta en" - .split("\n").map(|x| x.to_owned()).collect(); - let (_, res) = winnow(arg); - assert_eq!(exp, res); -} - -#[test] - -fn pipe() { - - let arg: Vec = - "\n| Hej du\n | glade\n| ta en\n" - .split("\n").map(|x| x.to_owned()) - .collect(); - let exp: Vec = - "Hej du\nglade\nta en" - .split("\n").map(|x| x.to_owned()).collect(); - let (conf, res) = winnow(arg); - assert_eq!(exp, res); - assert_eq!(conf.bullet, " | ".to_owned()); - assert_eq!(conf.number, "".to_owned()); -} - -#[test] - -fn bullout() { - - let conf = Conf { - bullet: " * ".to_owned(), - number: "".to_owned()}; - let prio = vec![0.6, 0.3, 0.1]; - let ranked: Vec = - "Hej du\nglade\nta en" - .split("\n").map(|x| x.to_owned()).collect(); - output(&conf, &prio, &ranked); -} -- 2.39.2