rkg 0.1.0

A one-liner oriented record/grid processor
use anyhow::{bail, Context, Result};

use crate::ast::{Call, Expr};

pub fn arg_string(call: &Call, idx: usize) -> Result<String> {
    call.args
        .get(idx)
        .ok_or_else(|| anyhow::anyhow!("missing arg {} for {}", idx + 1, call.name))
        .and_then(expr_to_string)
}

pub fn expr_to_string(expr: &Expr) -> Result<String> {
    match expr {
        Expr::Str(s) => Ok(s.clone()),
        Expr::Num(n) => Ok(n.to_string()),
        Expr::Ident(s) => Ok(s.clone()),
        Expr::Call(_) => bail!("nested call cannot be converted to string here"),
    }
}

pub fn arg_usize1(call: &Call, idx: usize) -> Result<usize> {
    arg_usize_expr(call, idx)
}

pub fn arg_usize_expr(call: &Call, idx: usize) -> Result<usize> {
    match call.args.get(idx) {
        Some(Expr::Num(n)) if *n > 0 => Ok(*n as usize),
        Some(Expr::Str(s)) => s
            .parse::<usize>()
            .with_context(|| format!("expected integer arg for {}", call.name)),
        _ => bail!(
            "expected positive integer arg {} for {}",
            idx + 1,
            call.name
        ),
    }
}

pub fn parse_num(s: &str) -> Result<f64> {
    s.parse::<f64>()
        .with_context(|| format!("not a number: {s}"))
}

pub fn trim_num(v: f64) -> String {
    if v.fract() == 0.0 {
        format!("{}", v as i64)
    } else {
        format!("{}", v)
    }
}

pub fn unescape(s: &str) -> String {
    s.replace(r"\t", "\t")
        .replace(r"\n", "\n")
        .replace(r"\r", "\r")
}

pub fn split_keep_nonempty<'a>(input: &'a str, sep: &str) -> Vec<&'a str> {
    if sep == "\n" {
        input.lines().collect()
    } else {
        input.split(sep).filter(|s| !s.is_empty()).collect()
    }
}