math-mumu 0.1.7

Math functions plugin for the Mumu / Lava language
Documentation
// src/banking.rs
use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::Value;

use crate::partials::{is_placeholder, make_two_arg_partial};

use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
use rust_decimal::{Decimal, RoundingStrategy};

/// math:banking(places, value)
/// - Bankers rounding (round half to even)
/// - `places` = number of decimal places (must be >= 0)
/// - Supports partial application:
///     bank2 = math:banking(2)      // returns a function expecting the value
///     bankv = math:banking(_, 2)   // also works with placeholder style
pub fn math_banking_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 => Ok(make_two_arg_partial(banking_finalize, None, None)),
        1 => {
            let a = &args[0];
            if is_placeholder(a) {
                Ok(make_two_arg_partial(banking_finalize, None, None))
            } else {
                // Treat single-arg call as providing `places`, return partial awaiting the value
                Ok(make_two_arg_partial(
                    banking_finalize,
                    Some(a.clone()),
                    None,
                ))
            }
        }
        2 => {
            let lp = is_placeholder(&args[0]);
            let rp = is_placeholder(&args[1]);

            if !lp && !rp {
                banking_finalize(vec![args[0].clone(), args[1].clone()])
            } else {
                Ok(make_two_arg_partial(
                    banking_finalize,
                    if lp { None } else { Some(args[0].clone()) },
                    if rp { None } else { Some(args[1].clone()) },
                ))
            }
        }
        n => Err(format!("math:banking expects 0, 1, or 2 arguments, got {}", n)),
    }
}

fn banking_finalize(args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 2 {
        return Err(format!(
            "banking_finalize => expected 2 args (places, value), got {}",
            args.len()
        ));
    }

    // 1) Parse `places`
    let places = coerce_places(&args[0])?;

    // 2) Convert input value to Decimal
    let dec_val = to_decimal(&args[1])?;

    // 3) Bankers rounding (round half to even)
    let rounded = dec_val.round_dp_with_strategy(places, RoundingStrategy::MidpointNearestEven);

    // 4) Return type:
    //    - If places == 0 and it fits in i32, return Int for ergonomics (mirrors math:round)
    //    - Otherwise return Float
    if places == 0 {
        if let Some(i) = rounded.to_i32() {
            return Ok(Value::Int(i));
        }
    }
    let f = rounded
        .to_f64()
        .ok_or_else(|| "math:banking: cannot convert result to float".to_string())?;
    Ok(Value::Float(f))
}

fn coerce_places(v: &Value) -> Result<u32, String> {
    let raw: i64 = match v {
        Value::Int(i) => *i as i64,
        Value::Long(l) => *l,
        Value::Float(f) => *f as i64, // <- no unnecessary parentheses
        Value::SingleString(s) => s
            .parse::<i64>()
            .map_err(|_| format!("math:banking: places must be an integer, got {:?}", v))?,
        other => {
            return Err(format!(
                "math:banking: places must be an integer, got {:?}",
                other
            ))
        }
    };
    if raw < 0 {
        return Err("math:banking: places must be >= 0".to_string());
    }
    Ok(raw as u32)
}

fn to_decimal(v: &Value) -> Result<Decimal, String> {
    match v {
        Value::Int(i) => Ok(Decimal::from(*i)),
        Value::Long(l) => Decimal::from_i64(*l).ok_or_else(|| "math:banking: long out of range".to_string()),
        Value::Float(f) => Decimal::from_f64(*f)
            .ok_or_else(|| "math:banking: cannot represent float precisely".to_string()),
        Value::SingleString(s) => s
            .parse::<Decimal>()
            .map_err(|_| "math:banking: cannot parse decimal string".to_string()),
        other => Err(format!(
            "math:banking: value must be numeric or decimal string, got {:?}",
            other
        )),
    }
}