Time zone - Date - JavaScript ExpressJS - Angular

Veja como trabalhar com time zone (fuso horário) em aplicações JavaScript como Angular e ExpressJS que utilizam o objeto Date

Time zone -  Date - JavaScript ExpressJS - Angular
Time zone - Date - JavaScript ExpressJS - Angular

Sempre que trabalhamos com fuso horário no JavaScript, o objeto Date pode causar algumas confusões. Talvez você já tenha presenciado alguma situação onde o desenvolvedor soma ou subtraí manualmente uma quantidade X de minutos ou horas para que o resultado de um Date.toString() exiba a informação em um fuso que o desenvolvedor está esperando.

Este com toda certeza não é o caminho correto e este texto ajudará a entender como podemos trabalhar com fuso horário em JavaScript. Essa lógica se estende ao Node.js, Angular ou qualquer outro framework e biblioteca baseada em JavaScript.

Introdução

Todos sabemos que existem os fusos horários, ou seja, diferenças entre horários de acordo com a região. Por exemplo, quando são 09:00h no Brasil (horário de Brasília), em Los Angeles (EUA) são 04:00h e em Londres 12:00h.

Diferença nos fusos entre Brasília, Los Angeles e Londres
Diferença nos fusos entre Brasília, Los Angeles e Londres - https://www.worldtimebuddy.com

Para entender essas diferenças de horários vamos falar rapidamente sobre UTC - Coordinated Universal Time.

De uma forma macro podemos pensar que o mapa mundial foi divido em zonas. Estabeleceu-se que a zona central é a referência (zona em vermelho na imagem abaixo). À esquerda desta referência temos as zonas onde estamos xhoras atrás e à direita temos as zonas que estão xhoras a frente. Veja na imagem abaixo que o Brasil fica na região de -3 horas:

Mapa global com a divisão de fuso horário no padrão UTC
Mapa global com a divisão de fuso horário no padrão UTC - https://geospatialclub.wordpress.com/2016/05/04/time-terminologies-gmt-and-utc/

Essas diferenças de horários, -3 horas por exemplo, também são conhecidas como time zone offset. Inclusive, provavelmente você já tenha visto o método getTimezoneOffset() do objeto Date. Este método retorna justamente a diferença de horário em minutos entre a região que o navegador está configurado e a zona de referência.

const dataAtual = new Date();
console.log(dataAtual.getTimezoneOffset());

// Resultado no console: 180

// Observação: o computador onde o
// código acima foi executado está
// configurado com o horário de
// Brasília (-03:00h ou -180 minutos)

Antes de entrarmos no código, quero ressaltar a importância de saber o time zone de uma data & hora. Suponha que alguém te fale:

"A transação foi efetuada no dia 01/01/2022 às 15:00h".

Bom, podemos nos perguntar se esta data & hora é em Londres? Por que se for em Londres significa que no Brasil a transação foi efetuada às 12:00h. Percebe a importância desta informação (localização ou time zone)?

Timezone nos navegadores

Agora que estamos alinhados quanto aos fusos, vamos entrar no código!

Quando estamos trabalhando com Date temos algumas formas de instanciar o objeto, vamos destacar 2:

  • Passando um date string no formato ISO 8601, por exemplo, YYYY-MM-DD ou YYYY-MM-DDTHH:mm:ss
  • Passando os valores numéricos do ano, mês, dia, hora, minuto e milissegundo. Aqui temos um ponto importante: em JavaScript o mês se inicia em zero, ou seja, janeiro = 0

Veja no exemplo abaixo, executado em um computador configurado com o horário de Brasília, a diferença do console.log:

const data1 = new Date('2021-12-01');
const data2 = new Date('2021-12-01T12:00');

const data3 = new Date(2021, 11, 1);
const data4 = new Date(2021, 11, 1, 12, 0);

console.log(data1);
// Tue Nov 30 2021 21:00:00 GMT-0300 (-03)

console.log(data2);
// Wed Dec 01 2021 12:00:00 GMT-0300 (-03)


