web-dev-qa-db-fra.com

API Java 8 Time - ZonedDateTime - spécifie le ZoneId par défaut lors de l'analyse

J'essaie d'écrire une méthode générique pour renvoyer un ZonedDateTime donné une date comme String et son format. 

Comment faire en sorte que ZonedDateTime utilise la valeur par défaut ZoneId si elle n'est pas spécifiée dans la date String

Cela peut être fait avec Java.util.Calendar, mais je veux utiliser l'API temporelle Java 8.

Cette question ici utilise un fuseau horaire fixe. Je spécifie le format comme argument. La date et son format sont des arguments String. Plus générique. 

Code et sortie ci-dessous: 

public class DateUtil {
    /** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone.  */
    public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
        //use Java.time from Java 8
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        ZonedDateTime zonedDateTime = ZonedDateTime.parse(date, formatter);
        return zonedDateTime;
    }

    public static void main(String args[]) {
        DateUtil dateUtil = new DateUtil();
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
    }
}

Sortie 

2017-09-14T15:00+05:30
Exception in thread "main" Java.time.format.DateTimeParseException: Text '2017-09-14 15:00:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type Java.time.format.Parsed
    at Java.time.format.DateTimeFormatter.createError(DateTimeFormatter.Java:1920)
    at Java.time.format.DateTimeFormatter.parse(DateTimeFormatter.Java:1855)
    at Java.time.ZonedDateTime.parse(ZonedDateTime.Java:597)
    at com.nam.sfmerchstorefhs.util.DateUtil.parseToZonedDateTime(DateUtil.Java:81)
    at com.nam.sfmerchstorefhs.util.DateUtil.main(DateUtil.Java:97)
Caused by: Java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type Java.time.format.Parsed
    at Java.time.ZonedDateTime.from(ZonedDateTime.Java:565)
    at Java.time.format.Parsed.query(Parsed.Java:226)
    at Java.time.format.DateTimeFormatter.parse(DateTimeFormatter.Java:1851)
    ... 3 more
Caused by: Java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type Java.time.format.Parsed
    at Java.time.ZoneId.from(ZoneId.Java:466)
    at Java.time.ZonedDateTime.from(ZonedDateTime.Java:553)
    ... 5 more
8
RuntimeException

Une ZonedDateTime a besoin d'un fuseau horaire ou d'un décalage pour être construite, et la deuxième entrée ne l'a pas. (Il ne contient qu'une date et une heure).

Vous devez donc vérifier s’il est possible de construire une ZonedDateTime et si ce n’est pas le cas, vous devrez choisir une zone arbitraire (l’entrée n’ayant aucune indication sur le fuseau horaire utilisé, vous devez en choisir une). à utiliser).

Une alternative consiste à essayer d’abord de créer une ZonedDateTime et si ce n’est pas possible, créez ensuite une LocalDateTime et convertissez-la en fuseau horaire:

public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
    // use Java.time from Java 8
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    ZonedDateTime zonedDateTime = null;
    try {
        zonedDateTime = ZonedDateTime.parse(date, formatter);
    } catch (DateTimeException e) {
        // couldn't parse to a ZoneDateTime, try LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(date, formatter);

        // convert to a timezone
        zonedDateTime = dt.atZone(ZoneId.systemDefault());
    }
    return zonedDateTime;
}

Dans le code ci-dessus, j'utilise ZoneId.systemDefault(), qui obtient le fuseau horaire par défaut de la JVM}, mais ceci peut être modifié sans préavis, même au moment de l'exécution , il est donc préférable de toujours l'indiquer explicitement. vous utilisez.

L'API utilise Les noms des fuseaux horaires IANA (toujours au format Region/City, tel que America/Sao_Paulo ou Europe/Berlin) . Évitez d'utiliser les abréviations à 3 lettres (telles que CST ou PST) car elles sont ambiguës et non standard .

Vous pouvez obtenir une liste des fuseaux horaires disponibles (et choisir celui qui convient le mieux à votre système) en appelant ZoneId.getAvailableZoneIds().

Si vous souhaitez utiliser un fuseau horaire spécifique, utilisez simplement ZoneId.of("America/New_York") (ou tout autre nom valide renvoyé par ZoneId.getAvailableZoneIds(), New York n’est qu’un exemple) au lieu de ZoneId.systemDefault().


