dryRun = (bool) $this->option('dry-run'); $stabile = trim((string) ($this->option('stabile') ?? '')); $stabileIdOption = (int) ($this->option('stabile-id') ?: 0); if ($stabile === '' && $stabileIdOption <= 0) { $this->error('Passa almeno --stabile=0023 oppure --stabile-id=.'); return self::FAILURE; } $stabileRow = $this->resolveStabile($stabile, $stabileIdOption); if (! $stabileRow) { $this->error('Stabile locale non trovato. Verifica codice/id e anagrafica stabili.'); return self::FAILURE; } $stabileId = (int) $stabileRow->id; $stabileCode = $stabile !== '' ? $stabile : trim((string) ($stabileRow->codice_stabile ?? '')); $mdb = $this->resolveMdbPath($stabileCode); if (! is_file($mdb)) { $this->error('File MDB non trovato: ' . $mdb); return self::FAILURE; } $bin = trim((string) @shell_exec('command -v mdb-export')); if ($bin === '') { $this->error('mdb-export non disponibile (installa mdbtools).'); return self::FAILURE; } $this->info('Stabile locale: #' . $stabileId . ' [' . ($stabileRow->codice_stabile ?? 'n/d') . '] ' . ($stabileRow->denominazione ?? '')); $this->line('MDB: ' . $mdb . ($this->dryRun ? ' (DRY RUN)' : '')); $anniRows = $this->exportTable($mdb, ['anni', 'Anni', 'ANNI']); $genRowsAll = $this->exportTable($mdb, ['acqua_gen', 'Acqua_Gen', 'ACQUA_GEN']); $fattRowsAll = $this->exportTable($mdb, ['acqua_fatture', 'Acqua_Fatture', 'ACQUA_FATTURE']); $dettRowsAll = $this->exportTable($mdb, ['acqua_dett', 'Acqua_Dett', 'ACQUA_DETT']); $targetAcquaYears = $this->parseYearsCsv((string) ($this->option('acqua-years') ?? '')); $allowedRifUte = []; $genRows = $genRowsAll; if (! empty($targetAcquaYears)) { $genRows = array_values(array_filter($genRowsAll, function (array $row) use ($targetAcquaYears): bool { $year = $this->extractYear(trim((string) ($row['rif_anno'] ?? ''))); return $year !== null && in_array((int) $year, $targetAcquaYears, true); })); foreach ($genRows as $row) { $rif = trim((string) ($row['rif_ute'] ?? '')); if ($rif !== '') { $allowedRifUte[$rif] = true; } } } else { foreach ($genRows as $row) { $rif = trim((string) ($row['rif_ute'] ?? '')); if ($rif !== '') { $allowedRifUte[$rif] = true; } } } $fattRows = array_values(array_filter($fattRowsAll, function (array $row) use ($allowedRifUte): bool { $rif = trim((string) ($row['rif_ute'] ?? '')); return $rif !== '' && isset($allowedRifUte[$rif]); })); $dettRows = array_values(array_filter($dettRowsAll, function (array $row) use ($allowedRifUte): bool { $rif = trim((string) ($row['rif_ute'] ?? '')); return $rif !== '' && isset($allowedRifUte[$rif]); })); if (empty($dettRows)) { $this->warn('Nessun record in acqua_dett: nulla da importare.'); return self::SUCCESS; } $limit = (int) ($this->option('limit') ?: 0); if ($limit > 0) { $dettRows = array_slice($dettRows, 0, $limit); } $servizio = $this->ensureAcquaServizio($stabileId, $stabileCode); if (! $servizio) { $this->error('Impossibile creare/risolvere il servizio acqua.'); return self::FAILURE; } $this->line('Servizio acqua: #' . (int) $servizio->id . ' ' . ($servizio->nome ?? 'acqua')); $purgedWaterRows = 0; if ((bool) $this->option('purge-non-target-water') && ! empty($allowedRifUte)) { $purgedWaterRows = $this->purgeLegacyLettureOutsideRifUte($stabileId, (int) $servizio->id, array_keys($allowedRifUte)); } $genByRif = $this->groupByKey($genRows, 'rif_ute'); $fattByRif = $this->groupByKey($fattRows, 'rif_ute'); $unitMaps = $this->buildUnitaMaps($stabileId); $lettureStats = $this->importLetture( $stabileId, (int) $servizio->id, $dettRows, $genByRif, $fattByRif, $unitMaps['byLegacyCond'] ?? [], $unitMaps['byScalaInterno'] ?? [] ); $tariffeStats = $this->importTariffeDaAcquaGen($stabileId, (int) $servizio->id, $genRows, $fattByRif); $closedCount = 0; if (! (bool) $this->option('no-close-gestioni')) { $closedCount = $this->closeHistoricGestioni($stabileId, (string) ($this->option('open-years') ?? '2024,2025')); } $this->newLine(); $this->info('Import ACQUA completato'); $this->line('- anni (legacy): ' . count($anniRows)); $this->line('- acqua_gen: ' . count($genRows)); $this->line('- acqua_fatture: ' . count($fattRows)); $this->line('- acqua_dett processate: ' . count($dettRows)); if (! empty($targetAcquaYears)) { $this->line('- anni acqua target: ' . implode(', ', $targetAcquaYears)); } $this->line('- letture create: ' . $lettureStats['created']); $this->line('- letture aggiornate: ' . $lettureStats['updated']); $this->line('- letture purge fuori target: ' . $purgedWaterRows); $this->line('- letture con unita risolta: ' . $lettureStats['matched_unita']); $this->line('- letture senza match unita: ' . $lettureStats['unmatched_unita']); $this->line('- nominativi C/I sincronizzati: ' . $lettureStats['nominativi_synced']); $this->line('- tariffe create: ' . $tariffeStats['created']); $this->line('- tariffe aggiornate: ' . $tariffeStats['updated']); if ($this->tariffeSkipReason) { $this->line('- tariffe skip: ' . $this->tariffeSkipReason); } $this->line('- gestioni storiche chiuse (< open years): ' . $closedCount); return self::SUCCESS; } private function resolveStabile(string $stabileCode, int $stabileId): ?object { if (! Schema::hasTable('stabili')) { return null; } if ($stabileId > 0) { return DB::table('stabili')->where('id', $stabileId)->first(); } $candidates = []; if ($stabileCode !== '') { $candidates[] = $stabileCode; $trim = ltrim($stabileCode, '0'); if ($trim !== '') { $candidates[] = $trim; } if (is_numeric($stabileCode)) { $int = (int) $stabileCode; $candidates[] = (string) $int; $candidates[] = str_pad((string) $int, 4, '0', STR_PAD_LEFT); } } $candidates = array_values(array_unique(array_filter($candidates, fn($v) => trim((string) $v) !== ''))); $query = DB::table('stabili'); $query->where(function ($q) use ($candidates): void { foreach ($candidates as $candidate) { $q->orWhere('codice_stabile', $candidate) ->orWhere('cod_stabile', $candidate) ->orWhere('old_id', $candidate); } }); return $query->orderByDesc('id')->first(); } private function resolveMdbPath(string $stabileCode): string { $explicit = trim((string) ($this->option('mdb') ?? '')); if ($explicit !== '') { return $explicit; } $base = rtrim((string) ($this->option('base-path') ?? '/mnt/gescon-archives/gescon'), '/'); return $base . '/' . $stabileCode . '/generale_stabile.mdb'; } private function exportTable(string $mdb, array $tableCandidates): array { foreach ($tableCandidates as $table) { $cmd = "mdb-export -D '%Y-%m-%d %H:%M:%S' -d '|' -q '" . '"' . "' -R '\\n' " . escapeshellarg($mdb) . ' ' . escapeshellarg($table); $csv = $this->runShell($cmd); if (! is_string($csv) || trim($csv) === '') { continue; } $rows = $this->parseDelimited($csv, '|'); if (! empty($rows)) { return $rows; } } return []; } private function runShell(string $cmd): ?string { $out = shell_exec($cmd . ' 2>/dev/null'); if (! is_string($out) || trim($out) === '') { return null; } return $out; } private function parseDelimited(string $text, string $sep): array { $lines = preg_split('/\r?\n/', trim($text)); if (! is_array($lines) || count($lines) === 0) { return []; } $headers = str_getcsv((string) array_shift($lines), $sep); $headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers); $rows = []; foreach ($lines as $line) { if (trim((string) $line) === '') { continue; } $cols = str_getcsv((string) $line, $sep); if (count($cols) < count($headers)) { $cols = array_pad($cols, count($headers), null); } if (count($cols) > count($headers)) { $cols = array_slice($cols, 0, count($headers)); } $row = @array_combine($headers, $cols); if (! is_array($row)) { continue; } $rows[] = $row; } return $rows; } private function ensureAcquaServizio(int $stabileId, string $stabileCode): ?StabileServizio { $servizio = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderByDesc('id') ->first(); if ($servizio) { return $servizio; } if ($this->dryRun) { return new StabileServizio([ 'id' => 0, 'stabile_id' => $stabileId, 'tipo' => 'acqua', 'nome' => 'Utenza acqua legacy ' . $stabileCode, 'attivo' => true, ]); } return StabileServizio::query()->create([ 'stabile_id' => $stabileId, 'tipo' => 'acqua', 'nome' => 'Utenza acqua legacy ' . $stabileCode, 'attivo' => true, 'meta' => [ 'canale_acquisizione' => 'import_file', 'sorgente' => 'generale_stabile.mdb', ], ]); } private function groupByKey(array $rows, string $key): array { $out = []; foreach ($rows as $row) { $k = trim((string) ($row[strtolower($key)] ?? '')); if ($k === '') { continue; } $out[$k][] = $row; } return $out; } private function buildUnitaMaps(int $stabileId): array { if (! Schema::hasTable('unita_immobiliari')) { return ['byLegacyCond' => [], 'byScalaInterno' => []]; } $rows = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->select(['id', 'legacy_cond_id', 'scala', 'interno']) ->get(); $byLegacyCond = []; $byScalaInterno = []; foreach ($rows as $row) { $id = (int) ($row->id ?? 0); if ($id <= 0) { continue; } $legacy = trim((string) ($row->legacy_cond_id ?? '')); if ($legacy !== '' && ! isset($byLegacyCond[$legacy])) { $byLegacyCond[$legacy] = $id; } $scala = $this->normalizeToken((string) ($row->scala ?? '')); $interno = $this->normalizeToken((string) ($row->interno ?? '')); if ($scala !== '' || $interno !== '') { $byScalaInterno[$scala . '|' . $interno] = $id; } } return ['byLegacyCond' => $byLegacyCond, 'byScalaInterno' => $byScalaInterno]; } private function importLetture( int $stabileId, int $servizioId, array $dettRows, array $genByRif, array $fattByRif, array $unitaByLegacyCond, array $unitaByScalaInterno ): array { if (! Schema::hasTable('stabile_servizio_letture')) { return ['created' => 0, 'updated' => 0, 'matched_unita' => 0, 'unmatched_unita' => 0, 'nominativi_synced' => 0]; } $tableCols = Schema::getColumnListing('stabile_servizio_letture'); $colsSet = array_flip($tableCols); $created = 0; $updated = 0; $matchedUnita = 0; $unmatchedUnita = 0; $nominativiSynced = 0; foreach ($dettRows as $row) { $rifUte = trim((string) ($row['rif_ute'] ?? '')); if ($rifUte === '') { continue; } $legacyCondId = trim((string) ($row['id_cond'] ?? $row['t_cod_cond'] ?? '')); $ruolo = strtoupper(trim((string) ($row['cond_inquil'] ?? ''))); if (! in_array($ruolo, ['C', 'I'], true)) { $ruolo = 'C'; } $scala = $this->normalizeToken((string) ($row['t_scala'] ?? '')); $interno = $this->normalizeToken((string) ($row['t_interno'] ?? '')); $unitaId = null; if ($legacyCondId !== '' && isset($unitaByLegacyCond[$legacyCondId])) { $unitaId = (int) $unitaByLegacyCond[$legacyCondId]; } elseif (isset($unitaByScalaInterno[$scala . '|' . $interno])) { $unitaId = (int) $unitaByScalaInterno[$scala . '|' . $interno]; } if ($unitaId) { $matchedUnita++; } else { $unmatchedUnita++; } $genRef = $genByRif[$rifUte][0] ?? []; $rifAnno = trim((string) ($genRef['rif_anno'] ?? '')); $year = $this->extractYear($rifAnno); $periodoDal = $year ? sprintf('%04d-01-01', $year) : null; $periodoAl = $year ? sprintf('%04d-12-31', $year) : null; $letturaInizio = $this->firstMoney($row, ['lett_vecchia_1', 'lett_vecchia_2', 'lett_vecchia_3']); $letturaFine = $this->firstMoney($row, ['lett_nuova_1', 'lett_nuova_2', 'lett_nuova_3']); $consumo = $this->money($row['consumo'] ?? null); if (abs($consumo) < 0.00001) { $consumo = 0.0; foreach (['cons_1', 'cons_2', 'cons_3', 'cons_4', 'cons_5'] as $consKey) { $consumo += $this->money($row[$consKey] ?? null); } } $importoTotale = 0.0; foreach ((array) ($fattByRif[$rifUte] ?? []) as $fattRow) { $importoTotale += $this->money($fattRow['importo_ft'] ?? null); } if (abs($importoTotale) < 0.00001) { $importoTotale = $this->money($genRef['di_cui_prop_consumi_euro'] ?? $genRef['di_cui_prop_consumi'] ?? null); } $tipoUtenza = trim((string) ($row['tipo_utenza'] ?? '')); $nominativo = trim((string) ($row['t_nome'] ?? '')); $ref = 'LEGACY:' . $rifUte . ':COND:' . ($legacyCondId !== '' ? $legacyCondId : 'NA') . ':' . $ruolo; $match = DB::table('stabile_servizio_letture') ->where('stabile_id', $stabileId) ->where('stabile_servizio_id', $servizioId) ->where('riferimento_acquisizione', $ref) ->first(['id']); $payload = [ 'stabile_id' => $stabileId, 'stabile_servizio_id' => $servizioId, 'unita_immobiliare_id' => $unitaId, 'periodo_dal' => $periodoDal, 'periodo_al' => $periodoAl, 'tipologia_lettura' => trim((string) ($row['tipo_lettura'] ?? '')) ?: null, 'canale_acquisizione' => 'import_file', 'riferimento_acquisizione' => $ref, 'lettura_inizio' => $letturaInizio, 'lettura_fine' => $letturaFine, 'consumo_valore' => $consumo !== 0.0 ? $consumo : null, 'consumo_unita' => 'mc', 'importo_totale' => $importoTotale !== 0.0 ? $importoTotale : null, 'raw' => json_encode([ 'source' => 'legacy_generale_stabile_mdb', 'rif_ute' => $rifUte, 'rif_anno' => $rifAnno, 'legacy_cond_id' => $legacyCondId, 'ruolo_legacy' => $ruolo, 'tipo_utenza' => $tipoUtenza !== '' ? $tipoUtenza : null, 'nome_legacy' => $nominativo !== '' ? $nominativo : null, 't_scala' => trim((string) ($row['t_scala'] ?? '')), 't_interno' => trim((string) ($row['t_interno'] ?? '')), 'n_persone' => trim((string) ($row['n_persone'] ?? '')), 'relativa_a' => trim((string) ($row['relativa_a'] ?? '')), 'acqua_gen' => $genRef, ], JSON_UNESCAPED_UNICODE), 'updated_at' => now(), ]; $payload = array_intersect_key($payload, $colsSet); if ($this->dryRun) { if ($match) { $updated++; } else { $created++; } } else { if ($match) { DB::table('stabile_servizio_letture')->where('id', (int) $match->id)->update($payload); $updated++; } else { $payload['created_at'] = now(); DB::table('stabile_servizio_letture')->insert($payload); $created++; } } if ($unitaId && $nominativo !== '') { $nominativiSynced += $this->syncNominativoRuolo( $stabileId, $unitaId, $legacyCondId, $ruolo, $nominativo, $rifUte, $tipoUtenza ); } } return [ 'created' => $created, 'updated' => $updated, 'matched_unita' => $matchedUnita, 'unmatched_unita' => $unmatchedUnita, 'nominativi_synced' => $nominativiSynced, ]; } private function syncNominativoRuolo( int $stabileId, int $unitaId, string $legacyCondId, string $ruolo, string $nominativo, string $rifUte, string $tipoUtenza ): int { if (! Schema::hasTable('unita_immobiliare_nominativi')) { return 0; } $cols = array_flip(Schema::getColumnListing('unita_immobiliare_nominativi')); $where = [ 'unita_immobiliare_id' => $unitaId, 'stabile_id' => $stabileId, 'legacy_cond_id' => $legacyCondId !== '' ? $legacyCondId : null, 'ruolo' => $ruolo, 'nominativo' => $nominativo, ]; $exists = DB::table('unita_immobiliare_nominativi')->where($where)->first(['id']); $payload = [ 'unita_immobiliare_id' => $unitaId, 'stabile_id' => $stabileId, 'legacy_cond_id' => $legacyCondId !== '' ? $legacyCondId : null, 'ruolo' => $ruolo, 'nominativo' => $nominativo, 'fonte' => 'legacy_acqua_dett', 'legacy_payload' => json_encode([ 'rif_ute' => $rifUte, 'tipo_utenza' => $tipoUtenza !== '' ? $tipoUtenza : null, ], JSON_UNESCAPED_UNICODE), 'updated_at' => now(), ]; $payload = array_intersect_key($payload, $cols); if ($this->dryRun) { return $exists ? 0 : 1; } if ($exists) { DB::table('unita_immobiliare_nominativi')->where('id', (int) $exists->id)->update($payload); return 0; } $payload['created_at'] = now(); DB::table('unita_immobiliare_nominativi')->insert($payload); return 1; } private function importTariffeDaAcquaGen(int $stabileId, int $servizioId, array $genRows, array $fattByRif): array { if (! Schema::hasTable('stabile_servizio_tariffe') || empty($genRows)) { return ['created' => 0, 'updated' => 0]; } // In alcuni ambienti la tabella richiede FK obbligatoria verso fatture_elettroniche. // I dati legacy acqua_gen non hanno questo riferimento diretto: evitiamo hard-fail. if (Schema::hasColumn('stabile_servizio_tariffe', 'fattura_elettronica_id')) { $this->tariffeSkipReason = 'richiesta fattura_elettronica_id obbligatoria (non disponibile su sorgente legacy acqua_gen)'; return ['created' => 0, 'updated' => 0]; } $cols = array_flip(Schema::getColumnListing('stabile_servizio_tariffe')); $created = 0; $updated = 0; foreach ($genRows as $row) { $rifUte = trim((string) ($row['rif_ute'] ?? '')); if ($rifUte === '') { continue; } $rifAnno = trim((string) ($row['rif_anno'] ?? '')); $year = $this->extractYear($rifAnno); $decorrenza = $year ? sprintf('%04d-01-01', $year) : null; $totFatture = 0.0; foreach ((array) ($fattByRif[$rifUte] ?? []) as $fattRow) { $totFatture += $this->money($fattRow['importo_ft'] ?? null); } $componenti = [ 'ripart_precedente' => $this->money($row['ripart_precedente'] ?? null), 'di_cui_prop_consumi_euro' => $this->money($row['di_cui_prop_consumi_euro'] ?? $row['di_cui_prop_consumi'] ?? null), 'di_cui_millesimale_euro' => $this->money($row['di_cui_millesimale_euro'] ?? $row['di_cui_millesimale'] ?? null), 'di_cui_parti_uguali_euro' => $this->money($row['di_cui_parti_uguali_euro'] ?? $row['di_cui_parti_uguali'] ?? null), 'totale_fatture_rif_ute' => $totFatture, 'max_fascia' => trim((string) ($row['max_fascia'] ?? '')), 'criterio_tariffazione' => trim((string) ($row['criterio_tariffazione'] ?? '')), 'n_mesi' => trim((string) ($row['n_mesi'] ?? '')), 'tariffa_periodo' => trim((string) ($row['tariffa_periodo'] ?? '')), ]; $keyWhere = [ 'stabile_id' => $stabileId, 'stabile_servizio_id' => $servizioId, 'profilo_tariffario' => 'LEGACY-RIF-' . $rifUte, ]; $existing = DB::table('stabile_servizio_tariffe')->where($keyWhere)->first(['id']); $payload = [ 'stabile_id' => $stabileId, 'stabile_servizio_id' => $servizioId, 'data_fattura' => $decorrenza, 'decorrenza_tariffe' => $decorrenza, 'profilo_tariffario' => 'LEGACY-RIF-' . $rifUte, 'delibera_ato' => trim((string) ($row['nom_ute'] ?? '')) ?: null, 'tariffe_componenti' => json_encode($componenti, JSON_UNESCAPED_UNICODE), 'payload' => json_encode([ 'source' => 'legacy_generale_stabile_mdb', 'rif_ute' => $rifUte, 'acqua_gen' => $row, 'acqua_fatture' => $fattByRif[$rifUte] ?? [], ], JSON_UNESCAPED_UNICODE), 'updated_at' => now(), ]; $payload = array_intersect_key($payload, $cols); if ($this->dryRun) { if ($existing) { $updated++; } else { $created++; } continue; } if ($existing) { DB::table('stabile_servizio_tariffe')->where('id', (int) $existing->id)->update($payload); $updated++; } else { $payload['created_at'] = now(); DB::table('stabile_servizio_tariffe')->insert($payload); $created++; } } return ['created' => $created, 'updated' => $updated]; } private function closeHistoricGestioni(int $stabileId, string $openYearsRaw): int { if (! Schema::hasTable('gestioni_contabili')) { return 0; } $openYears = array_values(array_filter(array_map(function ($y) { $v = trim((string) $y); return preg_match('/^\d{4}$/', $v) ? (int) $v : null; }, explode(',', $openYearsRaw)))); if (empty($openYears)) { return 0; } $minOpen = min($openYears); $query = DB::table('gestioni_contabili') ->where('anno_gestione', '<', $minOpen) ->when(Schema::hasColumn('gestioni_contabili', 'stabile_id'), fn($q) => $q->where('stabile_id', $stabileId)); $rows = $query->get(['id']); if ($rows->isEmpty()) { return 0; } if ($this->dryRun) { return $rows->count(); } $update = ['updated_at' => now()]; if (Schema::hasColumn('gestioni_contabili', 'stato')) { $update['stato'] = 'chiusa'; } if (Schema::hasColumn('gestioni_contabili', 'gestione_attiva')) { $update['gestione_attiva'] = false; } DB::table('gestioni_contabili') ->whereIn('id', $rows->pluck('id')->all()) ->update($update); return $rows->count(); } /** @return array */ private function parseYearsCsv(string $value): array { $years = []; foreach (explode(',', $value) as $raw) { $v = trim($raw); if (! preg_match('/^\d{4}$/', $v)) { continue; } $years[] = (int) $v; } $years = array_values(array_unique($years)); sort($years); return $years; } /** @param array $allowedRifUte */ private function purgeLegacyLettureOutsideRifUte(int $stabileId, int $servizioId, array $allowedRifUte): int { if (! Schema::hasTable('stabile_servizio_letture')) { return 0; } $allowed = array_fill_keys($allowedRifUte, true); $rows = DB::table('stabile_servizio_letture') ->where('stabile_id', $stabileId) ->where('stabile_servizio_id', $servizioId) ->where('canale_acquisizione', 'import_file') ->where('riferimento_acquisizione', 'like', 'LEGACY:%') ->get(['id', 'riferimento_acquisizione']); $toDeleteIds = []; foreach ($rows as $row) { $ref = (string) ($row->riferimento_acquisizione ?? ''); if (! preg_match('/^LEGACY:([^:]+):COND:/', $ref, $m)) { continue; } $rif = trim((string) ($m[1] ?? '')); if ($rif === '' || isset($allowed[$rif])) { continue; } $toDeleteIds[] = (int) $row->id; } if (empty($toDeleteIds)) { return 0; } if ($this->dryRun) { return count($toDeleteIds); } DB::table('stabile_servizio_letture')->whereIn('id', $toDeleteIds)->delete(); return count($toDeleteIds); } private function firstMoney(array $row, array $keys): ?float { foreach ($keys as $key) { $v = $this->money($row[$key] ?? null); if (abs($v) > 0.00001) { return $v; } } return null; } private function extractYear(string $value): ?int { if (preg_match('/(19|20)\d{2}/', $value, $m)) { return (int) $m[0]; } return null; } private function normalizeToken(string $value): string { $v = strtoupper(trim($value)); $v = preg_replace('/\s+/', '', $v) ?? $v; return $v; } private function money($value): float { if ($value === null || $value === '') { return 0.0; } $s = trim((string) $value); if ($s === '') { return 0.0; } $s = str_replace([" ", "\u{00A0}"], '', $s); if (preg_match('/^-?\d{1,3}(?:\.\d{3})+(?:,\d+)?$/', $s)) { $s = str_replace('.', '', $s); $s = str_replace(',', '.', $s); return (float) $s; } if (preg_match('/^-?\d{1,3}(?:,\d{3})+(?:\.\d+)?$/', $s)) { $s = str_replace(',', '', $s); return (float) $s; } if (str_contains($s, ',') && ! str_contains($s, '.')) { $s = str_replace(',', '.', $s); } elseif (str_contains($s, ',') && str_contains($s, '.')) { $s = str_replace('.', '', $s); $s = str_replace(',', '.', $s); } $s = preg_replace('/[^0-9\-\.]/', '', $s) ?? ''; if ($s === '' || $s === '-' || $s === '.') { return 0.0; } return (float) $s; } }