web-dev-qa-db-fra.com

Comment gérer les fuseaux horaires du calendrier avec Java?

J'ai une valeur d'horodatage qui provient de mon application. L'utilisateur peut être dans n'importe quel fuseau horaire local.

Comme cette date est utilisée pour un WebService qui suppose que l'heure indiquée est toujours en GMT, il me faut convertir le paramètre de l'utilisateur de say (EST) à (GMT). Voici le kicker: L'utilisateur est inconscient de sa TZ. Il entre la date de création qu'il souhaite envoyer sur le serveur Web. Ce dont j'ai besoin, c'est:

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 supposés être à l'heure 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 à l'heure GMT), les heures sont toujours désactivées, sauf si l'utilisateur est à l'heure 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 cela donne (format court pour faciliter la lecture):

[1 mai 2008 23h12]

91
Jorge Valois

Merci à tous d'avoir répondu. Après une enquête plus poussée, je suis arrivé à la bonne réponse. Comme mentionné par Skip Head, l'horodatage que je recevais de mon application était en cours de réglage pour le fuseau horaire de l'utilisateur. Donc, si l'utilisateur saisissait 6:12 PM (EST), j'obtiendrais 2:12 PM (GMT). Il me fallait un moyen pour annuler le La conversion de sorte que le temps entré par l'utilisateur soit le temps que j'ai envoyé à la demande WebServer. Voici comment j'ai accompli ceci:

// 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: (Entré par l'utilisateur le 01/05/2008 18:12 (EST)

Fuseau horaire de l'utilisateur actuel: EST
Décalage actuel par rapport à l'heure GMT (en heures): - 4 (normalement -5, sauf que l'heure d'été est ajustée)
TS from ACP: 2008-05-01 14: 12: 00.0
Date de conversion de la date de calendrier à partir de TS à l'aide de GMT et US_EN Paramètres régionaux: 5/1/08 6:12 PM (GMT)

29
Jorge Valois
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 le résultat si je passe l'heure actuelle ("12:09:05 EDT" de Calendar.getInstance()) dans:

DEBUG - calendrier d'entrée a date [jeu 23 octobre 12:09:05 EDT 2008]
DEBUG - offset est de -14400000
DEBUG - Création de l’appel GMT avec date [jeu 23 oct 08:09:05 EDT 2008]

12h09h05 GMT est 8h09h05 HAE.

La partie déroutante ici est que Calendar.getTime() vous renvoie un Date dans votre fuseau horaire actuel, et qu’il n’existe aucune 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 pris par votre service Web, vous souhaiterez peut-être simplement obtenir le contrat WS en millisecondes à partir de Epoch.

61
matt b

Vous dites que la date est utilisée dans le cadre de services Web. Je suppose donc qu’elle est sérialisée dans une chaîne à un moment donné.

Si c'est le cas, vous devriez jeter un coup d'œil à la méthode méthode setTimeZone de la classe DateFormat. Cela indique quel fuseau horaire 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());
20

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"));
12
Vitalii Fedorenko

Il semble que votre horodatage soit défini sur le fuseau horaire du système d'origine.

Ceci est déconseillé, mais cela devrait fonctionner:

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

La manière non-obsolète est d'utiliser

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

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

8
Skip Head

Méthode de conversion d'un timeZone en un autre (ça marche 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;
}
7
Helpa

Date et Horodatage les objets ont un timezone-inconscient: ils représentent un certain nombre de secondes depuis l'époque, sans s'engager pour une interprétation particulière de cet instant sous forme d'heures et de jours. Les fuseaux horaires entrent dans l'image uniquement entre GregorianCalendar (non directement nécessaire pour cette tâche) et SimpleDateFormat, qui nécessitent un décalage de fuseau horaire pour être convertis entre des champs distincts et Date (ou long ) valeurs.

Le problème du PO se situe au tout début de son traitement: l'utilisateur entre des heures ambiguës et les interprète dans le fuseau horaire local non GMT; à ce stade, la valeur est "6:12 EST" , qui peut être facilement imprimée sous la forme "11.12 GMT" ou tout autre fuseau horaire mais ne va jamais changer en "6.12 GMT ".

Il n'y a aucun moyen de faire le SimpleDateFormat qui analyse "06:12" en tant que "HH: MM" (par défaut, le fuseau horaire local) est remplacé par défaut par l'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 la mettez explicitement dans l'entrée: ajoutez simplement une chaîne fixe à l'élément reçu (et correctement validé) "06:12" pour analyser "06:12 GMT" en tant que "HH: MM z" .

Il n’est pas nécessaire de définir explicitement les champs GregorianCalendar ni d’extraire et d’utiliser le décalage horaire et le décalage horaire.

Le véritable problème réside dans la séparation des entrées par défaut dans le fuseau horaire local, des entrées par défaut à UTC et des entrées nécessitant réellement une indication explicite de fuseau horaire.

6
user412090

Quelque chose a fonctionné pour moi dans le passé a été 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 l’orientation de la conversion) pour obtenir l’heure appropriée dans les deux fuseaux horaires. Pour ce faire, je définirais généralement le champ millisecondes d'un objet Calendar, mais je suis certain que vous pourriez facilement l'appliquer à un objet timestamp. 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).

4
Adam

Java.time

L'approche moderne utilise les classes Java.time qui ont supplanté les anciennes classes de date-heure problématiques associées aux versions les plus anciennes de Java.

Le Java.sql.Timestamp class est l'une de ces classes héritées. Ne sont plus nécessaires. Utilisez plutôt Instant ou d'autres classes Java.time directement avec votre base de données à l'aide de JDBC 4.2 et versions ultérieures.

La classe Instant représente un moment sur la ligne temporelle de 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 Timestamp existant, convertissez-le immédiatement en Java.time via les nouvelles méthodes de conversion ajoutées aux anciennes classes.

Instant instant = myTimestamp.toInstant() ;

Pour vous adapter à un autre fuseau horaire, spécifiez le fuseau horaire en tant qu'objet ZoneId. Spécifiez un nom de fuseau horaire correct au format continent/region, tel que America/Montreal , Africa/Casablanca , ou Pacific/Auckland. N'utilisez jamais les pseudo-zones de 3-4 lettres telles que EST ou IST telles qu'elles sont pas vrais fuseaux horaires, non standardisés, et même pas uniques ( !).

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

Appliquer au Instant pour produire un objet ZonedDateTime.

ZonedDateTime zdt = instant.atZone( z ) ;

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

Votre question concerne réellement le sens inverse, de la saisie de données utilisateur aux objets date-heure. Il est généralement préférable de séparer 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 pour être au format UTC? Ou dans un autre fuseau horaire?

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

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

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

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

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

Instant instant = odt.toInstant() ;

…ou…

Instant instant = zdt.toInstant() ; 

Envoyez à votre base de données.

myPreparedStatement.setObject( … , instant ) ;

À propos de Java.time

Le cadre Java.time est intégré à Java 8 et versions ultérieures. Ces classes supplantent les anciennes classes ennuyeuses héritées telles que - Java.util.Date , Calendar , & SimpleDateFormat .

Le projet Joda-Time , actuellement en mode maintenance , conseille de migrer vers les classes Java.time .

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

Où obtenir les classes Java.time?

Le projet ThreeTen-Extra étend Java.time avec des classes supplémentaires. Ce projet est un terrain d’essai pour d’éventuels ajouts à Java.time. Vous pouvez trouver ici quelques classes utiles telles que Interval , YearWeek , YearQuarter , et plus .

1
Basil Bourque