Une autre alternative consiste à utiliser la méthode parseBest() , qui tente de créer un objet de date approprié (à l'aide d'une liste de TemporalQuery ') jusqu'à ce qu'elle crée le type souhaité:

public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);

    // try to create a ZonedDateTime, if it fails, try LocalDateTime
    TemporalAccessor parsed = formatter.parseBest(date, ZonedDateTime::from, LocalDateTime::from);

    // if it's a ZonedDateTime, return it
    if (parsed instanceof ZonedDateTime) {
        return (ZonedDateTime) parsed;
    }
    if (parsed instanceof LocalDateTime) {
        // convert LocalDateTime to JVM default timezone
        LocalDateTime dt = (LocalDateTime) parsed;
        return dt.atZone(ZoneId.systemDefault());
    }

    // if it can't be parsed, return null or throw exception?
    return null;
}

Dans ce cas, je viens d'utiliser ZonedDateTime::from et LocalDateTime::from. Le formateur essaiera donc de créer d'abord une ZonedDateTime et si ce n'est pas possible, il essaiera de créer une LocalDateTime.

Ensuite, je vérifie le type renvoyé et effectue les actions en conséquence ... Vous pouvez ajouter autant de types que vous le souhaitez (tous les types principaux, tels que LocalDate, LocalTime, OffsetDateTime et ainsi de suite, ont une méthode from qui fonctionne avec parseBest - vous pouvez aussi créer votre propre TemporalQuery si vous le souhaitez, mais je pense que les méthodes intégrées sont suffisantes dans ce cas).


Heure d'été

Lors de la conversion d'une LocalDateTime en une ZonedDateTime à l'aide de la méthode atZone(), il existe des cas délicats à propos de Heure d'été (DST).

Je vais utiliser le fuseau horaire dans lequel je vis (America/Sao_Paulo), mais cela peut se produire à tout fuseau horaire avec DST.

À São Paulo, l'heure d'été a commencé le 16 octobre.th 2016: à minuit, les horloges sont décalées d'une heure en avant de minuit à 1 heure du matin (et le décalage passe de -03:00 à -02:00). Ainsi, toutes les heures locales entre 00h00 et 00h59 n'existaient pas dans ce fuseau horaire (vous pouvez également penser que les horloges ont changé de 23: 59: 59.999999999 directement à 01:00). Si je crée une date locale dans cet intervalle, elle est ajustée au prochain instant valide:

ZoneId zone = ZoneId.of("America/Sao_Paulo");

// October 16th 2016 at midnight, DST started in Sao Paulo
LocalDateTime d = LocalDateTime.of(2016, 10, 16, 0, 0, 0, 0);
ZonedDateTime z = d.atZone(zone);
System.out.println(z);// adjusted to 2017-10-15T01:00-02:00[America/Sao_Paulo]

À la fin de l'heure d'été: le 19 févrierth 2017 à minuit, les horloges sont décalées retour 1 heure, de minuit à 23 PM de 18th (et le décalage passe de -02:00 à -03:00). Donc, toutes les heures locales de 23h00 à 23h59 existaient deux fois (dans les deux décalages: -03:00 et -02:00), et vous devez choisir celle que vous voulez . Par défaut, il utilise le décalage avant DST se termine, mais vous pouvez utiliser la méthode withLaterOffsetAtOverlap() pour obtenir le décalage à la fin de DST:

// February 19th 2017 at midnight, DST ends in Sao Paulo
// local times from 23:00 to 23:59 at 18th exist twice
LocalDateTime d = LocalDateTime.of(2017, 2, 18, 23, 0, 0, 0);
// by default, it gets the offset before DST ends
ZonedDateTime beforeDST = d.atZone(zone);
System.out.println(beforeDST); // before DST end: 2018-02-17T23:00-02:00[America/Sao_Paulo]

// get the offset after DST ends
ZonedDateTime afterDST = beforeDST.withLaterOffsetAtOverlap();
System.out.println(afterDST); // after DST end: 2018-02-17T23:00-03:00[America/Sao_Paulo]

