, * meta: array{header_row:int, delimiter:string, parsed_rows:int, skipped_rows:int} * } */ public function parse(string $content): array { $content = str_replace(["\r\n", "\r"], "\n", $content); $lines = explode("\n", $content); $delimiter = ','; $headerIndex = null; $scanMax = min(count($lines), 100); for ($i = 0; $i < $scanMax; $i++) { $line = trim((string) $lines[$i]); if ($line === '') { continue; } if ( stripos($line, 'Data contabile') !== false && stripos($line, 'Importo') !== false && stripos($line, 'Descrizione') !== false ) { $headerIndex = $i; if (substr_count($line, ';') > substr_count($line, ',')) { $delimiter = ';'; } break; } } if ($headerIndex === null) { throw new \RuntimeException('Formato CSV Banco BPM non riconosciuto: header con "Data contabile", "Importo" e "Descrizione" non trovato'); } $header = str_getcsv((string) $lines[$headerIndex], $delimiter); $header = array_map(fn($h) => $this->normHeader((string) $h), $header); $hmap = []; foreach ($header as $idx => $h) { if ($h === '') { continue; } if (!array_key_exists($h, $hmap)) { $hmap[$h] = (int) $idx; } } $idxData = $hmap['datacontabile'] ?? ($hmap['data'] ?? null); $idxValuta = $hmap['datavaluta'] ?? ($hmap['valuta'] ?? null); $idxDescr = $hmap['descrizione'] ?? null; $idxImporto = $hmap['importo'] ?? null; $idxCat = $hmap['categoriasottocategoria'] ?? ($hmap['categoria'] ?? null); if ($idxData === null || $idxDescr === null || $idxImporto === null) { throw new \RuntimeException('Formato CSV Banco BPM non riconosciuto: colonne richieste mancanti'); } $rows = []; $skipped = 0; for ($i = $headerIndex + 1; $i < count($lines); $i++) { $line = trim((string) $lines[$i]); if ($line === '') { continue; } $cols = str_getcsv($line, $delimiter); if (count($cols) <= max($idxData, $idxDescr, $idxImporto)) { $skipped++; continue; } $dateRaw = trim((string) ($cols[$idxData] ?? '')); $valutaRaw = $idxValuta !== null ? trim((string) ($cols[$idxValuta] ?? '')) : ''; $descr = trim((string) ($cols[$idxDescr] ?? '')); $importoRaw = trim((string) ($cols[$idxImporto] ?? '')); $categoria = $idxCat !== null ? trim((string) ($cols[$idxCat] ?? '')) : null; if ($dateRaw === '' || $importoRaw === '') { $skipped++; continue; } $date = $this->parseDate($dateRaw); if (! $date) { $skipped++; continue; } $valuta = $valutaRaw !== '' ? $this->parseDate($valutaRaw) : null; $importo = $this->parseAmount($importoRaw); if ($importo === null) { $skipped++; continue; } $causale = null; if ($categoria !== null && $categoria !== '') { $causale = mb_substr($categoria, 0, 20); } $rows[] = [ 'data' => $date, 'valuta' => $valuta, 'descrizione' => $descr, 'importo' => $importo, 'causale' => $causale, 'raw_line' => $line, ]; } return [ 'rows' => $rows, 'meta' => [ 'header_row' => $headerIndex, 'delimiter' => $delimiter, 'parsed_rows' => count($rows), 'skipped_rows' => $skipped, ], ]; } private function normHeader(string $value): string { $v = mb_strtolower(trim($value)); return (string) preg_replace('/[^a-z0-9]+/i', '', $v); } private function parseDate(string $raw): ?Carbon { $raw = trim($raw); if ($raw === '') { return null; } foreach (['d/m/Y', 'Y-m-d', 'd-m-Y', 'd.m.Y'] as $fmt) { try { $dt = Carbon::createFromFormat($fmt, $raw); if ($dt && $dt->year >= 1990 && $dt->year <= 2100) { return $dt->startOfDay(); } } catch (\Throwable) {} } return null; } private function parseAmount(string $raw): ?float { $raw = trim($raw); if ($raw === '') { return null; } $raw = str_replace('€', '', $raw); $raw = trim($raw); if (str_contains($raw, ',') && str_contains($raw, '.')) { $raw = str_replace('.', '', $raw); $raw = str_replace(',', '.', $raw); } elseif (str_contains($raw, ',')) { $raw = str_replace(',', '.', $raw); } $raw = preg_replace('/[^\d\.\+\-]/', '', $raw); if ($raw === '' || !is_numeric($raw)) { return null; } return round((float) $raw, 2); } }