arm-web/src/utils/money.ts
Andrey Epifancev a8a6da0de3 Форматирование перед PR: ESLint + Prettier и npm run format (#29)
- Prettier — единственный форматтер (табы, 140, одинарные кавычки, в CSS — двойные);
  форматтер biome выключен, biome остаётся линтером и сортирует импорты.
- ESLint (flat config): js/typescript-eslint/react-hooks recommended + правила biome.json,
  eslint-config-prettier последним.
- npm run format / make format: eslint --fix → biome check --write → prettier --write.
- npm run lint: eslint + prettier --check; входит в make lint и make check.
- Разовое переформатирование 4 файлов, `(A | B)[]` вместо Array<…> в 3 местах,
  две находки ESLint в ConsoleTable (лишний `?? null`, тернарник-инструкция).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:15:14 +04:00

69 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const NBSP = ' ';
const PLAIN = /^(-?)(\d+)(?:\.(\d+))?$/;
const INPUT = /^-?\d+(\.\d{1,2})?$/;
const MONEY = /^(-?)(\d+)(?:\.(\d{1,2}))?$/;
export type AmountFormat = {
/** Ноль показывать прочерком (свод, ОСВ — как в макете [30]). */
zeroDash?: boolean;
/** 'auto' — без копеек, если они нулевые (формы ББ/ОФР в тыс. руб., макет [41]). */
fraction?: 'always' | 'auto';
/** Что показать вместо пустого значения. */
empty?: string;
};
/** Сумма из API (строка Decimal) → «1 234 567,80». Без арифметики над float; нераспознанное — как есть. */
export function formatAmount(value: string | null | undefined, format: AmountFormat = {}): string {
const empty = format.empty ?? '—';
if (value === null || value === undefined || value === '') {
return empty;
}
const match = PLAIN.exec(value);
if (!match) {
return value;
}
const [, sign, intPart, frac = ''] = match;
if (frac.length > 2) {
return value;
}
const isZero = /^0+$/.test(intPart) && /^0*$/.test(frac);
if (isZero && format.zeroDash) {
return '—';
}
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, NBSP);
const cents = frac.padEnd(2, '0');
if (format.fraction === 'auto' && cents === '00') {
return `${isZero ? '' : sign}${grouped}`;
}
return `${isZero ? '' : sign}${grouped},${cents}`;
}
/** Ввод аналитика → строка суммы для API; пустое — null; неверный формат — ok: false. */
export function parseAmountInput(input: string): { ok: true; value: string | null } | { ok: false } {
const normalized = input.replace(/[\s ]/g, '').replace(',', '.');
if (normalized === '') {
return { ok: true, value: null };
}
return INPUT.test(normalized) ? { ok: true, value: normalized } : { ok: false };
}
/** Точная сумма строк Decimal из API (копейками, без float) → «123.45»; пустые пропускаются, все пустые — null.
* Сумма, которую не удалось разобрать (больше двух знаков, текст), делает итог null — итог не выдумываем. */
export function sumAmounts(values: readonly (string | null | undefined)[]): string | null {
let cents = 0n;
let any = false;
for (const value of values) {
if (value === null || value === undefined || value === '') continue;
const match = MONEY.exec(value);
if (!match) return null;
const [, sign, intPart, frac = ''] = match;
const amount = BigInt(intPart) * 100n + BigInt(frac.padEnd(2, '0'));
cents += sign ? -amount : amount;
any = true;
}
if (!any) return null;
const negative = cents < 0n;
const abs = negative ? -cents : cents;
return `${negative ? '-' : ''}${abs / 100n}.${(abs % 100n).toString().padStart(2, '0')}`;
}