use crate::{NodeError, NodeResult};
use serde_json::Value;
use std::collections::HashMap;
fn get_f64(inputs: &HashMap<String, Value>, key: &str) -> Result<f64, NodeError> {
inputs
.get(key)
.and_then(|v| v.as_f64())
.ok_or_else(|| NodeError::MissingInput(key.to_owned()))
}
pub fn format(inputs: HashMap<String, Value>) -> NodeResult {
let value = get_f64(&inputs, "value")?;
let decimals = inputs.get("decimals").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let sep = inputs
.get("separator")
.and_then(|v| v.as_str())
.unwrap_or(",");
let dp = inputs
.get("decimal_point")
.and_then(|v| v.as_str())
.unwrap_or(".");
let formatted = format_number(value, decimals, sep, dp);
let mut out = HashMap::new();
out.insert("result".to_owned(), Value::String(formatted));
Ok(out)
}
pub fn round(inputs: HashMap<String, Value>) -> NodeResult {
let value = get_f64(&inputs, "value")?;
let decimals = inputs.get("decimals").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
let mode = inputs
.get("mode")
.and_then(|v| v.as_str())
.unwrap_or("half_up");
let factor = 10f64.powi(decimals as i32);
let rounded = match mode {
"floor" => (value * factor).floor() / factor,
"ceil" => (value * factor).ceil() / factor,
_ => (value * factor).round() / factor,
};
let mut out = HashMap::new();
if decimals == 0 {
out.insert(
"result".to_owned(),
Value::Number(serde_json::Number::from(rounded as i64)),
);
} else {
out.insert(
"result".to_owned(),
Value::Number(
serde_json::Number::from_f64(rounded)
.ok_or_else(|| NodeError::Other("round produced NaN/Inf".to_owned()))?,
),
);
}
Ok(out)
}
pub fn to_percent(inputs: HashMap<String, Value>) -> NodeResult {
let value = get_f64(&inputs, "value")?;
let decimals = inputs.get("decimals").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
let already_percent = inputs
.get("percent")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let pct = if already_percent {
value
} else {
value * 100.0
};
let s = format!("{:.prec$}%", pct, prec = decimals);
let mut out = HashMap::new();
out.insert("result".to_owned(), Value::String(s));
Ok(out)
}
fn format_number(value: f64, decimals: usize, sep: &str, dp: &str) -> String {
let negative = value < 0.0;
let abs = value.abs();
let integer_part = abs as u64;
let frac_part = abs - integer_part as f64;
let int_str = integer_part.to_string();
let mut grouped = String::new();
for (i, ch) in int_str.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
grouped.push_str(&sep.chars().rev().collect::<String>());
}
grouped.push(ch);
}
let int_formatted: String = grouped.chars().rev().collect();
let result = if decimals > 0 {
let frac_str = format!("{:.prec$}", frac_part, prec = decimals);
let frac_digits = &frac_str[2..]; format!("{}{}{}", int_formatted, dp, frac_digits)
} else {
int_formatted
};
if negative {
format!("-{result}")
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn inputs_val(v: f64) -> HashMap<String, Value> {
let mut m = HashMap::new();
m.insert("value".to_owned(), json!(v));
m
}
#[test]
fn format_integer() {
let inp = inputs_val(1234567.0);
let out = format(inp).unwrap();
assert_eq!(out["result"], json!("1,234,567"));
}
#[test]
fn format_with_decimals() {
let mut inp = inputs_val(1234.5);
inp.insert("decimals".to_owned(), json!(2));
let out = format(inp).unwrap();
assert_eq!(out["result"], json!("1,234.50"));
}
#[test]
fn round_half_up() {
let mut inp = inputs_val(2.345);
inp.insert("decimals".to_owned(), json!(2));
let out = round(inp).unwrap();
let result = out["result"].as_f64().unwrap();
assert!((result - 2.34).abs() < 0.01 || (result - 2.35).abs() < 0.01);
}
#[test]
fn to_percent_ratio() {
let inp = inputs_val(0.1234);
let out = to_percent(inp).unwrap();
assert_eq!(out["result"], json!("12.3%"));
}
}