78 lines
2.4 KiB
TypeScript
78 lines
2.4 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}`;
|
|
}
|