Parsing delle date in Java
Analizza stringhe in oggetti data-ora Java con DateTimeFormatter e gestisci le eccezioni di parsing.
Il parsing è il contrario della formattazione. Stesso DateTimeFormatter, stesso alfabeto di pattern, stesse avvertenze — ma si legge una stringa invece di scriverne una. Ogni tipo java.time ha un factory method parse(...); con un pattern predefinito (ISO-8601) accetta un argomento, e con un pattern personalizzato accetta un formatter.
LocalDate d = LocalDate.parse("2025-11-04"); // ISO default
LocalTime t = LocalTime.parse("14:30:00");
LocalDateTime dt = LocalDateTime.parse("2025-11-04T14:30:00"); // note the T
ZonedDateTime zdt = ZonedDateTime.parse("2025-11-04T14:30:00-05:00[America/New_York]");
Instant i = Instant.parse("2025-11-04T19:30:00Z"); // trailing Z mandatoryCiascuno usa il formatter predefinito del tipo — ISO-8601 strict. Per qualsiasi cosa che non abbia forma ISO, costruisci un formatter e passalo.
Parsing con pattern personalizzato
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate d = LocalDate.parse("04/11/2025", f); // British DMYL'alfabeto dei pattern è lo stesso della formattazione — dd MMM yyyy, HH:mm:ss, MM/dd/yyyy h:mm a. La regola di corrispondenza è strict per impostazione predefinita: ogni carattere letterale nel pattern deve comparire nell'input verbatim, e ogni campo deve avere il numero corretto di cifre.
DateTimeFormatter dmy = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate.parse("4/11/2025", dmy); // FAILS: "dd" requires 2 digits
LocalDate.parse("04/11/2025", dmy); // OKSe l'input a volte ha una sola cifra, usa d/M/yyyy (che accetta 1-2 cifre) oppure costruisci il formatter con DateTimeFormatterBuilder e parseStrict(false). La forma a lettera singola è la soluzione più semplice.
Il locale conta al confine del parsing
La stessa questione del locale della formattazione: i nomi dei mesi (MMMM) e i nomi dei giorni della settimana (EEEE) sono specifici della lingua, quindi il formatter deve sapere in quale lingua è scritto l'input.
DateTimeFormatter englishDay = DateTimeFormatter.ofPattern("EEEE, MMMM d yyyy", Locale.US);
DateTimeFormatter germanDay = DateTimeFormatter.ofPattern("EEEE, d. MMMM yyyy", Locale.GERMAN);
LocalDate.parse("Tuesday, November 4 2025", englishDay); // 2025-11-04
LocalDate.parse("Dienstag, 4. November 2025", germanDay); // 2025-11-04 (German month name)Nota il yyyy in entrambi i pattern. Per produrre un LocalDate l'input deve fornire un anno — un pattern come "EEEE, MMMM d" esegue il parsing, ma solo in un TemporalAccessor senza campo anno, quindi LocalDate.parse su di esso lancia un'eccezione. Se le tue stringhe non hanno genuinamente un anno, esegui il parsing su un TemporalAccessor e combinalo con un anno tu stesso.
Senza un locale esplicito, viene usato Locale.getDefault() — e il locale predefinito di una JVM server è imprevedibile. Passa sempre un locale quando si effettua il parsing di nomi di mesi o giorni da una stringa che l'utente potrebbe digitare. Vale lo specchio del principio "formatta sempre con un locale".
DateTimeParseException
Un parsing fallito lancia DateTimeParseException (una sottoclasse di RuntimeException, quindi non dichiarata su parse). Il messaggio indica sia la posizione che cosa era atteso:
try {
LocalDate.parse("2025-13-45"); // month 13, day 45
} catch (DateTimeParseException e) {
e.getParsedString(); // "2025-13-45"
e.getErrorIndex(); // index where parsing gave up
e.getMessage(); // human description
}Due tipi distinti di errore finiscono qui:
- Formato non corrispondente. La stringa non corrisponde affatto al pattern —
"04 nov 2025"contro"dd-MM-yyyy". - Valore fuori range. La stringa corrisponde al pattern ma un valore è impossibile — mese 13, giorno 32.
Entrambi lanciano la stessa classe. Cattura e segnala; non ingoiare mai silenziosamente.
Anni a due cifre e la trappola del MAX_VALUE
Il pattern yy (anno a due cifre) ha un comportamento predefinito documentato ma sorprendente: esegue il parsing all'anno più vicino a oggi all'interno di una finestra di 100 anni. LocalDate.parse("11/04/25", DateTimeFormatter.ofPattern("MM/dd/yy")) è 2025-11-04 nel 2025 e 2125-11-04 nel 2076. È una funzionalità per i casi "vicino a oggi" e un trabocchetto per i dati d'archivio.
La soluzione è usare yyyy quando l'input ha quattro cifre ed essere espliciti sulla finestra del secolo quando non le ha:
DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("MM/dd/")
.appendValueReduced(ChronoField.YEAR, 2, 2, 1950) // window starts at 1950
.toFormatter();Se stai gestendo dati legacy con yy, documenta la finestra nel codice. Il default è "pivot mobile intorno all'anno corrente", che non è ciò che vuoi per "tutti i miei dati sono degli anni '80".
Parsing senza impegnarsi su un tipo
DateTimeFormatter.parse(String) restituisce un TemporalAccessor — il fondo della gerarchia dei tipi. Utile quando l'input potrebbe essere un LocalDate o un LocalDateTime:
TemporalAccessor ta = DateTimeFormatter.ISO_DATE_TIME.parseBest(
"2025-11-04T14:30:00",
LocalDateTime::from, // preferred
LocalDate::from); // fallbackparseBest(text, ...queries) prova il metodo from di ogni tipo in ordine e restituisce il primo che ha successo. Il risultato richiede instanceof per essere usato in modo specifico:
if (ta instanceof LocalDateTime ldt) ...
else if (ta instanceof LocalDate ld) ...Per la maggior parte del codice, chiamare direttamente parse(...) del tipo specifico è più semplice. parseBest è per il caso in cui si accettano più forme (una colonna CSV che potrebbe essere una data o una data-ora).
Parsing tollerante con il builder
DateTimeFormatterBuilder consente di assemblare un formatter più permissivo:
DateTimeFormatter lenient = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd[ HH:mm[:ss]]") // optional sections in []
.parseLenient() // accept missing leading zeros etc.
.parseCaseInsensitive() // ignore case on month/day names
.toFormatter(Locale.US);La sintassi con parentesi quadre [...] contrassegna una sezione opzionale — quel pattern esegue il parsing sia di "2025-11-04" (senza ora) che di "2025-11-04 14:30" (con ora). Combinato con parseLenient e parseCaseInsensitive, puoi costruire un formatter che accetta una gamma più ampia di input senza scrivere un parser personalizzato.
Questo è eccessivo per codice che controlla entrambi i lati. Usa il default strict a meno che non stia leggendo input utente o dati legacy.
Instant.parse è strict
Instant.parse("2025-11-04T19:30:00Z") funziona. La Z finale (UTC) è obbligatoria; qualsiasi altro offset (-05:00, +09:00) richiede prima OffsetDateTime.parse o ZonedDateTime.parse, poi .toInstant():
Instant inst = OffsetDateTime.parse("2025-11-04T14:30:00-05:00").toInstant();Questa è la conversione canonica quando un'API esterna ti fornisce stringhe ISO-8601 con offset di fuso orario ma senza zona IANA.
Un esempio pratico: leggi una config, esegui il parsing, reagisci agli input errati
Il programma seguente effettua il parsing di tre stringhe a forma di data da una config sintetica: una data ISO, una data con pattern personalizzato e un istante ISO. Dimostra poi il builder tollerante con una sezione temporale opzionale, l'API parseBest per "sia una data che una data-ora", e la modalità di fallimento quando l'input non corrisponde.
Cosa ricavare dall'esecuzione:
- I tre parser ISO predefiniti hanno accettato esattamente la forma standard:
yyyy-MM-ddperLocalDate, la forma separata daTperLocalDateTime, e la forma terminata conZperInstant. Nessuna flessibilità, nessuna supposizione — ed è questo il punto. Se il tuo input si adatta, non serve alcun formatter; se non si adatta, costruirne uno è questione di una riga. - Il formatter tollerante ha accettato tre forme di input diverse — solo data, data con ora, data con ora e secondi — perché la sezione tra parentesi
[...]è opzionale.parseBest(text, LocalDateTime::from, LocalDate::from)ha scelto il tipo più ricco supportato da ciascun input. Questo è il pattern corretto quando si accettano date inserite dall'utente o da config con precisione variabile. OffsetDateTime.parse(wire).toInstant()è stato il bridge canonico da "un timestamp ISO-8601 con offset" aInstant.Instant.parsestesso accetta solo UTC con suffissoZ; tutto il resto deve passare prima perOffsetDateTime(oZonedDateTime). La conversione è di una riga per ogni direzione.- Il parsing in locale tedesco ha funzionato solo perché il formatter era costruito con
Locale.GERMAN. Il locale predefinito avrebbe rifiutato"November"se la JVM fosse in esecuzione in tedesco (che si aspetta che"November"diLocale.GERMANvenga confrontato con i nomi tedeschi). Fissa sempre il locale al confine del parsing — il formatter da solo non basta; il locale controlla la risoluzione dei nomi di mesi e giorni. - I due blocchi di fallimento hanno entrambi lanciato
DateTimeParseExceptioncon informazioni utili sulla posizione.getErrorIndexindica dove il parser si è fermato;getParsedStringè l'input come lo ha visto il parser. Visualizza questi dati negli errori per l'utente — "impossibile analizzare la data al carattere 5" è enormemente più utile di "data non valida".
Cosa c'è dopo
La formattazione e il parsing chiudono il confine con le stringhe. Il prossimo capitolo, Java Temporal Adjusters, torna al lato dei valori e copre gli adjuster predefiniti (firstDayOfMonth, nextOrSame(MONDAY), ecc.) e come scriverne di propri — utili ogni volta che la data desiderata dipende dalla data che si ha ("il primo martedì dopo il 15").