pub mod macros { pub use crate::{elog, fatal, iota, log, log_fmt, puts}; } mod colors { pub const CLR: &str = "\x1b[22;39m"; pub const BOLD_RED: &str = "\x1b[1;91m"; } /** in k: !n. * make vec of range 0..n. * * $f optionally expands into a map before collect */ #[macro_export] macro_rules! iota { ($n:expr) => {{ (0..$n).collect::>() }}; ($n:expr, $f:expr) => {{ (0..$n).map($f).collect::>() }}; } /** crash and die gracefully. * optional $p for process exit code */ #[macro_export] macro_rules! fatal { ($($t:tt)*) => {{ eprintln!( "{}fatal error{}: {}", crate::colors::BOLD_RED, crate::colors::CLR, format!($($t)*) ); std::process::exit(-1); }}; ($($t:tt)*, $p:expr) => {{ eprintln!( "{}fatal error{}: {}", crate::colors::BOLD_RED, crate::colors::CLR, format!($($t)*) ); std::process::exit($p); }}; } /** put a string to stdout with no newline */ #[macro_export] macro_rules! puts { ($($t:tt)*) => {{ use std::io::Write; print!($($t)*); match std::io::stdout().flush() { Ok(_) => (), Err(e) => fatal!("can't flush stdout: {e}"), } }}; } /** format a log string */ #[macro_export] macro_rules! log_fmt { ($x:expr, $($y:tt)*) => {{ use colored::Colorize; format!("{} {} : {}", $x, $crate::com::now().blue(), format!($($y)*)) }}; } /** print a log message */ #[macro_export] macro_rules! log { ($($x:tt)*) => {{ println!("{}", log_fmt!("log".yellow(), $($x)*)); }}; } /** log an error */ #[macro_export] macro_rules! elog { ($($x:tt)*) => {{ eprintln!("{}", log_fmt!("err".red(), $($x)*)); }}; } #[allow(dead_code)] pub mod com { /** division that rounds up */ pub fn ceil_div(x: usize, y: usize) -> usize { (x as f64 / y as f64).ceil() as usize } /** reverse x. mutates rather than returns */ pub fn rev(x: &mut [T]) { let l = x.len() - 1; for i in (0..ceil_div(l, 2)).into_iter() { x.swap(i, l - i); } } /** basename of a filename */ pub fn basename(x: T) -> String where String: From, { let x = String::from(x); let v: Vec<_> = x.split("/").collect(); (if v.len() > 0 { v[v.len() - 1] } else { v[0] }).to_string() } /** dirname of a filename. * warning: leaves a trailing / for root files */ pub fn dirname(x: T) -> String where String: From, { let x = String::from(x); let mut v: Vec<_> = x.split("/").collect(); /* remove the last item, which is the basename */ v.pop(); if v.len() > 1 { v.join("/") } else { String::new() } } pub type F<'a> = &'a dyn Fn(char) -> bool; /** generic trim for trim_right and trim_left, taking an argument * that determines if the chars are reversed. * setting rev to true makes it a trim left, otherwise it's * a trim right */ fn trim_(s: T, c: F, rev: bool) -> String where String: From, { let mut s = { let s = String::from(s); /* reverse if needed */ if rev { s.chars().rev().collect::() } else { s } }; /* mutable iterator * has to be in two separate steps because otherwise it breaks */ let mut i = s.chars().rev().collect::>(); let mut i = i.iter_mut(); /* pop while char == c */ while let Some(x) = i.next() { if c(*x) { let _ = s.pop(); } else { break; } } if rev { s.chars().rev().collect::() } else { s } } /** trim all chars matching c from the left */ pub fn trim_left(s: T, c: char) -> String where String: From, { trim_(s, &|x| x == c, true) } /** trim all chars matching c from the right */ pub fn trim_right(s: T, c: char) -> String where String: From, { trim_(s, &|x| x == c, false) } /** trim all chars where f */ pub fn trim_left_f(s: T, c: F) -> String where String: From, { trim_(s, c, true) } /** trim all chars where f */ pub fn trim_right_f(s: T, c: F) -> String where String: From, { trim_(s, c, false) } /** convert an int to a corresponding bool */ pub fn i64_to_bool(x: i64) -> bool { if x == 0 { false } else { true } } /** convert a bool to a corresponding int */ pub fn bool_to_i64(x: bool) -> i64 { if x { 1 } else { 0 } } /** read line from stdin, taking prompt * like the python thing, yk? */ pub fn input(p: &str) -> String { /* read into b */ let mut b = String::new(); let stdin = std::io::stdin(); puts!("{p}"); match stdin.read_line(&mut b) { Ok(x) => x, Err(e) => fatal!("failed to read line from stdin: {e}"), }; /* trim writespace bc the newline is tacked * on and we don't need it anyway */ b.trim().to_string() } /** get input or return x if empty */ pub fn input_or(p: &str, x: T) -> String where String: From, { let i = input(p); if i == String::new() || is_all_space(&mut i.chars()) { String::from(x) } else { i } } /** are all chars whitespace. * x has to be a mutable for all */ fn is_all_space(x: &mut std::str::Chars) -> bool { x.all(|x| match x { ' ' | '\t' | '\n' => true, _ => false, }) } /** get the current time as a nice formatted string */ pub fn now() -> String { chrono::Local::now().format("%Y-%m-%d %H:%M").to_string() } } #[test] fn rev_even_len() { let mut x = vec![1, 2, 3, 4]; com::rev(&mut x); assert_eq!(x, vec![4, 3, 2, 1]); } #[test] fn rev_odd_len() { let mut x = iota!(5, |x| x + 1); com::rev(&mut x); assert_eq!(x, iota!(5, |x| x + 1).into_iter().rev().collect::>()); } #[test] fn iota_map() { let x = iota!(5, |x| x + 1); assert_eq!(x, iota!(5).into_iter().map(|x| x + 1).collect::>()) } #[test] fn basename() { assert_eq!(com::basename("/home/skye"), "skye"); assert_eq!(com::basename("foo"), "foo"); } #[test] fn dirname() { assert_eq!(com::dirname("/home/skye"), "/home"); assert_eq!(com::dirname("/home"), ""); } #[test] fn trim() { assert_eq!(com::trim_right("foo ", ' '), "foo"); assert_eq!(com::trim_left(" foo", ' '), "foo"); } #[test] fn trim_f() { let w = |x| match x { ' ' | '\n' | '\t' => true, _ => false, }; assert_eq!(com::trim_right_f("foo \n\t", &w), "foo"); assert_eq!(com::trim_left_f("\n \tfoo", &w), "foo"); }