, meta: array} */ public function parse(string $content): array { $content = str_replace(["\r\n", "\r"], "\n", $content); $lines = array_values(array_filter(explode("\n", $content), static fn(string $line): bool => trim($line) !== '')); if ($lines === []) { throw new \RuntimeException('File QIF vuoto.'); } $type = null; $rows = []; $transaction = []; foreach ($lines as $line) { $line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line; $trimmed = trim($line); if ($trimmed === '') { continue; } if (str_starts_with($trimmed, '!Type:')) { $type = substr($trimmed, 6) ?: null; continue; } if ($trimmed === '^') { $row = $this->buildRow($transaction, $type); if ($row !== null) { $rows[] = $row; } $transaction = []; continue; } $code = substr($trimmed, 0, 1); $value = trim(substr($trimmed, 1)); if (! isset($transaction[$code])) { $transaction[$code] = []; } $transaction[$code][] = $value; } if ($transaction !== []) { $row = $this->buildRow($transaction, $type); if ($row !== null) { $rows[] = $row; } } if ($rows === []) { throw new \RuntimeException('Nessun movimento valido trovato nel file QIF.'); } return [ 'rows' => $rows, 'meta' => [ 'format' => 'mps_qif', 'qif_type' => $type, ], ]; } /** * @param array> $transaction * @return array{data: Carbon, valuta: Carbon|null, descrizione: string, descrizione_estesa?: string|null, importo: float, causale: string|null, raw_line: string}|null */ private function buildRow(array $transaction, ?string $type): ?array { $date = $this->parseDate($transaction['D'][0] ?? null); $amount = $this->parseMoney($transaction['T'][0] ?? null); if (! $date || $amount === null) { return null; } $payee = $this->joinParts($transaction['P'] ?? []); $memo = $this->joinParts($transaction['M'] ?? []); $description = $payee !== '' ? $payee : ($memo !== '' ? mb_substr($memo, 0, 255) : 'Movimento QIF'); $extended = trim($payee . ($payee !== '' && $memo !== '' ? ' · ' : '') . $memo); $rawLines = []; foreach ($transaction as $code => $values) { foreach ($values as $value) { $rawLines[] = $code . $value; } } return [ 'data' => $date, 'valuta' => $date->copy(), 'descrizione' => $description, 'descrizione_estesa' => $extended !== '' ? $extended : null, 'importo' => $amount, 'causale' => $type ? ('QIF:' . strtoupper($type)) : 'QIF', 'raw_line' => implode("\n", $rawLines), ]; } /** * @param array $parts */ private function joinParts(array $parts): string { $parts = array_map(static fn(string $value): string => trim($value), $parts); $parts = array_values(array_filter($parts, static fn(string $value): bool => $value !== '')); return trim(implode(' ', $parts)); } private function parseDate(?string $value): ?Carbon { $value = trim((string) $value); if ($value === '') { return null; } foreach (['d/m/Y', 'd/m/y', 'm/d/Y', 'm/d/y'] as $format) { try { return Carbon::createFromFormat($format, $value)->startOfDay(); } catch (\Throwable) { // Try next format. } } return null; } private function parseMoney(?string $value): ?float { $value = trim((string) $value); if ($value === '') { return null; } $value = str_replace(["\u{00A0}", "\t", ' '], '', $value); $value = str_ireplace(['EUR', 'EURO', '€'], '', $value); $value = preg_replace('/[^0-9,\.\-\+]/', '', $value) ?? $value; if (str_contains($value, ',') && str_contains($value, '.')) { $value = str_replace('.', '', $value); $value = str_replace(',', '.', $value); } elseif (str_contains($value, ',')) { $value = str_replace(',', '.', $value); } return is_numeric($value) ? (float) $value : null; } }