/** Exact amounts in currency units; decimal formatting is presentation, never a tax rule. */ export type FiscalFraction = { numerator:string; denominator:string }; function parts(value:FiscalFraction):[bigint,bigint] { if(!/^-?\d{1,80}$/.test(value.numerator)||!/^\d{1,80}$/.test(value.denominator)||BigInt(value.denominator)===0n)throw new Error('Representação racional inválida; sem substituição por zero.'); return [BigInt(value.numerator),BigInt(value.denominator)]; } export function fraction(n:bigint,d=1n):FiscalFraction { if(d===0n)throw new Error('Denominador nulo.');if(d<0n){n=-n;d=-d;} let a=n<0n?-n:n,b=d;while(b){const r=a%b;a=b;b=r;}return {numerator:String(n/(a||1n)),denominator:String(d/(a||1n))}; } export function decimalFraction(raw:string):FiscalFraction { if(!/^\d{1,18}(\.\d{1,6})?$/.test(raw))throw new Error('Montante decimal inválido.');const [a,b='']=raw.split('.');return fraction(BigInt(a+b),10n**BigInt(b.length)); } export function addFractions(values:FiscalFraction[]):FiscalFraction {return values.reduce((a,b)=>{const [an,ad]=parts(a),[bn,bd]=parts(b);return fraction(an*bd+bn*ad,ad*bd);},fraction(0n));} export function subtractFractions(a:FiscalFraction,b:FiscalFraction):FiscalFraction {const [an,ad]=parts(a),[bn,bd]=parts(b);return fraction(an*bd-bn*ad,ad*bd);} export function positiveFraction(value:FiscalFraction):FiscalFraction {return parts(value)[0]>0n?value:fraction(0n);} export function fractionPresentation(value:FiscalFraction):{decimal:string;approximate:boolean} { const [raw,d]=parts(value),n=raw<0n?-raw:raw,scaled=n*1000000n; const q=(scaled+d/2n)/d,dec=(q%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0'); return {decimal:`${raw<0n?'-':''}${q/1000000n}.${dec}`,approximate:scaled%d!==0n}; } export function fractionLabel(value:FiscalFraction):string {parts(value);return `${value.numerator}/${value.denominator}`;}