console.log(data3);
// Wed Dec 01 2021 00:00:00 GMT-0300 (-03)

console.log(data4);
// Wed Dec 01 2021 12:00:00 GMT-0300 (-03)

Quando omitimos as horas/minutos, subentende-se que sejam 00:00h (meia noite).

Vamos analisar cada caso acima lembrando que executei o código com o computador no horário de Brasília (UTC-03:00). Lembre-se também que o .toString() do Date retorna o horário ajustado para o time zone do seu computador:

  1. Quando passamos um date string e omitimos o horário, o navegador considera que aquela data está em UTC-0 (offset zero). Como consequência, o valor passado no Date, '2021-12-01', teve como resultado: 30/11/2021 21:00 (GMT-03:00 - horário de Brasília).
  2. Quando passamos um date string com o horário, o navegador considera que aquela data & hora corresponde ao time zone configurado no computador.
  3. Diferente do primeiro exemplo, quando passamos a informação de forma numérica com ou sem o horário, o navegador considera que aquela data & hora corresponde ao time zone configurado no computador.
  4. Idem ao item anterior

A título de exemplo, se alterar o time zone do computador para Los Angeles UTC-08:00 (8 horas atrás da zona de referência) e executar o código acima o resultado será:

Alterando o time zone do computador de Brasília para Los Angeles
Alterando o time zone do computador de Brasília para Los Angeles
const data1 = new Date('2021-12-01');
const data2 = new Date('2021-12-01T12:00');

const data3 = new Date(2021, 11, 1);
const data4 = new Date(2021, 11, 1, 12, 0);

console.log(data1);
// Tue Nov 30 2021 16:00:00 GMT-0800 (PST)

console.log(data2);
// Wed Dec 01 2021 12:00:00 GMT-0800 (PST)


console.log(data3);
// Wed Dec 01 2021 00:00:00 GMT-0800 (PST)

console.log(data4);
// Wed Dec 01 2021 12:00:00 GMT-0800 (PST)

Veja que você sempre deve ter atenção quando for instanciar um Date. Dependendo dos parâmetros passados no construtor a interpretação do time zone pode variar.

Então é extremamente importante que ao trabalhar no frontend você tenha atenção em como vai instanciar seus objeto do tipo Date.

Enviando os dados do frontend para API

Vamos quebrar a barreira do frontend e mostrar como um Date é representado em um POST que seu front faz para uma API.

No código abaixo informamos no frontend que a data que estamos postando é dia 1 de dezembro de 2021 ás 12:00 no horário de Brasília.


O código foi executado em um navegador cujo o sistema operacional estava configurado com o horário de Brasília.


Veja como a data fica no body da requisição:

(async () => {
  await fetch("http://localhost:3000/dados", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      data: new Date("2021-12-01T12:00"),
    }),
  });
})();
POST /dados HTTP/1.1
Accept: application/json
Content-Type: application/json
Origin: http://localhost:3000
Content-Length: 35
Host: localhost:3000
Connection: keep-alive

{"data":"2021-12-01T15:00:00.000Z"}

Podemos observar que no body do POST a data trafega no padrão ISO 8601. Vamos detalhar um pouco mais:

  • No navegador o usuário informou que a informação data corresponde ao dia 1 de dezembro de 2021 ás 12:00h horário de Brasília.
  • O navegador transmite a informação em UTC-0 (por isso o Z no final da string): dia 1 de dezembro de 2021 às 15h. Novamente, lembrando que durante os testes o computador estava configurado para o horário de Brasília (-03:00).

Perceba a importância deste Z. Veja que agora surgiu um padrão, toda data pode ser trafegada em UTC-0 e fica a cargo do frontend exibí-la no fuso que o usuário desejar. Vamos explorar este comportamento mais abaixo.

Recebendo os dados da API

Agora vamos estudar o fluxo inverso, quando seu frontend recebe a data de uma API. Considere que a data chegará no formato ISO 8601:

const data = new Date("2021-12-01T15:00:00.000Z");
console.log(data);

// Wed Dec 01 2021 12:00:00 GMT-0300 (-03)

