Ignorer les fuseaux horaires dans Rails et PostgreSQL

Je traite des dates et des heures dans Rails et Postgres et je me lance dans ce problème:

La firebase database est en UTC.

L’utilisateur définit un fuseau horaire de choix dans l’application Rails, mais il ne doit être utilisé que pour obtenir l’heure locale des utilisateurs afin de comparer les temps.

L’utilisateur stocke une heure, par exemple le 17 mars 2012 à 19h00. Je ne souhaite pas que les conversions de fuseau horaire ou le fuseau horaire soient stockés. Je veux juste que cette date et cette heure soient sauvées. De cette façon, si l’utilisateur changeait de fuseau horaire, il afficherait toujours le 17 mars 2012 à 19h.

Je n’utilise que le fuseau horaire des utilisateurs pour obtenir les enregistrements «avant» ou «après» l’heure actuelle dans le fuseau horaire de l’utilisateur.

J’utilise actuellement «horodatage sans fuseau horaire», mais lorsque je récupère les enregistrements, rails (?) Les convertit dans le fuseau horaire de l’application, ce que je ne veux pas.

Appointment.first.time => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

Étant donné que les enregistrements de la firebase database semblent sortir en UTC, mon hack consiste à prendre l’heure actuelle, à supprimer le fuseau horaire avec ‘Date.strptime (str, “% m /% d /% Y”)’ requête avec cela:

 .where("time >= ?", date_start) 

Il semble qu’il y ait un moyen plus simple d’ignorer les fuseaux horaires. Des idées?

L’ timestamp type de données est le nom abrégé de l’ timestamp without time zone .
L’autre option timestamptz est courte pour l’ timestamp with time zone .

timestamptz est le type préféré dans la famille date / heure, littéralement. Il a typispreferred mis dans pg_type , ce qui peut être pertinent:

  • Générer des séries chronologiques entre deux dates dans PostgreSQL

Époque

En interne , les horodatages sont stockés sous la forme d’un décompte d’une époque . Postgres utilise une époque du premier moment du premier jour de l’an 2000 en UTC, c’est-à-dire 2000-01-01T00: 00: 00Z. Huit octets sont utilisés pour stocker le numéro de compte. Selon une option de compilation, ce nombre est soit:

  • Un entier de 8 octets (par défaut), avec 0 à 6 chiffres d’une seconde fractionnaire
  • Un nombre à virgule flottante (déconseillé), avec 0 à 10 chiffres d’une fraction de seconde, où la précision se dégrade rapidement pour les valeurs plus éloignées de l’époque.
    Les installations modernes de Postgres utilisent le nombre entier de 8 octets.

Notez que Postgres n’utilise pas l’ heure Unix . L’époque de Postgres est le premier moment de 2000-01-01 plutôt que d’Unix 1970-01-01. Alors que le temps Unix a une résolution de secondes entières, Postgres conserve des fractions de secondes.

timestamp

Si vous définissez un timestamp type de données [without time zone] vous indiquez à Postgres: “Je ne fournis pas de fuseau horaire explicitement, supposez le fuseau horaire actuel. Postgres enregistre l’horodatage tel quel – en ignorant un modificateur de fuseau horaire un!

Lorsque vous affichez plus tard cet timestamp , vous récupérez ce que vous avez entré littéralement. Avec le même réglage de fuseau horaire, tout va bien. Si le paramètre de fuseau horaire de la session change, la signification de l’ timestamp change également – la valeur rest la même.

timestamptz

Le traitement de l’ timestamp with time zone est subtilement différent. Je cite le manuel ici :

Pour l’ timestamp with time zone , la valeur stockée en interne est toujours en UTC (Universal Coordinated Time …)

Emphase audacieuse la mienne Le fuseau horaire lui-même n’est jamais stocké . C’est un modificateur d’entrée utilisé pour calculer l’horodatage UTC en question, qui est stocké – ou le modificateur de sortie utilisé pour calculer l’heure locale à afficher – avec le décalage du fuseau horaire ajouté. Si vous n’ajoutez pas de décalage pour timestamptz en entrée, le paramètre de fuseau horaire actuel de la session est supposé. Tous les calculs sont effectués avec les valeurs d’horodatage UTC. Si vous devez (ou devez) traiter avec plusieurs fuseaux horaires, utilisez timestamptz .

Les clients comme psql ou pgAdmin ou toute application communiquant via libpq (comme Ruby avec le gem pg) sont présentés avec l’horodatage plus le décalage pour le fuseau horaire actuel ou selon un fuseau horaire demandé (voir ci-dessous). C’est toujours le même moment , seul le format d’affichage varie. Ou, comme le dit le manuel :

Toutes les dates et heures sensibles au fuseau horaire sont stockées en interne dans UTC. Ils sont convertis en heure locale dans la zone spécifiée par le paramètre de configuration TimeZone avant d’être affichés au client.

Considérons cet exemple simple (en psql):

 db = # SELECT timestamptz '2012-03-05 20:00 +03 ';
       timestamptz
 ------------------------
  2012-03-05 18:00:00 +01

Emphase audacieuse la mienne Que s’est-il passé ici?
J’ai choisi un décalage du fuseau horaire arbitraire +3 pour le littéral d’entrée. Pour Postgres, c’est l’une des nombreuses façons de saisir l’horodatage UTC 2012-03-05 17:00:00 . Le résultat de la requête est affiché pour le paramètre de fuseau horaire actuel Vienne / Ausortingche dans mon test, qui a un décalage +1 en hiver et +2 en été: 2012-03-05 18:00:00+01 , car il tombe en hiver.

