#![warn(missing_docs)]
use clap::{Parser, ValueEnum};
use miette::*;
use std::{
fs,
io::{self, Write},
path::PathBuf,
};
mod data;
pub mod expr;
mod solve;
pub use data::{Data, DataRow, Headers};
pub use expr::Equation;
pub use solve::{fit, Fit};
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct App {
pub target: String,
pub expr: String,
pub data: Option<PathBuf>,
#[arg(long, default_value_t, value_enum)]
pub eq_resolver: EquationResolver,
#[arg(short, long, default_value_t, value_enum)]
pub out: Output,
#[arg(short, long)]
pub no_stats: bool,
#[arg(long)]
pub debug: bool,
}
#[derive(Debug, Copy, Clone, ValueEnum, Default)]
pub enum EquationResolver {
#[default]
V1,
}
#[derive(Debug, Copy, Clone, ValueEnum, Default)]
pub enum Output {
#[default]
Table,
Plain,
Csv,
Md,
Json,
}
impl App {
pub fn run(self) -> Result<()> {
match self.eq_resolver {
EquationResolver::V1 => run::<expr::v1::Eq>(self),
}
}
}
fn run<E>(app: App) -> Result<()>
where
E: Equation,
{
let App {
target,
expr,
data,
eq_resolver: _,
out,
no_stats,
debug,
} = app;
let mut rdr = match &data {
Some(path) => data::CsvReader::new(io::BufReader::new(
fs::File::open(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to open '{}'", path.display()))?,
)),
None => {
eprintln!("Reading CSV from stdin");
data::CsvReader::new(io::stdin())
}
};
let with_path_ctx = || {
data.as_ref()
.map(|p| format!("in '{}'", p.display()))
.unwrap_or_else(|| "from stdin".into())
};
let hdrs = rdr.headers().wrap_err_with(with_path_ctx)?;
let eq = E::parse(&expr, hdrs).wrap_err_with(with_path_ctx)?;
if debug {
return output_debug(&eq, hdrs, &target);
}
let data = data::Data::try_from(rdr).wrap_err_with(with_path_ctx)?;
let fitted = fit(eq, data, &target).wrap_err_with(with_path_ctx)?;
fitted.write_results(out, !no_stats, std::io::stdout())
}
fn output_debug<E: Equation>(eq: &E, hdrs: &Headers, target: &str) -> Result<()> {
if let Some(expr) = eq.expr() {
println!("✖️ Expression:");
println!(" {expr}");
}
let params = eq.params();
println!("📊 Parameters:");
if params.is_empty() {
println!(" <none>");
} else {
for p in params {
print!(" {p}");
let h = data::match_hdr_help(hdrs, &p);
if !h.starts_with("help - no columns match") {
println!(" :: {h}");
} else {
println!();
}
}
}
let vars = eq.vars();
println!("🧮 Variables:");
if vars.is_empty() {
println!(" <none>");
} else {
for x in vars {
println!(" {x}");
}
}
println!("🔎 Target:");
println!(" {target}");
hdrs.find_ignore_case_and_ws(target)
.ok_or_else(|| miette!("target column '{}' not found in headers", target))
.wrap_err_with(|| data::match_hdr_help(hdrs, target))?;
Ok(())
}