mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 11:06:40 +07:00
feat: улучшить импорт CSV — дедупликация, кодировка, парсинг сумм
Пять доработок импорта транзакций из CSV: 1. Проверка дублей при повторном импорте — сверка с уже существующими транзакциями на счёте за диапазон дат файла (по дате+типу+сумме+ описанию), похожие исключаются из импорта по умолчанию с опцией всё равно их добавить. 2. Автоопределение кодировки файла (UTF-8 с откатом на Windows-1251) вместо жёстко зашитого UTF-8 — раньше кириллица из банковских выгрузок превращалась в кракозябры. Есть ручной выбор на случай неверного автоопределения. 3. Исправлен баг в parseAmount: суммы с несколькими разделителями тысяч и без дробной части (например "1,234,567") теряли цифры и отбрасывались как нераспознанные — теперь распознаются верно. 4. Добавлен явный выбор разделителя дробной части (точка/запятая) на случай, когда автоопределение не может разрешить неоднозначность. 5. Пример значения из файла показывается прямо под каждым выбором колонки при сопоставлении — не нужно доходить до превью, чтобы понять, что в колонке.
This commit is contained in:
@@ -86,21 +86,53 @@ export function parseDateWithFormat(raw: string, format: DateFormat): string | n
|
||||
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/** Parses a bank-export amount string — handles thousands separators and
|
||||
* either "," or "." as the decimal mark by assuming whichever comes last is
|
||||
* the decimal separator (e.g. "1.234,56" and "1,234.56" both work). */
|
||||
export function parseAmount(raw: string): number | null {
|
||||
export const AMOUNT_FORMATS = ["auto", "dot-decimal", "comma-decimal"] as const;
|
||||
export type AmountFormat = (typeof AMOUNT_FORMATS)[number];
|
||||
|
||||
/** Parses a bank-export amount string. `format` lets the user resolve what
|
||||
* "auto" can't: a single "," or "." with nothing else in the string is
|
||||
* genuinely ambiguous (thousands separator vs. decimal mark) — there's no
|
||||
* way to tell "1,234" (one thousand two hundred thirty-four) from "1,234"
|
||||
* (one point two three four) without knowing the exporting bank's locale.
|
||||
* "auto" keeps the historical last-separator-wins heuristic for that case
|
||||
* (e.g. "1.234,56" and "1,234.56" both resolve correctly since both
|
||||
* separators are present), but no longer corrupts amounts that have
|
||||
* *multiple* thousands separators and no decimal part at all (e.g.
|
||||
* "1,234,567" or "1.234.567") — a number can have at most one decimal
|
||||
* point, so 2+ occurrences of the same separator can only be thousands
|
||||
* grouping. */
|
||||
export function parseAmount(raw: string, format: AmountFormat = "auto"): number | null {
|
||||
let value = raw.trim().replace(/[\s ]/g, "");
|
||||
if (!value) return null;
|
||||
|
||||
const hasComma = value.includes(",");
|
||||
const hasDot = value.includes(".");
|
||||
if (hasComma && hasDot) {
|
||||
value = value.lastIndexOf(",") > value.lastIndexOf(".") ? value.replace(/\./g, "").replace(",", ".") : value.replace(/,/g, "");
|
||||
} else if (hasComma) {
|
||||
value = value.replace(",", ".");
|
||||
if (format === "dot-decimal") {
|
||||
value = value.replace(/,/g, "");
|
||||
} else if (format === "comma-decimal") {
|
||||
value = value.replace(/\./g, "").replace(",", ".");
|
||||
} else {
|
||||
const commaCount = (value.match(/,/g) ?? []).length;
|
||||
const dotCount = (value.match(/\./g) ?? []).length;
|
||||
if (commaCount > 0 && dotCount > 0) {
|
||||
value = value.lastIndexOf(",") > value.lastIndexOf(".") ? value.replace(/\./g, "").replace(",", ".") : value.replace(/,/g, "");
|
||||
} else if (commaCount > 1) {
|
||||
value = value.replace(/,/g, "");
|
||||
} else if (commaCount === 1) {
|
||||
value = value.replace(",", ".");
|
||||
} else if (dotCount > 1) {
|
||||
value = value.replace(/\./g, "");
|
||||
}
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
|
||||
/** Stable key for spotting a re-imported bank export that overlaps a
|
||||
* previous one — see pages/CsvImportPage.tsx's duplicate check. Matches on
|
||||
* date + type + amount (2dp) + description, the same fields the API
|
||||
* actually stores, so it works whether the "existing" side comes from a
|
||||
* Transaction or the "candidate" side from a freshly parsed CSV row. */
|
||||
export function transactionDedupeKey(date: string, type: string, amount: number | string, description: string): string {
|
||||
const normalizedAmount = typeof amount === "number" ? amount : Number(amount);
|
||||
return `${date}|${type}|${normalizedAmount.toFixed(2)}|${description.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user