~skye/com/com.rs

view raw


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::<Vec<_>>()
    }};
    ($n:expr, $f:expr) => {{
        (0..$n).map($f).collect::<Vec<_>>()
    }};
}

/** 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<T>(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<T>(x: T) -> String
    where
        String: From<T>,
    {
        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<T>(x: T) -> String
    where
        String: From<T>,
    {
        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_<T>(s: T, c: F, rev: bool) -> String
    where
        String: From<T>,
    {
        let mut s = {
            let s = String::from(s);
            /* reverse if needed */
            if rev {
                s.chars().rev().collect::<String>()
            } else {
                s
            }
        };

        /* mutable iterator
         * has to be in two separate steps because otherwise it breaks */
        let mut i = s.chars().rev().collect::<Vec<_>>();
        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::<String>()
        } else {
            s
        }
    }

    /** trim all chars matching c from the left */
    pub fn trim_left<T>(s: T, c: char) -> String
    where
        String: From<T>,
    {
        trim_(s, &|x| x == c, true)
    }

    /** trim all chars matching c from the right */
    pub fn trim_right<T>(s: T, c: char) -> String
    where
        String: From<T>,
    {
        trim_(s, &|x| x == c, false)
    }

    /** trim all chars where f */
    pub fn trim_left_f<T>(s: T, c: F) -> String
    where
        String: From<T>,
    {
        trim_(s, c, true)
    }

    /** trim all chars where f */
    pub fn trim_right_f<T>(s: T, c: F) -> String
    where
        String: From<T>,
    {
        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<T>(p: &str, x: T) -> String
    where
        String: From<T>,
    {
        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::<Vec<_>>());
}

#[test]
fn iota_map() {
    let x = iota!(5, |x| x + 1);
    assert_eq!(x, iota!(5).into_iter().map(|x| x + 1).collect::<Vec<_>>())
}

#[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");
}