O comportamento está correto! o frontend recebe a informação da data, hora e time zone e apresenta a informação no horário local do navegador.

Suponha que seu frontend permita que o usuário personalize o time zone, então podemos utilizar o intl para formatar a data no fuso que desejarmos, por exemplo, America/Boa_Vista cujo o fuso é -04:00:

> console.log(new Intl.DateTimeFormat(
    'pt-BR', {
        timeZone: 'America/Boa_Vista',
        dateStyle: 'short',
        timeStyle: 'short'
    }).format(data));

// 01/12/2021 11:00

Agora vamos supor que sua API não indique o time zone:

const data = new Date("2021-12-01T15:00:00");
console.log(data);

//  Wed Dec 01 2021 15:00:00 GMT-0300 (-03)

O navegador considerou que a data e hora correspondem ao horário local, ou seja, dia 1 de dezembro de 2021 às 15h do horário local. Isto está errado se considerarmos que enviamos para nossa API o dia 1 de dezembro de 2021 às 12h do horário local (citação do exemplo em Enviando os dados do frontend para API).

Então sempre que receber uma data de uma API saiba qual é o time zone daquela informação, sempre!

Como indicar o time zone?

Nos casos que precisar indicar o time zone durante a criação do objeto Date, podemos proceder da seguinte forma:

console.log(new Date('2021-12-01T12:00:00.00-0200'));
// Wed Dec 01 2021 11:00:00 GMT-0300 (-03)

console.log(new Date('2021-12-01T12:00:00.00-0000'));
// Wed Dec 01 2021 09:00:00 GMT-0300 (-03)

console.log(new Date('2021-12-01T12:00:00.00+0200'));
// Wed Dec 01 2021 07:00:00 GMT-0300 (-03)

Node.js Express

Até este ponto só executamos o código dentro do navegador. Devemos lembrar que quando a renderização é feita no lado do servidor, como é o caso do modelo MVC do Express, o .toString() do Date corresponde a configuração do servidor e não do navegador.

No caso do Express, podemos alterar o fuso horário da aplicação indicando a região de fuso em uma variável de ambiente. A variável de ambiente é TZ e os valores possíveis podem ser encontrados nesta lista.

Veja no exemplo abaixo que o http://localhost:3000/ renderiza a view index.ejs. Esta view recebe como parâmetro a data 2021-12-01T15:00:00.000Z:

process.env.TZ = "Europe/London";

const express = require("express");
const path = require("path");
const app = express();

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "/views"));

app.get("/", (_, res) => {
  var dados = {
      data: new Date(
          "2021-12-01T15:00:00.000Z"
      )
  };
  
  res.render("index", { dadosDaView: dados });
});

app.listen(3000, () => {
  console.log("Servidor rodando na porta 3000");
});
index.js
Teste de data:
<div>Item: <%= dadosDaView.data %></div>
index.ejs

O resultado que aparece no navegador está de acordo com o esperado já que Londres (Europe/London) não tem diferença de fuso horário. Como declaramos na variável data o valor "1 de dezembro de 2021 ás 15 horas no fuso horário zero (UTM-0), o resultado é exatamente este:

Renderização da data com o time zone configurado para Europe/London
Renderição da view com o time zone configurado para Europe/London

Trocando o process.env.TZ para o valor "America/Sao_Paulo", onde o fuso horário é - 3 horas, temos uma renderização diferente:

Renderização da data com o time zone configurado para America/Sao_Paulo
Renderição da view com o time zone configurado para America/Sao_Paulo

Veja que a lógica está correta. Durante a renderização o fuso horário é levado em conta, então o Node.js "desconta" as 3 horas para exibir o valor da variável data no fuso local (indicado na variável de ambiente TZ).

Considerações

Particularmente acho este assunto um pouco maçante para estudo. Mas para quem trabalha especialmente com sistemas internacionais o entedimento do time zone é de grande importância.

Não comentei sobre o time zone no contexto de aplicações Angular porque já tinha feito um outro post sobre este assunto. Vou deixar o link aqui.

Links interessantes: