/** * Exact rational arithmetic for genetics probabilities. * * Punnett distributions are unions of dyadic/rational fractions (1/4, 3/16, …). * Using exact fractions instead of floats keeps probabilities precise and lets * us assert e.g. "exactly 1/4 of young are lost" after renormalisation. */ export interface Fraction { readonly num: number readonly den: number } function gcd(a: number, b: number): number { a = Math.abs(a) b = Math.abs(b) while (b !== 0) { ;[a, b] = [b, a % b] } return a } /** Create a normalised (reduced, positive-denominator) fraction. */ export function frac(num: number, den: number): Fraction { if (den === 0) throw new Error('Fraction denominator must not be zero') if (den < 0) { num = -num den = -den } if (num === 0) return { num: 0, den: 1 } const g = gcd(num, den) return { num: num / g, den: den / g } } export const ZERO: Fraction = { num: 0, den: 1 } export const ONE: Fraction = { num: 1, den: 1 } export function add(a: Fraction, b: Fraction): Fraction { return frac(a.num * b.den + b.num * a.den, a.den * b.den) } export function multiply(a: Fraction, b: Fraction): Fraction { return frac(a.num * b.num, a.den * b.den) } export function divide(a: Fraction, b: Fraction): Fraction { if (b.num === 0) throw new Error('Cannot divide by zero fraction') return frac(a.num * b.den, a.den * b.num) } export function equals(a: Fraction, b: Fraction): boolean { return a.num * b.den === b.num * a.den } export function toNumber(f: Fraction): number { return f.num / f.den } /** Human-readable "3/16" (or "0" / "1"). */ export function toString(f: Fraction): string { if (f.num === 0) return '0' if (f.den === 1) return String(f.num) return `${f.num}/${f.den}` } /** "18.75 %" style label for display (rounded to `digits` decimals). */ export function toPercentString(f: Fraction, digits = 2): string { const pct = (f.num / f.den) * 100 const rounded = Number(pct.toFixed(digits)) return `${rounded} %` }