Notez que les dates avant et après la fin de l'heure d'été ont des décalages différents (-02:00 et -03:00). Si vous travaillez avec un fuseau horaire comportant l'heure d'été, gardez à l'esprit que ces problèmes peuvent se produire.

9
user7605325

La bibliothèque Java.time ne contient pratiquement aucun paramètre par défaut, ce qui est généralement une bonne chose - vous voyez ce que vous obtenez, point à point.

Je suggérerais que si votre chaîne de date ne contient pas Zone - c'est une LocalDateTime et ne peut pas être une ZonedDateTime, ce qui est le sens de l'exception que vous obtenez (même si verbiage a souffert en raison d'une structure de code trop souple).

Ma suggestion principale est d'analyser la date et l'heure locales si vous savez que ce modèle ne contient aucune information de zone.

Cependant, si vous en avez vraiment besoin, voici une autre façon de faire ce que vous voulez (une solution alternative qui n'utilise pas d'exceptions pour contrôler le flux):

TemporalAccessor parsed = f.parse(string);
if (parsed.query(TemporalQueries.zone()) == null) {
  parsed = f.withZone(ZoneId.systemDefault()).parse(string);
}
return ZonedDateTime.from(parsed);

Ici, nous utilisons un résultat d'analyse intermédiaire pour déterminer si la chaîne contient des informations de zone. Sinon, nous analysons à nouveau (en utilisant la même chaîne, mais un analyseur d'imprimante différent) afin qu'il contienne une zone cette fois.

Vous pouvez également créer cette classe, ce qui vous évitera d’analyser la deuxième fois et devrait vous autorise à analyser la date et l’heure zonées, en supposant que tous les autres champs sont présents:

class TemporalWithZone implements TemporalAccessor {
  private final ZoneId zone;
  private final TemporalAccessor delegate;
  public TemporalWithZone(TemporalAccessor delegate, ZoneId zone) {
    this.delegate = requireNonNull(delegate);
    this.zone = requireNonNull(zone);
  }

  <delegate methods: isSupported(TemporalField), range(TemporalField), getLong(TemporalField)>

  public <R> R query(TemporalQuery<R> query) {
    if (query == TemporalQueries.zone() || query == TemporalQueries.zoneId()) {
      return (R) zone;
    }
    return delegate.query(query);
  }
}
3
M. Prokhorov

Selon l'implémentation ZonedDateTime de Java 8, vous ne pouvez pas analyser la date sans zone dans ZonedDateTime. 

Pour résoudre un problème donné, vous devez obligatoirement mettre try catch au cas où une exception considérerait le fuseau horaire par défaut. 

Veuillez trouver le programme révisé ci-dessous:

public class DateUtil {
     /** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone.  */
    public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
        //use Java.time from Java 8
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        ZonedDateTime zonedDateTime = null;
        try {
            zonedDateTime = ZonedDateTime.parse(date, formatter);
        } catch (DateTimeException e) {
            // If date doesn't contains Zone then parse with LocalDateTime 
            LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
            zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
        }
        return zonedDateTime;
    }

    public static void main(String args[]) {
        DateUtil dateUtil = new DateUtil();
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
    }
}

Vous faites référence à/ http://www.codenuclear.com/Java-8-date-time-intro pour plus de détails sur les fonctionnalités Java à venir

3
viviek

Vous pouvez simplement ajouter une valeur par défaut dans une DateTimeFormatterBuilder s'il n'y a pas de OFFSET_SECOND:


EDIT: Pour obtenir la ZoneOffset par défaut du système, vous devez appliquer la ZoneRules à la Instant actuelle. Le résultat ressemble à ceci:

class DateUtil {
  public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
    ZoneOffset defaultOffset =  ZoneId.systemDefault().getRules().getOffset(localDateTime);
    DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
            .append(formatter)
            .parseDefaulting(ChronoField.OFFSET_SECONDS, defaultOffset.getTotalSeconds())
            .toFormatter();
    return ZonedDateTime.parse(date, dateTimeFormatter);
  }
}

Sortie:

2017-09-14T15:00+05:30
2017-09-14T15:00+02:00
2
Flown

La ZoneId peut être spécifiée en utilisant la méthode withZone dans la DateTimeFormatter:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat).withZone("+0530");
1
RSloeserwij