Postgres a déjà oublié comment cette valeur a été saisie. Tout ce dont il se souvient est la valeur et le type de données. Tout comme avec un nombre décimal. numeric '003.4' , numeric '3.40' ou numeric '+3.4' – donnent tous exactement la même valeur interne.

AT TIME ZONE

Dès que vous maîsortingsez cette logique, vous pouvez faire tout ce que vous voulez. Tout ce qui manque maintenant, c’est un outil pour interpréter ou représenter les littéraux d’horodatage en fonction d’un fuseau horaire spécifique. C’est là AT TIME ZONE construction AT TIME ZONE . Il existe deux cas d’utilisation différents. timestamptz est converti en timestamp et inversement.

Pour entrer le UTC timestamptz 2012-03-05 17:00:00+0 :

 SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' 

… ce qui équivaut à:

 SELECT timestamptz '2012-03-05 17:00:00 UTC' 

Pour afficher le même moment que l’ timestamp EST (heure normale de l’Est):

 SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST' 

C’est vrai, deux fois sur la zone de AT TIME ZONE 'UTC' . Le premier interprète la valeur d’ timestamp comme (donnée) horodatage UTC renvoyant le type timestamptz . Le second convertit le timestamptz en timestamp dans le fuseau horaire donné ‘EST’ – quelle horloge dans le fuseau horaire EST affiche à ce moment unique.

Exemples

 SELECT ts AT TIME ZONE 'UTC' FROM ( VALUES (1, timestamptz '2012-03-05 17:00:00+0') , (2, timestamptz '2012-03-05 18:00:00+1') , (3, timestamptz '2012-03-05 17:00:00 UTC') , (4, timestamp '2012-03-05 11:00:00' AT TIME ZONE '+6') , (5, timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC') , (6, timestamp '2012-03-05 07:00:00' AT TIME ZONE 'US/Hawaii') -- ① , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii') -- ① , (8, timestamp '2012-03-05 07:00:00' AT TIME ZONE 'HST') -- ① , (9, timestamp '2012-03-05 18:00:00+1') -- ② loaded footgun! ) t(id, ts); 

Renvoie 8 (ou 9) lignes identiques avec une colonne timestamptz contenant le même horodatage UTC 2012-03-05 17:00:00 . La 9ème rangée fonctionne en quelque sorte dans mon fuseau horaire, mais est un piège maléfique. Voir ci-dessous.

Ows Les lignes 6 à 8 avec le nom du fuseau horaire et l’ abréviation du fuseau horaire pour l’heure d’Hawaï sont soumises à l’heure d’été et peuvent différer, mais pas actuellement. Un nom de fuseau horaire comme 'US/Hawaii' connaît automatiquement les règles DST et tous les changements historiques, tandis qu’une abréviation comme HST n’est qu’un code muet pour un décalage fixe. Vous devrez peut-être append une abréviation différente pour l’heure d’été / heure standard. Le nom interprète correctement n’importe quel horodatage au fuseau horaire donné. Une abréviation est bon marché, mais doit être la bonne pour l’horodatage donné:

  • Les noms de fuseau horaire ayant des propriétés identiques génèrent des résultats différents lorsqu’ils sont appliqués à l’horodatage

L’heure d’été n’est pas l’une des idées les plus shinyes de l’humanité.

② La rangée 9, marquée comme une arme à feu chargée fonctionne pour moi , mais seulement par coïncidence. Si vous convertissez explicitement un littéral en timestamp [without time zone] , tout décalage de fuseau horaire est ignoré ! Seul l’horodatage nu est utilisé. La valeur est ensuite automatiquement contrainte à timestamptz dans l’exemple pour correspondre au type de colonne. Pour cette étape, le réglage du timezone de la session en cours est supposé être le même fuseau horaire +1 dans mon cas (Europe / Vienne). Mais probablement pas dans votre cas – ce qui se traduira par une valeur différente. En bref: ne timestamptz littéraux timestamptz en timestamp ou vous perdez le décalage du fuseau horaire.

Vos questions

L’utilisateur stocke une heure, par exemple le 17 mars 2012 à 19h00. Je ne souhaite pas que les conversions de fuseau horaire ou le fuseau horaire soient stockés.

Le fuseau horaire lui-même n’est jamais stocké. Utilisez l’une des méthodes ci-dessus pour entrer un horodatage UTC.

Je n’utilise que le fuseau horaire des utilisateurs pour obtenir les enregistrements «avant» ou «après» l’heure actuelle dans le fuseau horaire de l’utilisateur.

Vous pouvez utiliser une requête pour tous les clients situés dans des fuseaux horaires différents.
Pour un temps global absolu:

 SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time 

Pour l’heure selon l’horloge locale:

 SELECT * FROM tbl WHERE time_col > now()::time 

Pas fatigué des informations de fond, encore? Il y a plus dans le manuel.

Si vous voulez traiter en UTC par défaut:

Dans config/application.rb , ajoutez:

 config.time_zone = 'UTC' 

Ensuite, si vous stockez l’utilisateur actuel, le nom du fuseau horaire est current_user.timezone vous pouvez le dire.

 post.created_at.in_time_zone(current_user.timezone) 

current_user.timezone doit être un nom de fuseau horaire valide, sinon vous obtiendrez ArgumentError: Invalid Timezone , voir la liste complète .