2015-01-21 13 views
11

Utilizzo l'API DateTime JSR 310 * nella mia applicazione e ho bisogno di analizzare e formattare i tempi delle date militari (noti come DTG o "gruppo data ora").Fusi orari militari utilizzando JSR 310 (API DateTime)

Il formato sto parsing si presenta così (con DateTimeFormatter):

"ddHHmm'Z' MMM yy" // (ie. "312359Z DEC 14", for new years eve 2014) 

Questo formato è abbastanza facile da analizzare come descritto sopra. Il problema sorge quando le date contengono un fuso orario diverso da "Z" (fuso orario Zulu, uguale a UTC/GMT), ad esempio "A" (Alfa, UTC + 1: 00) o "B" (Bravo, UTC + 2:00). Vedere Military time zones per l'elenco completo.

Come posso analizzare questi fusi orari? O in altre parole, cosa posso inserire nel formato sopra diverso dalla "Z" letterale per far sì che analizzi correttamente tutte le zone? Ho provato a utilizzare "ddHHmmX MMM yy", "ddHHmmZ MMM yy" e "ddHHmmVV MMM yy", ma nessuno di essi funziona (tutto getterà DateTimeParseException: Text '312359A DEC 14' could not be parsed at index 6 per l'esempio sopra, durante l'analisi). L'utilizzo di un singolo V nel formato non è consentito (IllegalArgumentException quando si tenta di creare un'istanza DateTimeFormatter).

Modifica: Sembra che il simbolo z avrebbe potuto funzionare, se non fosse stato per il problema di seguito.

Vorrei anche ricordare che ho creato uno ZoneRulesProvider con tutte le zone denominate e l'offset corretto. Ho verificato che questi sono registrati correttamente utilizzando il meccanismo SPI e il mio metodo provideZoneIds() viene richiamato come previsto. Ancora non si analizzerà. Come problema collaterale (Modifica: questo sembra essere il problema principale), l'ID del fuso orario a carattere singolo (o "regioni") diverso da "Z" non è consentito dall'API.

Ad esempio:

ZoneId alpha = ZoneId.of("A"); // boom 

getterà DateTimeException: Invalid zone: A (senza nemmeno l'accesso al mio fornitore di regole per vedere se esiste).

Si tratta di una svista nell'API? O sto facendo qualcosa di sbagliato?


*) A dire il vero, sto usando Java 7 e ThreeTen Backport, ma non credo che le questioni a questa domanda.

PS: mia soluzione attuale è quella di utilizzare 25 diversi DateTimeFormatter s con letterale zona id (. Es "ddHHmm'A' MMM yy", "ddHHmm'B' MMM yy", ecc), utilizzare un RegExp per estrarre la zona id, e delegando al formattatore corretto in base alla zona. Gli id ​​di zona nel provider sono denominati "Alpha", "Bravo", ecc. Per consentire a ZoneId.of(...) di trovare le zone. Funziona. Ma non è molto elegante, e spero che ci sia una soluzione migliore.

+1

Hai provato 'V per la zona-id nella stringa di formato (vale a dire ' "ddHHmmV MMM yy"')? AFAIK "Z" significa solo carattere "Z". https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html –

+1

Sì, ho capito che ''Z'' è un valore letterale e non viene interpretato dal parser. 'Z' (non quotato) è un simbolo (ma non funziona). E sì, ho provato 'V'. Tuttavia, dà un errore diverso ... 'IllegalArgumentException: Il conteggio delle lettere del modello deve essere 2: V' quando si tenta di creare il formattatore. Usando 'VV', torno all'eccezione" ... non può essere analizzata all'indice 6 "durante l'analisi. Grazie comunque! :-) – haraldK

+0

@PavelHoral In realtà, sembra che il simbolo "corretto" sia 'z' (in minuscolo) ... Sembra che analizzi correttamente la zona, ma ovviamente esplode perché la zona a singola lettera non è consentita ..: -P "' ... non è stato possibile analizzare: zona non valida: A' ". Aggiornerò la domanda ... – haraldK

risposta

8

In java.time, il ZoneId è limitato ad essere 2 o più caratteri. Ironia della sorte, si trattava di riservare spazio per consentire l'aggiunta degli ID militari in una futura versione di JDK, se si fosse dimostrata fortemente richiesta. In quanto tale, purtroppo il tuo provider non funzionerà, e non c'è modo di creare le istanze ZoneId che desideri con quei nomi.

Il problema di analisi è solubile quando si considera di lavorare con ZoneOffset anziché ZoneId (e dato che le zone militari sono offset fissi, è un buon modo per esaminare il problema).

