Ignorer complètement les fuseaux horaires dans Rails et PostgreSQL

164

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

La base de données est en UTC.

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

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

J'utilise uniquement le fuseau horaire spécifié par les utilisateurs pour obtenir des enregistrements «avant» ou «après» l'heure actuelle dans le fuseau horaire local des utilisateurs.

J'utilise actuellement «horodatage sans fuseau horaire», mais lorsque je récupère les enregistrements, rails (?) Les convertit en fuseau horaire dans 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 base de données semblent sortir au format UTC, mon hack consiste à prendre l'heure actuelle, supprimer le fuseau horaire avec 'Date.strptime (str, "% m /% d /% Y")' puis faire mon requête avec ça:

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

Il semble qu'il doit y avoir un moyen plus simple d'ignorer les fuseaux horaires partout. Des idées?

99 milles
la source

Réponses:

347

Le type de données timestampest le nom court de timestamp without time zone.
L'autre option timestamptzest courte pour timestamp with time zone.

timestamptzest le type préféré dans la famille date / heure, littéralement. Il s'est typispreferredinstallé pg_type, ce qui peut être pertinent:

Stockage interne et époque

En interne, les horodatages occupent 8 octets de stockage sur disque et en RAM. Il s'agit d'une valeur entière représentant le nombre de microsecondes depuis l'époque Postgres, 2000-01-01 00:00:00 UTC.

Postgres a également une connaissance intégrée du comptage de temps UNIX couramment utilisé en secondes à partir de l'époque UNIX, 1970-01-01 00:00:00 UTC, et l'utilise dans les fonctions to_timestamp(double precision)ou EXTRACT(EPOCH FROM timestamptz).

Le code source:

* Les horodatages, ainsi que les champs h / m / s des intervalles, sont stockés comme
* valeurs int64 avec unités de microsecondes. (Il était une fois  
* valeurs doubles avec unités de secondes.)

Et:

/ * Équivalents à date julienne du jour 0 dans le calcul Unix et Postgres * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

La résolution en microseconde se traduit par un maximum de 6 chiffres fractionnaires pour les secondes.

### timestamp Une valeur tapée comme indique à Postgres qu'aucun fuseau horaire n'est fourni explicitement. Le fuseau horaire actuel est supposé. Postgres ignore tout modificateur de fuseau horaire ajouté par erreur!timestamp [without time zone]

Aucune heure n'est décalée pour l'affichage. Avec le même réglage de fuseau horaire, tout va bien. Pour un réglage de fuseau horaire différent, la signification change, mais la valeur et l' affichage restent les mêmes.

### La timestamptz manipulation de timestamp with time zoneest subtilement différente. Je cite le manuel ici :

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

Je souligne le mien. Le fuseau horaire lui-même n'est jamais stocké . Il s'agit d'un modificateur d'entrée utilisé pour calculer l'horodatage UTC correspondant, qui est stocké - ou un modificateur de sortie utilisé pour calculer l'heure locale à afficher - avec le décalage de fuseau horaire ajouté. Si vous n'ajoutez pas de décalage pour l' timestamptzentrée, le paramètre de fuseau horaire actuel de la session est supposé. Tous les calculs sont effectués avec des valeurs d'horodatage UTC. Si vous devez (ou pourriez devoir) gérer plusieurs fuseaux horaires, utilisez timestamptz.

Les clients comme psql ou pgAdmin ou toute application communiquant via libpq (comme Ruby avec le pg gem) 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 dans le temps , seul le format d'affichage varie. Ou, comme le dit le manuel :

Toutes les dates et heures tenant compte du fuseau horaire sont stockées en interne au format 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érez cet exemple simple (dans psql):

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

Je souligne le mien. Que s'est-il passé ici?
J'ai choisi un décalage de fuseau horaire arbitraire +3pour le littéral d'entrée. Pour Postgres, ce n'est qu'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 réglage du fuseau horaire actuel Vienne / Autriche dans mon test, qui a un décalage +1pendant l'hiver et +2pendant l'heure d'été:, 2012-03-05 18:00:00+01car il tombe à l'heure d'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'- tous donnent exactement la même valeur interne.

### AT TIME ZONE Dès que vous maîtrisez 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à que la AT TIME ZONEconstruction entre en jeu. Il existe deux cas d'utilisation différents. timestamptzest converti en timestampet vice versa.

