Comment gérer les fuseaux horaires du calendrier en utilisant Java?

92

J'ai une valeur d'horodatage qui provient de mon application. L'utilisateur peut se trouver dans n'importe quelle zone horaire locale.

Puisque cette date est utilisée pour un WebService qui suppose que l'heure donnée est toujours en GMT, j'ai besoin de convertir le paramètre de l'utilisateur de disons (EST) à (GMT). Voici le kicker: l'utilisateur est inconscient de son TZ. Il entre la date de création qu'il souhaite envoyer au WS, donc j'ai besoin de:

L'utilisateur entre: 5/1/2008 6:12 PM (EST)
Le paramètre du WS doit être : 5/1/2008 6:12 PM (GMT)

Je sais que les horodatages sont toujours censés être en GMT par défaut, mais lors de l'envoi du paramètre, même si j'ai créé mon calendrier à partir du TS (qui est censé être en GMT), les heures sont toujours éteintes sauf si l'utilisateur est en GMT. Qu'est-ce que je rate?

Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
...
private static java.util.Calendar convertTimestampToJavaCalendar(Timestamp ts_) {
  java.util.Calendar cal = java.util.Calendar.getInstance(
      GMT_TIMEZONE, EN_US_LOCALE);
  cal.setTimeInMillis(ts_.getTime());
  return cal;
}

Avec le code précédent, voici ce que j'obtiens en conséquence (format court pour une lecture facile):

[1er mai 2008 23h12]

Jorge Valois
la source
2
Pourquoi ne changez-vous que le fuseau horaire et ne convertissez-vous pas la date / l'heure en même temps?
Spencer Kormos
1
C'est vraiment pénible de prendre une date Java qui est dans un fuseau horaire et d'obtenir cette date dans un autre fuseau horaire. IE, prenez 5PM EDT et obtenez 17PM PDT.
mtyson

Réponses:

62
public static Calendar convertToGmt(Calendar cal) {

    Date date = cal.getTime();
    TimeZone tz = cal.getTimeZone();

    log.debug("input calendar has date [" + date + "]");

    //Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT 
    long msFromEpochGmt = date.getTime();

    //gives you the current offset in ms from GMT at the current date
    int offsetFromUTC = tz.getOffset(msFromEpochGmt);
    log.debug("offset is " + offsetFromUTC);

    //create a new calendar in GMT timezone, set to this date and add the offset
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.setTime(date);
    gmtCal.add(Calendar.MILLISECOND, offsetFromUTC);

    log.debug("Created GMT cal with date [" + gmtCal.getTime() + "]");

    return gmtCal;
}

Voici la sortie si je passe l'heure actuelle ("12:09:05 EDT" de Calendar.getInstance()) dans:

DEBUG - le calendrier d'entrée a la date [Thu Oct 23 12:09:05 EDT 2008]
DEBUG - offset is -14400000
DEBUG - Créé GMT cal avec la date [Thu Oct 23 08:09:05 EDT 2008]

12:09:05 GMT est 8:09:05 EDT.

La partie déroutante ici est que Calendar.getTime()vous renvoie un Datedans votre fuseau horaire actuel, et aussi qu'il n'y a pas de méthode pour modifier le fuseau horaire d'un calendrier et faire rouler la date sous-jacente également. En fonction du type de paramètre utilisé par votre service Web, vous souhaiterez peut-être simplement avoir l'accord WS en millisecondes à partir de l'époque.

mat b
la source
11
Ne devriez-vous pas soustraire offsetFromUTCau lieu de l'ajouter? En utilisant votre exemple, si 12:09 GMT est 8:09 EDT (ce qui est vrai), et que l'utilisateur entre "12:09 EDT", l'algorithme devrait afficher "16:09 GMT", à mon avis.
DzinX
29

Merci à tous d'avoir répondu. Après une enquête plus approfondie, j'ai trouvé la bonne réponse. Comme mentionné par Skip Head, l'horodatage que je recevais de mon application était ajusté à la zone horaire de l'utilisateur. Donc, si l'utilisateur entrait 18h12 (EST), j'obtiendrais 14h12 (GMT). Ce dont j'avais besoin était un moyen d'annuler la conversion afin que l'heure saisie par l'utilisateur soit l'heure que j'ai envoyée à la requête du serveur Web. Voici comment j'ai accompli cela:

