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);
}