Pour entrer l'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 HNE timestamp(heure normale de l'Est):

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

C'est vrai, AT TIME ZONE 'UTC' deux fois . Le premier interprète la timestampvaleur comme un horodatage UTC (donné) retournant le type timestamptz. Le second convertit le timestamptzen le timestampdans le fuseau horaire donné 'EST' - ce qu'une 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 des colonnes timestamptz contenant le même horodatage UTC 2012-03-05 17:00:00. La 9ème rangée fonctionne dans mon fuseau horaire, mais c'est un piège maléfique. Voir ci-dessous.

① 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é (heure d'été) et peuvent différer, mais pas actuellement. Un nom de fuseau horaire comme 'US/Hawaii'est conscient des règles DST et de tous les décalages historiques automatiquement, tandis qu'une abréviation comme HSTest juste un code stupide pour un décalage fixe. Vous devrez peut-être ajouter une abréviation différente pour l'heure d'été / 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é:

L'heure d'été ne fait pas partie des idées les plus brillantes que l'humanité ait jamais imaginées.

② La ligne 9, marquée comme une arme à pied 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 brut est utilisé. La valeur est alors automatiquement forcée timestamptzdans l'exemple pour correspondre au type de colonne. Pour cette étape, le timezoneréglage de la session en cours est supposé, qui se trouve être le même fuseau horaire +1dans mon cas (Europe / Vienne). Mais probablement pas dans votre cas - ce qui entraînera une valeur différente. En bref: ne timestamptzconvertissez pas de littéraux timestampou vous perdez le décalage du fuseau horaire.

##Vos questions

L'utilisateur enregistre une heure, par exemple le 17 mars 2012, 19 h. Je ne veux 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.

J'utilise uniquement le fuseau horaire spécifié par les utilisateurs pour obtenir des enregistrements «avant» ou «après» l'heure actuelle dans le fuseau horaire local des utilisateurs.

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

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

Pour l'heure en fonction de l'horloge locale:

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

Vous n'êtes pas encore fatigué des informations de base? Il y a plus dans le manuel.

Erwin Brandstetter
la source
2
Détail mineur, mais je pense que les horodatages sont stockés en interne sous forme de nombre de microsecondes depuis le 01/01/2000 - voir la section type de données date / heure du manuel. Mes propres inspections de la source semblent le confirmer. Etrange d'utiliser une origine différente pour l'époque!
Harmic
2
@harmic Quant à une époque différente… En fait, pas si étrange. Cette page Wikipedia répertorie deux douzaines d'époques utilisées par divers systèmes informatiques. Bien que l' époque Unix soit courante, ce n'est pas la seule.
Basil Bourque
4
@ErwinBrandstetter C'est une excellente réponse, à l'exception d'un grave défaut. Comme l'a fait remarquer Harmic, Postgres n'utilise pas l' heure Unix. D'après le document : (a) L'époque est 2001-01-01 plutôt que Unix '1970-01-01, et (b) Alors que l'heure Unix a une résolution de secondes entières, Postgres garde des fractions de secondes. Le nombre de chiffres fractionnaires dépend de l'option de compilation: 0 à 6 lorsque le stockage d'entiers de huit octets (par défaut) est utilisé, ou de 0 à 10 lorsque le stockage à virgule flottante (obsolète) est utilisé.
Basil Bourque
2
@BasilBourque: Je suis conscient de cette malheureuse erreur. Si cela ne vous dérange pas, n'hésitez pas à le modifier. J'ai vu certaines de vos réponses dans le passé et vous y êtes douée. Une autre modification de ma part forcerait cela au wiki de la communauté - au fil du temps, j'ai déployé beaucoup d'efforts (et de modifications) pour le rendre clair et complet.
Erwin Brandstetter
2
CORRECTION: Lors de mon commentaire précédent, j'ai cité à tort l'époque Postgres comme 2001. En fait, c'est 2000 .
Basil Bourque
1

Si vous souhaitez traiter en UTC par défaut:

Dans config/application.rb, ajoutez:

config.time_zone = 'UTC'

Ensuite, si vous stockez le nom du fuseau horaire de l'utilisateur actuel, current_user.timezonevous pouvez le dire.

post.created_at.in_time_zone(current_user.timezone)

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

Dorian
la source