23 lines
1.9 KiB
TypeScript
23 lines
1.9 KiB
TypeScript
/** 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}`;}
|