GEN-1: add vitest + exact Fraction utility
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
gerbil-manager-web/src/genetics/fraction.ts
Normal file
70
gerbil-manager-web/src/genetics/fraction.ts
Normal 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} %`
|
||||
}
|
||||
Reference in New Issue
Block a user