use super::funejson::Value; use super::math_round_div::round_div; use super::money_add::subtract_money; use super::money_amount::{money_from_value, money_to_value, Money}; /// Profit on a sale, expressed both ways. /// /// Margin divides by the price, markup divides by the cost, and the gap between /// them is wide enough to price a whole range wrongly: 10.00 to 15.00 is a 50% /// markup and a 33.33% margin. Returning only one of them invites the caller to /// use it as the other, so both are returned and neither is called "the rate". /// /// Both ratios are in basis points and come out of integer division, so no /// float ever touches the money. /// /// # Panics /// Panics if cost and price are in different currencies. pub fn margin(cost: &Money, price: &Money) -> MarginBreakdown { // subtract_money also rejects a GBP cost against a EUR price, which is the // only way this function can be called with genuinely unanswerable inputs. let profit = subtract_money(price, cost); // A zero denominator is not an error here: a giveaway (price 0) and a free // sample (cost 0) are real trade. The ratio is undefined, not wrong, and // the rest of the breakdown is still correct. let margin_basis_points = if price.minor == 0 { None } else { Some(round_div(profit.minor * 10000, price.minor, "half-up")) }; let markup_basis_points = if cost.minor == 0 { None } else { Some(round_div(profit.minor * 10000, cost.minor, "half-up")) }; MarginBreakdown { cost: cost.clone(), price: price.clone(), profit, margin_basis_points, markup_basis_points, } } /// Convert a margin to the markup that produces it. Undefined at 100% margin. /// /// # Panics /// Panics at or above a 100% margin, where the markup is infinite. pub fn margin_to_markup_basis_points(margin_basis_points: i64) -> i64 { if margin_basis_points >= 10000 { panic!( "a margin of {} basis points has no finite markup", margin_basis_points ); } round_div(margin_basis_points * 10000, 10000 - margin_basis_points, "half-up") } /// Convert a markup to the margin it produces. pub fn markup_to_margin_basis_points(markup_basis_points: i64) -> i64 { round_div(markup_basis_points * 10000, 10000 + markup_basis_points, "half-up") } fn basis_points_to_value(basis_points: Option) -> Value { match basis_points { Some(bp) => Value::Int(bp), None => Value::Null, } } pub fn margin_breakdown_to_value(breakdown: &MarginBreakdown) -> Value { Value::obj(vec![ ("cost", money_to_value(&breakdown.cost)), ("price", money_to_value(&breakdown.price)), ("profit", money_to_value(&breakdown.profit)), ( "marginBasisPoints", basis_points_to_value(breakdown.margin_basis_points), ), ( "markupBasisPoints", basis_points_to_value(breakdown.markup_basis_points), ), ]) } pub fn fune_vector(args: &[Value]) -> Value { margin_breakdown_to_value(&margin( &money_from_value(&args[0]), &money_from_value(&args[1]), )) }