use super::funejson::Value; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::units_convert::convert_units; // 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(), )) }