Functional Weave
Code in Rust

retail.price-per-unit@1.0.1

impl/rust.rs

3,027 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::units_convert::convert_units;  ← from units.convert ^1.0.0 · built alongside by fune

// Each basis is a units.convert symbol and how many of it the price is for.
fn basis(per: &str) -> (&'static str, i128) {
    match per {
        "kg" => ("kg", 1),
        "100g" => ("g", 100),
        "10g" => ("g", 10),
        "L" => ("L", 1),
        "100mL" => ("mL", 100),
        "10mL" => ("mL", 10),
        "75cL" => ("mL", 750),
        "m" => ("m", 1),
        "m2" => ("m2", 1),
        "m3" => ("m3", 1),
        "item" => ("item", 1),
        other => panic!("unknown unit price basis \"{}\"", other),
    }
}

fn is_decimal(value: &str) -> bool {
    let mut parts = value.splitn(2, '.');
    let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
    digits(parts.next().unwrap_or("")) && parts.next().map_or(true, digits)
}

/// The price of one `per` of the product, rounded half up to the minor unit.
///
/// The quantity is converted exactly (units.convert, 12 decimal places), then
/// price x multiplier / quantity is done in integers, so the one rounding step
/// is the last one.
///
/// # Panics
/// Panics on a negative price, a zero or malformed quantity, an item quantity
/// with a measured basis or the other way round, or units that do not convert.
pub fn unit_price(price: &Money, quantity: &str, unit: &str, per: &str) -> Money {
    if price.minor < 0 {
        panic!("price must not be negative, received {}", price.minor);
    }
    let (target, multiplier) = basis(per);

    let converted: String = if target == "item" || unit == "item" {
        if target != "item" {
            panic!("a quantity in items can only be priced per \"item\"");
        }
        if unit != "item" {
            panic!("per \"item\" needs the quantity in \"item\"");
        }
        if !is_decimal(quantity) {
            panic!("quantity must be more than 0, received \"{}\"", quantity);
        }
        quantity.to_string()
    } else {
        convert_units(quantity, unit, target, 12)
    };
    if converted.starts_with('-') || !converted.bytes().any(|b| (b'1'..=b'9').contains(&b)) {
        panic!("quantity must be more than 0, received \"{}\"", quantity);
    }

    // converted = digits / 10^places, so price / converted = price x 10^places / digits.
    let mut parts = converted.splitn(2, '.');
    let whole = parts.next().unwrap_or("");
    let fraction = parts.next().unwrap_or("");
    let digits: i128 = format!("{}{}", whole, fraction).parse().unwrap();
    let numerator = (price.minor as i128) * multiplier * 10i128.pow(fraction.len() as u32);
    let mut quotient = numerator / digits;
    if (numerator % digits) * 2 >= digits {
        quotient += 1;
    }
    money(quotient as i64, &price.currency)
}

pub fn fune_vector(args: &[Value]) -> Value {
    money_to_value(&unit_price(
        &money_from_value(&args[0]),
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_str(),
    ))
}