Functional Weave
Code in TypeScript

invest.cagr@1.0.1

impl/typescript.ts

2,137 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 { type Money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { FIXED_SCALE, fractionalPowerFixed } from "./math_fractional_power.ts";  ← from math.fractional-power ^1.0.0 · built alongside by fune

/** The largest integer every language holds exactly (2^53 − 1). */
const MAX_SAFE = 9007199254740991n;

function wholeMinor(amount: Money): bigint {
  if (!Number.isInteger(amount.minor)) {
    throw new RangeError(`amounts must be whole minor units, received ${amount.minor}`);
  }
  return BigInt(amount.minor);
}

function checkYears(name: string, value: number): void {
  if (!Number.isInteger(value) || value < 1 || value > 100000) {
    throw new RangeError(`${name} must be a whole number from 1 to 100000, received ${value}`);
  }
}

/** n / d rounded half away from zero; d > 0. */
function roundHalfAway(n: bigint, d: bigint): bigint {
  const magnitude = ((n < 0n ? -n : n) * 2n + d) / (2n * d);
  return n < 0n ? -magnitude : magnitude;
}

/**
 * Compound annual growth rate, (end / start)^(1 / years) − 1, in basis points
 * rounded half away from zero. Years are yearsNumerator / yearsDenominator so
 * no float ever enters the power.
 */
export function cagr(startValue: Money, endValue: Money, yearsNumerator: number, yearsDenominator: number): number {
  const start = wholeMinor(startValue);
  const end = wholeMinor(endValue);
  if (startValue.currency !== endValue.currency) {
    throw new RangeError(`currency mismatch: ${startValue.currency} and ${endValue.currency}`);
  }
  checkYears("yearsNumerator", yearsNumerator);
  checkYears("yearsDenominator", yearsDenominator);
  if (start <= 0n) throw new RangeError(`startValue must be greater than zero, received ${start}`);
  if (end < 0n) throw new RangeError(`endValue must not be negative, received ${end}`);
  if (end === 0n) return -10000;
  const ratio = (end * FIXED_SCALE) / start;
  // Exponent 1 / years = yearsDenominator / yearsNumerator.
  const growth = fractionalPowerFixed(ratio, yearsDenominator, yearsNumerator);
  const bp = roundHalfAway((growth - FIXED_SCALE) * 10000n, FIXED_SCALE);
  if (bp > MAX_SAFE) throw new RangeError("the growth rate is too large to express in basis points");
  return Number(bp);
}