feat(importer): support YYYYMMDD date format (#470)

## Summary
Adds a compact `YYYYMMDD` (no separators) date format to the
transactions and balance importers.

- New `DateFormat.YearMonthDayCompact = 'YYYYMMDD'` enum value
- `parseDate` parses the 8-digit form (`20241231` → `2024-12-31`)
- `autoDetectDateFormat` recognizes it (unambiguous, no separators)
- Both mapping UIs (transactions + balances) expose it as a selectable
option

## Testing
- `bunx vitest run resources/js/lib/file-parser.test.ts` — 34 passed
- Added tests: compact auto-detection + compact conversion
- `bun run format`, `bun run lint` clean
This commit is contained in:
Víctor Falcón 2026-06-01 16:44:46 +02:00 committed by GitHub
parent d68fee6c2d
commit f9d1303d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 73 additions and 1 deletions

View File

@ -1696,6 +1696,7 @@
"Works everywhere": "Funciona en todas partes",
"Would you like to add more accounts or continue to the dashboard?": "¿Quieres agregar más cuentas o continuar al panel?",
"YYYY-MM-DD (e.g., 2024-12-31)": "YYYY-MM-DD (ej., 2024-12-31)",
"YYYYMMDD (e.g., 20241231)": "YYYYMMDD (ej., 20241231)",
"Year": "Año",
"Yearly": "Anual",
"Yearly code": "Código anual",

View File

@ -282,6 +282,19 @@ export function ImportBalanceStepMapping({
{__('DD-MM-YYYY (e.g., 31-12-2024)')}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value={DateFormat.YearMonthDayCompact}
id="format-ymd-compact"
/>
<Label
htmlFor="format-ymd-compact"
className="cursor-pointer font-normal"
>
{__('YYYYMMDD (e.g., 20241231)')}
</Label>
</div>
</RadioGroup>
</div>
)}

View File

@ -593,6 +593,19 @@ export function ImportStepMapping({
{__('DD-MM-YYYY (e.g., 31-12-2024)')}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value={DateFormat.YearMonthDayCompact}
id="format-ymd-compact"
/>
<Label
htmlFor="format-ymd-compact"
className="cursor-pointer font-normal"
>
{__('YYYYMMDD (e.g., 20241231)')}
</Label>
</div>
</RadioGroup>
</div>
)}

View File

@ -72,6 +72,30 @@ describe('convertRowsToTransactions', () => {
process.env.TZ = originalTimezone;
}
});
it('parses YYYYMMDD compact dates', () => {
const transactions = convertRowsToTransactions(
[
{
date: '20241231',
description: 'New Year Eve',
amount: '10.00',
},
],
{
transaction_date: 'date',
description: 'description',
amount: 'amount',
balance: null,
creditor_name: null,
debtor_name: null,
},
DateFormat.YearMonthDayCompact,
);
expect(transactions).toHaveLength(1);
expect(transactions[0].transaction_date).toBe('2024-12-31');
});
});
describe('convertRowsToTransactions balance column', () => {
@ -279,6 +303,17 @@ describe('autoDetectDateFormat', () => {
DateFormat.DayMonthYear,
);
});
it('detects YYYYMMDD compact format unambiguously', () => {
const data = [
{ date: '20240115' },
{ date: '20240220' },
{ date: '20240325' },
];
expect(autoDetectDateFormat(data, 'date')).toBe(
DateFormat.YearMonthDayCompact,
);
});
});
describe('getLatestTransactionDate', () => {

View File

@ -224,12 +224,14 @@ export function autoDetectDateFormat(
DateFormat.YearMonthDay,
DateFormat.DayMonthYear,
DateFormat.MonthDayYear,
DateFormat.YearMonthDayCompact,
];
const sampleSize = Math.min(10, data.length);
const scores: Record<DateFormat, number> = {
[DateFormat.YearMonthDay]: 0,
[DateFormat.DayMonthYear]: 0,
[DateFormat.MonthDayYear]: 0,
[DateFormat.YearMonthDayCompact]: 0,
};
for (let i = 0; i < sampleSize; i++) {
@ -427,7 +429,14 @@ export function parseDate(
month: number | undefined,
day: number | undefined;
if (str.length === 5) {
if (format === DateFormat.YearMonthDayCompact) {
const compactArray = /^(\d{4})(\d{2})(\d{2})$/.exec(str);
if (compactArray) {
year = Number(compactArray[1]);
month = Number(compactArray[2]);
day = Number(compactArray[3]);
}
} else if (str.length === 5) {
const dateRegex = /^(\d{1,2})-(\d{1,2})$/;
const dateArray = dateRegex.exec(str);
if (dateArray) {

View File

@ -11,6 +11,7 @@ export enum DateFormat {
YearMonthDay = 'YYYY-MM-DD',
MonthDayYear = 'MM-DD-YYYY',
DayMonthYear = 'DD-MM-YYYY',
YearMonthDayCompact = 'YYYYMMDD',
}
export interface ColumnMapping {