// Get TimeZone of user
TimeZone currentTimeZone = sc_.getTimeZone();
Calendar currentDt = new GregorianCalendar(currentTimeZone, EN_US_LOCALE);
// Get the Offset from GMT taking DST into account
int gmtOffset = currentTimeZone.getOffset(
    currentDt.get(Calendar.ERA), 
    currentDt.get(Calendar.YEAR), 
    currentDt.get(Calendar.MONTH), 
    currentDt.get(Calendar.DAY_OF_MONTH), 
    currentDt.get(Calendar.DAY_OF_WEEK), 
    currentDt.get(Calendar.MILLISECOND));
// convert to hours
gmtOffset = gmtOffset / (60*60*1000);
System.out.println("Current User's TimeZone: " + currentTimeZone.getID());
System.out.println("Current Offset from GMT (in hrs):" + gmtOffset);
// Get TS from User Input
Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
System.out.println("TS from ACP: " + issuedDate);
// Set TS into Calendar
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
// Adjust for GMT (note the offset negation)
issueDate.add(Calendar.HOUR_OF_DAY, -gmtOffset);
System.out.println("Calendar Date converted from TS using GMT and US_EN Locale: "
    + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
    .format(issueDate.getTime()));

La sortie du code est: (L'utilisateur a saisi 5/1/2008 6:12 PM (EST)


Fuseau horaire de l' utilisateur actuel: EST Décalage actuel par rapport à GMT (en heures): - 4 (normalement -5, sauf est ajusté à l'heure d'été)
TS de ACP: 2008-05-01 14: 12: 00.0
Date calendaire convertie à partir de TS en utilisant GMT et US_EN Locale : 01/05/08 à 18h12 (GMT)

Jorge Valois
la source
20

Vous dites que la date est utilisée en relation avec les services Web, donc je suppose qu'elle est sérialisée dans une chaîne à un moment donné.

Si tel est le cas, vous devriez jeter un œil à la méthode setTimeZone de la classe DateFormat. Cela détermine le fuseau horaire qui sera utilisé lors de l'impression de l'horodatage.

Un exemple simple:

SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));

Calendar cal = Calendar.getInstance();
String timestamp = formatter.format(cal.getTime());
Henrik Aasted Sørensen
la source
4
N'a pas publié SDF Avait son propre fuseau horaire, je me demandais pourquoi changer la zone horaire du calendrier semble n'avoir aucun effet!
Chris.Jenkins
TimeZone.getTimeZone ("UTC") n'est pas valide car UTC n'est pas dans AvailableIDs () ... Dieu sait pourquoi
childno͡.de
TimeZone.getTimeZone ("UTC") est disponible sur ma machine. Est-il dépendant de JVM?
CodeClimber
2
Super idée avec la méthode setTimeZone (). Vous m'avez sauvé beaucoup de maux de tête, merci beaucoup!
Bogdan Zurac
1
Juste ce dont j'avais besoin! Vous pouvez définir le fuseau horaire du serveur dans le formateur, puis lorsque vous le convertissez au format de calendrier, vous n'avez rien à craindre. Meilleure méthode de loin! pour getTimeZone, vous devrez peut-être utiliser le format Ex: "GMT-4: 00" pour ETD
Michael Kern
12

Vous pouvez le résoudre avec Joda Time :

Date utcDate = new Date(timezoneFrom.convertLocalToUTC(date.getTime(), false));
Date localDate = new Date(timezoneTo.convertUTCToLocal(utcDate.getTime()));

Java 8:

LocalDateTime localDateTime = LocalDateTime.parse("2007-12-03T10:15:30");
ZonedDateTime fromDateTime = localDateTime.atZone(
    ZoneId.of("America/Toronto"));
ZonedDateTime toDateTime = fromDateTime.withZoneSameInstant(
    ZoneId.of("Canada/Newfoundland"));
Vitalii Fedorenko
la source
attention cependant si vous utilisez hibernate 4, qui n'est pas directement compatible sans dépendance latérale et configuration supplémentaire. Cependant, c'était l'approche la plus rapide et la plus simple à utiliser pour la 3ème version.
Aubergine
8

