Functional Weave
Code in Rust

finance.margin@1.0.0

impl/typescript.ts

2,034 bytes · the TypeScript 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.

import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { subtractMoney } from "./money_add.ts";  ← from money.add ^1.0.0 · built alongside by fune
import type { Money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type MarginBreakdown } from "./finance_margin_types.ts";

/**
 * 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.
 */
export function margin(cost: Money, price: Money): MarginBreakdown {
  // subtractMoney also rejects a GBP cost against a EUR price, which is the
  // only way this function can be called with genuinely unanswerable inputs.
  const profit = subtractMoney(price, cost);

  return {
    cost,
    price,
    profit,
    // 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.
    marginBasisPoints: price.minor === 0 ? null : roundDiv(profit.minor * 10000, price.minor, "half-up"),
    markupBasisPoints: cost.minor === 0 ? null : roundDiv(profit.minor * 10000, cost.minor, "half-up"),
  };
}

/** Convert a margin to the markup that produces it. Undefined at 100% margin. */
export function marginToMarkupBasisPoints(marginBasisPoints: number): number {
  if (marginBasisPoints >= 10000) {
    throw new RangeError(`a margin of ${marginBasisPoints} basis points has no finite markup`);
  }
  return roundDiv(marginBasisPoints * 10000, 10000 - marginBasisPoints, "half-up");
}

/** Convert a markup to the margin it produces. */
export function markupToMarginBasisPoints(markupBasisPoints: number): number {
  return roundDiv(markupBasisPoints * 10000, 10000 + markupBasisPoints, "half-up");
}