La chiave è il metodo DateTimeFormatterBuilder.appendText(TemporalField, Map) che consente di formattare e analizzare un campo numerico come testo, utilizzando il testo desiderato. E ZoneOffset è un campo numerico (il valore è il numero totale di secondi nell'offset).

In questo esempio, ho impostato il mapping per Z, A e B, ma è necessario aggiungerli tutti. In caso contrario, il codice è piuttosto semplice, impostando un formattatore in grado di stampare e analizzare il tempo militare (utilizzare OffsetDateTime per data e ora).

Map<Long, String> map = ImmutableMap.of(0L, "Z", 3600L, "A", 7200L, "B"); 
DateTimeFormatter f = new DateTimeFormatterBuilder() 
    .appendPattern("HH:mm") 
    .appendText(ChronoField.OFFSET_SECONDS, map) 
    .toFormatter(); 
System.out.println(OffsetTime.now().format(f)); 
System.out.println(OffsetTime.parse("11:30A", f)); 
+0

Grazie! Sicuramente una soluzione molto più pulita e più elegante! Testato e funziona molto bene con il mio formato. – haraldK

+0

PS: Attualmente sto convertendo tutte le date/orari internamente in Zulu e uso 'ZonedDateTime' piuttosto che' OffsetDateTime'. Sono abituato a Joda, ma questo è il mio primo progetto con JSR 310. C'è una buona ragione per usare ODT su ZDT (mi rendo conto che volevi unire queste classi ad un certo punto ... :-)? – haraldK

+2

ODT è più semplice e non può avere problemi di ora legale. Anche il formato standard ISO08601 della rete. Altrimenti le classi sono molto simili. Usa quello che funziona meglio per te. – JodaStephen

3

Il comportamento di java.time-package (JSR-310) in relazione al supporto di id di zona è come specificato - vedere javadoc. Citazione esplicita della sezione pertinente (altri ID sono considerati come offset-id nel formato "Z", "+ hh: mm", "-hh: mm" o "UTC + hh: mm" ecc.):

Un ID regione a base devono essere di due o più caratteri

la condizione di avere almeno due caratteri è implementata anche nel codice sorgente della classe ZoneRegion prima del caricamento dei dati fuso orario partenze:

/** 
* Checks that the given string is a legal ZondId name. 
* 
* @param zoneId the time-zone ID, not null 
* @throws DateTimeException if the ID format is invalid 
*/ 
private static void checkName(String zoneId) { 
    int n = zoneId.length(); 
    if (n < 2) { 
     throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId); 
    } 
    for (int i = 0; i < n; i++) { 
     char c = zoneId.charAt(i); 
     if (c >= 'a' && c <= 'z') continue; 
     if (c >= 'A' && c <= 'Z') continue; 
     if (c == '/' && i != 0) continue; 
     if (c >= '0' && c <= '9' && i != 0) continue; 
     if (c == '~' && i != 0) continue; 
     if (c == '.' && i != 0) continue; 
     if (c == '_' && i != 0) continue; 
     if (c == '+' && i != 0) continue; 
     if (c == '-' && i != 0) continue; 
     throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId); 
    } 
} 

Questo spiega il motivo per cui non è possibile con JSR-310/Threeten di scrivere un'espressione come ZoneId.of("A"). La lettera Z funziona perché è specificata anche in ISO-8601 come in JSR-310 per rappresentare l'offset zero.

La soluzione che avete trovato va bene nell'ambito di JSR-310 che NON supporta i fusi orari militari. Di conseguenza non troverete alcun supporto per il formato (studiare solo la classe DateTimeFormatterBuilder - ogni elaborazione dei simboli del modello di formato viene instradata al costruttore). L'unica idea vaga che ho avuto è stata quella di implementare uno specialista TemporalField che rappresenta un offset di fuso orario militare. Ma l'implementazione è (se possibile del tutto) con certezza più complessa della soluzione.

Un'altra soluzione più adatta è la pre-elaborazione delle stringhe. Dal momento che si lavora con un formato fisso in attesa della lettera militari sempre nella stessa posizione in ingresso, si può semplicemente fare questo:

String input = "312359A Dec 14"; 
String offset = ""; 

switch (input.charAt(6)) { 
    case 'A': 
    offset = "+01:00"; 
    break; 
    case 'B': 
    offset = "+02:00"; 
    break; 
    //... 
    case 'Z': 
    offset = "Z"; 
    break; 
    default: 
    throw new ParseException("Wrong military timezone: " + input, 6); 
} 
input = input.substring(0, 6) + offset + input.substring(7); 
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ddHHmmVV MMM yy", Locale.ENGLISH); 
ZonedDateTime odt = ZonedDateTime.parse(input, formatter); 
System.out.println(odt); 
// output: 2014-12-31T23:59+01:00 

Note:

  • ho usato "Dec" invece di "DEC", altrimenti il ​​parser si lamenterà. Se il tuo input ha davvero lettere maiuscole, puoi utilizzare il metodo builder parseCaseInsensitive().

  • L'altra risposta utilizzando il campo OffsetSeconds è la risposta migliore per quanto riguarda il problema di analisi e ha anche ottenuto il mio upvote (aver trascurato questa caratteristica). Non è migliore perché mette l'utente in difficoltà a definire una mappatura dalle lettere delle zone militari agli offset - come il mio suggerimento con la preelaborazione delle stringhe. Ma è meglio perché consente di utilizzare i metodi builder optionalStart() e optionalEnd() in modo che sia possibile gestire le lettere di fuso orario facoltative A, B, .... Vedere anche il commento dell'OP relativo agli ID zona facoltativi.

+0

Grazie per la conferma, penso che contrassegnerò questa come la risposta corretta, poiché ciò che voglio fare sembra impossibile data la specifica/implementazione corrente (ho archiviato un problema con il progetto threetenbp, in attesa di feedback). – haraldK

+0

Re. preelaborazione delle stringhe e distinzione tra maiuscole e minuscole, sì, ne sono consapevole. Il formato effettivo che utilizzo per l'analisi è più complesso di quello che ho mostrato. L'id di zona è facoltativo (se omesso, Z è assunto) e anche gli spazi sono opzionali (che significa 'charAt (6)' potrebbe essere la A in Apr ...). Continuerò alla mia soluzione basata su espressioni regolari. Cercherò di usare la regexp replacement, potrebbe funzionare! E il 'ZoneRulesProvider' che ho creato è forse ridondante, potrei semplicemente usare' ZoneOffset's direttamente. Comunque, grazie per una risposta buona e completa! :-) – haraldK