GEN-1: add vitest + exact Fraction utility

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:37:53 +02:00
parent 9b9cf2b35a
commit 2b9a0cd648
3 changed files with 448 additions and 3 deletions

View File

@@ -0,0 +1,70 @@
/**
* 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} %`
}