phrs 0.4.1

A tabular data shell tool compatible with ph
Documentation
pub mod cat;
pub mod columns;
pub mod date;
pub mod describe;
pub mod diff;
pub mod dropna;
pub mod eval;
pub mod fillna;
pub mod head;
pub mod help;
pub mod merge;
pub mod open;
pub mod query;
pub mod rename;
pub mod rolling;
pub mod round;
pub mod shape;
pub mod show;
pub mod slice;
pub mod slugify;
pub mod sort;
pub mod statistics;
pub mod strip;
pub mod tail;

use crate::{cli::Invocation, error::PhError, io};

pub fn validate(inv: &Invocation) -> Result<(), PhError> {
    match inv.command.as_str() {
        "open" => {
            inv.arity(2, 2)?;
            if inv.args[0] != "excel" {
                return Err(PhError::new(format!(
                    "Unknown open format: {}.",
                    inv.args[0]
                )));
            }
            if let Some((key, _)) = inv
                .kwargs
                .iter()
                .find(|(key, _)| !matches!(key.as_str(), "sheet" | "sheet_name"))
            {
                return Err(PhError::new(format!("Unknown option --{key}.")));
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "cat" => {
            for (key, _) in &inv.kwargs {
                if key != "axis" {
                    return Err(PhError::new(format!("Unknown option --{key}.")));
                }
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "merge" => {
            inv.arity(2, 2)?;
            for (key, _) in &inv.kwargs {
                if !["how", "on", "left", "right"].contains(&key.as_str()) {
                    return Err(PhError::new(format!("Unknown option --{key}.")));
                }
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "sum" | "mean" | "median" | "std" | "min" | "max" => {
            for (key, _) in &inv.kwargs {
                if !["axis", "skipna", "numeric_only", "ddof", "min_count"].contains(&key.as_str())
                {
                    return Err(PhError::new(format!("Unknown option --{key}.")));
                }
                if key == "ddof" && inv.command != "std" {
                    return Err(PhError::new("--ddof is only supported for std."));
                }
                if key == "min_count" && inv.command != "sum" {
                    return Err(PhError::new("--min_count is only supported for sum."));
                }
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "rolling" => {
            inv.arity(1, usize::MAX)?;
            for (key, _) in &inv.kwargs {
                if !["how", "min_periods", "center", "ddof"].contains(&key.as_str()) {
                    return Err(PhError::new(format!("Unknown option --{key}.")));
                }
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "dropna" => {
            inv.arity(0, 0)?;
            for (key, _) in &inv.kwargs {
                if !["axis", "how", "thresh"].contains(&key.as_str()) {
                    return Err(PhError::new(format!("Unknown option --{key}.")));
                }
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "fillna" => {
            inv.arity(0, 1)?;
            for (key, _) in &inv.kwargs {
                if !["method", "limit"].contains(&key.as_str()) {
                    return Err(PhError::new(format!("Unknown option --{key}.")));
                }
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "help" => {
            inv.no_options()?;
            inv.arity(0, 1)
        }
        "round" => {
            inv.arity(1, 2)?;
            if let Some((key, _)) = inv.kwargs.iter().find(|(key, _)| key != "decimals") {
                return Err(PhError::new(format!("Unknown option --{key}.")));
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "columns" => inv.no_options(),
        "date" => {
            inv.arity(1, 1)?;
            if let Some((key, _)) = inv
                .kwargs
                .iter()
                .find(|(key, _)| !matches!(key.as_str(), "format" | "dayfirst" | "utc"))
            {
                return Err(PhError::new(format!("Unknown option --{key}.")));
            }
            if let Some(key) = inv
                .flags
                .iter()
                .find(|key| !matches!(key.as_str(), "dayfirst" | "utc"))
            {
                return Err(PhError::new(format!("Unknown flag --{key}.")));
            }
            Ok(())
        }

        "describe" => {
            inv.no_options()?;
            inv.arity(0, 0)
        }

        "diff" => {
            if let Some((key, _)) = inv
                .kwargs
                .iter()
                .find(|(key, _)| key != "periods" && key != "axis")
            {
                return Err(PhError::new(format!("Unknown option --{key}.")));
            }
            if let Some(flag) = inv.flags.first() {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }

        "head" | "tail" => {
            inv.no_options()?;
            inv.arity(0, 1)
        }
        "rename" => {
            inv.no_options()?;
            inv.arity(2, 2)
        }
        "sort" => {
            inv.arity(1, 1)?;
            Ok(())
        }
        "show" => {
            inv.arity(0, 0)?;
            if let Some((key, _)) = inv.kwargs.first() {
                return Err(PhError::new(format!("Unknown option --{key}.")));
            }
            if let Some(flag) = inv.flags.iter().find(|flag| flag.as_str() != "no-index") {
                return Err(PhError::new(format!("Unknown flag --{flag}.")));
            }
            Ok(())
        }
        "slice" => {
            inv.no_options()?;
            inv.arity(1, 1)
        }

        "shape" => {
            inv.no_options()?;
            inv.arity(0, 0)
        }
        "query" | "eval" => {
            inv.no_options()?;
            inv.arity(1, 1)
        }
        "strip" => inv.no_options(),
        "slugify" => {
            inv.no_options()?;
            inv.arity(0, 0)
        }
        _ => Err(PhError::new(format!(
            "Unknown command {}.\nUsage: ph command [args]",
            inv.command
        ))),
    }
}

pub fn execute(inv: &Invocation, data: &[u8]) -> Result<Vec<u8>, PhError> {
    if inv.command == "help" {
        return help::run(inv);
    }
    if inv.command == "open" {
        return open::run(inv);
    }
    if inv.command == "cat" {
        return cat::run(inv, data);
    }
    if inv.command == "merge" {
        return merge::run(inv);
    }
    let df = io::read_csv(data)?;
    match inv.command.as_str() {
        "sum" | "mean" | "median" | "std" | "min" | "max" => statistics::run(df, inv),
        "rolling" => rolling::run(df, inv),
        "round" => round::run(df, inv),
        "dropna" => dropna::run(df, inv),
        "fillna" => fillna::run(df, inv),
        "columns" => columns::run(df, inv),
        "date" => date::run(df, inv),
        "describe" => describe::run(df),
        "diff" => diff::run(df, inv),
        "head" => head::run(df, inv),
        "tail" => tail::run(df, inv),
        "rename" => rename::run(df, inv),
        "sort" => sort::run(df, inv),
        "slice" => slice::run(df, inv),
        "shape" => shape::run(df),
        "show" => show::run(df, inv),
        "query" => query::run(df, inv),
        "eval" => eval::run(df, inv),
        "strip" => strip::run(df, inv),
        "slugify" => slugify::run(df),
        _ => unreachable!("validated before CSV input"),
    }
}

pub fn count_arg(inv: &Invocation, default: i64) -> Result<i64, PhError> {
    match inv.args.first() {
        None => Ok(default),
        Some(v) => v
            .parse::<i64>()
            .map_err(|_| PhError::new(format!("Invalid row count: {v}."))),
    }
}