- 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>
69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
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')}`;
|
||
}
|