Il semble que votre TimeStamp soit réglé sur le fuseau horaire du système d'origine.

Ceci est obsolète, mais cela devrait fonctionner:

cal.setTimeInMillis(ts_.getTime() - ts_.getTimezoneOffset());

La méthode non obsolète consiste à utiliser

Calendar.get(Calendar.ZONE_OFFSET) + Calendar.get(Calendar.DST_OFFSET)) / (60 * 1000)

mais cela devrait être fait du côté client, car ce système sait dans quel fuseau horaire il se trouve.

Passer la tête
la source
7

Méthode de conversion d'une timeZone à une autre (cela fonctionne probablement :)).

/**
 * Adapt calendar to client time zone.
 * @param calendar - adapting calendar
 * @param timeZone - client time zone
 * @return adapt calendar to client time zone
 */
public static Calendar convertCalendar(final Calendar calendar, final TimeZone timeZone) {
    Calendar ret = new GregorianCalendar(timeZone);
    ret.setTimeInMillis(calendar.getTimeInMillis() +
            timeZone.getOffset(calendar.getTimeInMillis()) -
            TimeZone.getDefault().getOffset(calendar.getTimeInMillis()));
    ret.getTime();
    return ret;
}
Helpa
la source
6

Les objets Date et Horodatage sont inconscients du fuseau horaire: ils représentent un certain nombre de secondes depuis l'époque, sans s'engager dans une interprétation particulière de cet instant en heures et en jours. Les fuseaux horaires n'entrent dans l'image que dans GregorianCalendar (pas directement nécessaire pour cette tâche) et SimpleDateFormat , qui nécessitent un décalage de fuseau horaire pour convertir entre des champs séparés et des valeurs de date (ou longues ).

Le problème de l'OP est juste au début de son traitement: l'utilisateur saisit les heures, qui sont ambiguës, et elles sont interprétées dans le fuseau horaire local, non GMT; à ce stade, la valeur est "6:12 EST" , qui peut être facilement imprimée comme "11.12 GMT" ou tout autre fuseau horaire mais ne changera jamais en "6.12 GMT" .

Il n'y a aucun moyen de rendre le SimpleDateFormat qui analyse "06:12" comme "HH: MM" (par défaut au fuseau horaire local) par défaut à UTC; SimpleDateFormat est un peu trop intelligent pour son propre bien.

Cependant, vous pouvez convaincre n'importe quelle instance SimpleDateFormat d'utiliser le bon fuseau horaire si vous le mettez explicitement dans l'entrée: ajoutez simplement une chaîne fixe à la réception (et correctement validée) "06:12" pour analyser "06:12 GMT" comme "HH: MM z" .

Il n'est pas nécessaire de définir explicitement les champs GregorianCalendar ou de récupérer et d'utiliser les décalages de fuseau horaire et d'heure d'été.

Le vrai problème est de séparer les entrées par défaut sur le fuseau horaire local, les entrées par défaut sur UTC et les entrées qui nécessitent vraiment une indication de fuseau horaire explicite.

utilisateur412090
la source
4

Quelque chose qui a fonctionné pour moi dans le passé était de déterminer le décalage (en millisecondes) entre le fuseau horaire de l'utilisateur et l'heure GMT. Une fois que vous avez le décalage, vous pouvez simplement ajouter / soustraire (en fonction de la façon dont la conversion se déroule) pour obtenir l'heure appropriée dans l'un ou l'autre fuseau horaire. J'accomplirais généralement cela en définissant le champ millisecondes d'un objet Calendar, mais je suis sûr que vous pouvez facilement l'appliquer à un objet d'horodatage. Voici le code que j'utilise pour obtenir le décalage

int offset = TimeZone.getTimeZone(timezoneId).getRawOffset();

timezoneId est l'identifiant du fuseau horaire de l'utilisateur (tel que EST).

Adam
la source
9
En utilisant le décalage brut, vous ignorez l'heure d'été.
Werner Lehmann
1
Exactement, cette méthode donnera des résultats incorrects pendant une demi-année. La pire forme d'erreur.
Danubian Sailor
1

java.time

L'approche moderne utilise les classes java.time qui ont remplacé les anciennes classes de date-heure gênantes fournies avec les versions les plus anciennes de Java.

