Files
backend/src/common/decimal.ts
T
ksv741 9cc9fbdcff
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 31s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 1m56s
refactor: replace ignored numeric literals with constants
2026-09-28 11:24:59 +03:00

85 lines
3.3 KiB
TypeScript

export const DECIMAL_RADIX = 10n;
const SIGN_LENGTH = 1;
const NEGATIVE_SIGN = -1n;
const POSITIVE_SIGN = 1n;
const ZERO_UNITS = 0n;
const EQUAL = 0;
const GREATER_THAN = 1;
const LESS_THAN = -1;
const INTEGER_SCALE = 0;
const MIN_WHOLE_DIGITS = 1;
const DIGITS_START = 0;
const PREVIOUS_DIGIT_OFFSET = 1;
const DIGIT_STEP = 1;
const ROUNDING_DIVISOR = 2n;
export type Decimal = { scale: number; value: bigint };
export function decimalFromString(value: string): Decimal {
// eslint-disable-next-line security/detect-unsafe-regex -- Anchored decimal grammar: the literal decimal point separates disjoint digit runs; backtracking is linear.
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
throw new Error('Invalid decimal string');
}
const negative = value.startsWith('-');
const [whole, fraction = ''] = (negative ? value.slice(SIGN_LENGTH) : value).split('.');
return {
value: (negative ? NEGATIVE_SIGN : POSITIVE_SIGN) * BigInt(`${whole}${fraction}`),
scale: fraction.length,
};
}
export function add(left: Decimal, right: Decimal): Decimal {
const scale = Math.max(left.scale, right.scale);
return {
value:
left.value * DECIMAL_RADIX ** BigInt(scale - left.scale) +
right.value * DECIMAL_RADIX ** BigInt(scale - right.scale),
scale,
};
}
export function subtract(left: Decimal, right: Decimal): Decimal {
return add(left, { value: -right.value, scale: right.scale });
}
export function multiply(left: Decimal, right: Decimal): Decimal {
return { value: left.value * right.value, scale: left.scale + right.scale };
}
export function isZero(value: Decimal): boolean {
return value.value === ZERO_UNITS;
}
export function compare(left: Decimal, right: Decimal): number {
const difference = subtract(left, right).value;
return difference === ZERO_UNITS ? EQUAL : difference > ZERO_UNITS ? GREATER_THAN : LESS_THAN;
}
export function formatDecimal({ value, scale }: Decimal): string {
if (value === ZERO_UNITS) return '0';
const sign = value < ZERO_UNITS ? '-' : '';
if (scale === INTEGER_SCALE) return `${sign}${value < ZERO_UNITS ? -value : value}`;
const digits = (value < ZERO_UNITS ? -value : value)
.toString()
.padStart(scale + MIN_WHOLE_DIGITS, '0');
const whole = digits.slice(DIGITS_START, -scale) || '0';
const rawFraction = digits.slice(-scale);
let fractionEnd = rawFraction.length;
while (rawFraction.charAt(fractionEnd - PREVIOUS_DIGIT_OFFSET) === '0') fractionEnd -= DIGIT_STEP;
const fraction = rawFraction.slice(DIGITS_START, fractionEnd);
const fractionalPart = fraction ? '.' + fraction : '';
return `${sign}${whole}${fractionalPart}`;
}
// Round half away from zero; callers choose presentation precision explicitly.
export function divide(left: Decimal, right: Decimal, scale: number): Decimal {
if (right.value === ZERO_UNITS) throw new Error('Division by zero');
const numerator = left.value * DECIMAL_RADIX ** BigInt(right.scale + scale);
const denominator = right.value * DECIMAL_RADIX ** BigInt(left.scale);
const sign = (numerator < ZERO_UNITS) !== (denominator < ZERO_UNITS) ? NEGATIVE_SIGN : POSITIVE_SIGN;
const n = numerator < ZERO_UNITS ? -numerator : numerator;
const d = denominator < ZERO_UNITS ? -denominator : denominator;
return { value: sign * ((n + d / ROUNDING_DIVISOR) / d), scale };
}