71 lines
2.0 KiB
TypeScript
71 lines
2.0 KiB
TypeScript
/**
|
|
* 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} %`
|
|
}
|