La java.sql.Timestampclasse est l'une de ces classes héritées. Ne sont plus nécessaires. À la place, utilisez Instantou d'autres classes java.time directement avec votre base de données en utilisant JDBC 4.2 et versions ultérieures.

La Instantclasse représente un moment sur la chronologie en UTC avec une résolution de nanosecondes (jusqu'à neuf (9) chiffres d'une fraction décimale).

Instant instant = myResultSet.getObject(  , Instant.class ) ; 

Si vous devez interagir avec un existant Timestamp, convertissez immédiatement en java.time via les nouvelles méthodes de conversion ajoutées aux anciennes classes.

Instant instant = myTimestamp.toInstant() ;

Pour ajuster dans un autre fuseau horaire, spécifiez le fuseau horaire en tant ZoneIdqu'objet. Indiquez un nom de fuseau horaire approprié dans le format continent/region, tels que America/Montreal, Africa/Casablancaou Pacific/Auckland. N'utilisez jamais les pseudo-zones de 3 à 4 lettres telles que ESTou ISTcar ce ne sont pas de vrais fuseaux horaires, non standardisés et même pas uniques (!).

ZoneId z = ZoneId.of( "America/Montreal" ) ;

Appliquer au Instantpour produire un ZonedDateTimeobjet.

ZonedDateTime zdt = instant.atZone( z ) ;

Pour générer une chaîne à afficher pour l'utilisateur, recherchez Stack Overflow pour DateTimeFormattertrouver de nombreuses discussions et exemples.

Votre question consiste vraiment à aller dans l'autre sens, de la saisie des données utilisateur aux objets date-heure. Il est généralement préférable de diviser votre saisie de données en deux parties, une date et une heure.

LocalDate ld = LocalDate.parse( dateInput , DateTimeFormatter.ofPattern( "M/d/uuuu" , Locale.US ) ) ;
LocalTime lt = LocalTime.parse( timeInput , DateTimeFormatter.ofPattern( "H:m a" , Locale.US ) ) ;

Votre question n'est pas claire. Voulez-vous interpréter la date et l'heure entrées par l'utilisateur comme étant en UTC? Ou dans un autre fuseau horaire?

Si vous vouliez dire UTC, créez un OffsetDateTimeavec un décalage en utilisant la constante pour UTC, ZoneOffset.UTC.

OffsetDateTime odt = OffsetDateTime.of( ld , lt , ZoneOffset.UTC ) ;

Si vous vouliez dire un autre fuseau horaire, combinez-le avec un objet de fuseau horaire, a ZoneId. Mais quel fuseau horaire? Vous pouvez détecter un fuseau horaire par défaut. Ou, si critique, vous devez confirmer avec l'utilisateur pour être certain de son intention.

ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

Pour obtenir un objet plus simple qui est toujours en UTC par définition, extrayez un fichier Instant.

Instant instant = odt.toInstant() ;

…ou…

Instant instant = zdt.toInstant() ; 

Envoyez à votre base de données.

myPreparedStatement.setObject(  , instant ) ;

À propos de java.time

Le framework java.time est intégré à Java 8 et versions ultérieures. Ces classes supplantent les anciens gênants hérités des classes date-heure tels que java.util.Date, Calendar, et SimpleDateFormat.

Le projet Joda-Time , désormais en mode maintenance , conseille la migration vers les classes java.time .

Pour en savoir plus, consultez le didacticiel Oracle . Et recherchez Stack Overflow pour de nombreux exemples et explications. La spécification est JSR 310 .

Où obtenir les classes java.time?

  • Java SE 8 , Java SE 9 et versions ultérieures
    • Intégré.
    • Partie de l'API Java standard avec une implémentation groupée.
    • Java 9 ajoute quelques fonctionnalités et corrections mineures.
  • Java SE 6 et Java SE 7
    • Une grande partie de la fonctionnalité java.time est rétroportée vers Java 6 et 7 dans ThreeTen-Backport .
  • Android

Le projet ThreeTen-Extra étend java.time avec des classes supplémentaires. Ce projet est un terrain d'essai pour d'éventuels futurs ajouts à java.time. Vous trouverez peut - être des classes utiles ici, comme Interval, YearWeek, YearQuarteret plus .

Basil Bourque
la source