Moment: Adicionar e subtrair das datas não preserva as horas em que o fuso horário tem regras de horário de verão à meia-noite

Criado em 22 ago. 2018  ·  8Comentários  ·  Fonte: moment/moment

Descrição do assunto:

Estou tentando criar um intervalo de datas em (matriz de datas de momento) onde cada data é 1 dia após a anterior. Isso funciona conforme o esperado para todos os usuários, mas obtivemos um relatório de bug para um usuário no fuso horário América / Santiago que está vendo datas duplicadas na IU. O que percebi é que adicionar 1 dia a uma determinada data significa, na verdade, apenas 23 horas.

Com uma data em 8/11/2018 00:00:00 no fuso horário da América / Santiago, adicionar 1 dia aumenta o tempo para 8/11/2018 23:00:00 vez de 8/12/2018 00:00:00 .

Os documentos explicam que as horas devem ser preservadas ao mudar em um horário de verão _enquanto usa dias_, mas isso não parece ser verdade aqui.

Há também considerações especiais a serem observadas ao adicionar um horário que ultrapasse o horário de verão. Se você estiver adicionando anos, meses, semanas ou dias, a hora original sempre corresponderá à hora adicionada.

// This code is from moment.js docs https://momentjs.com/docs/#/manipulating/add/
var m = moment(new Date(2011, 2, 12, 5, 0, 0)); // the day before DST in the US
m.hours(); // 5
m.add(1, 'days').hours(); // 5

Passos para reproduzir

No código a seguir, o fuso horário usado tem sua regra de horário de verão à meia-noite. Eu tenho uma data que é meia-noite do dia anterior a esta regra entrar em vigor. Eu adicionei 1 dia a esta data, e espero que seja o dia seguinte à meia-noite / 0 horas (as horas devem ser preservadas), mas na verdade está apenas adicionando 23 horas.

fmt = d => d.format() + ' ' + d.tz()

x = moment.tz(new Date('08/11/2018 00:00:00'), 'America/Santiago');
fmt(x);                       // "2018-08-11T00:00:00-04:00 America/Santiago"
fmt(x.clone().add(1, 'day')); // "2018-08-11T23:00:00-04:00 America/Santiago" - offset unchanged, added 23 hours not 1 day
fmt(x.clone().add(2, 'day')); // "2018-08-13T00:00:00-03:00 America/Santiago" - original hour preserved now

Aqui, você pode ver que adicionar 24 horas aumentou em 25 horas e a diferença de fuso horário foi alterada.

fmt = d => d.format() + ' ' + d.tz()

x = moment.tz(new Date('08/11/2018 00:00:00'), 'America/Santiago');
fmt(x);                          // "2018-08-11T00:00:00-04:00 America/Santiago"
fmt(x.clone().add(24, 'hours')); // "2018-08-12T01:00:00-03:00 America/Santiago" - offset changed, added 25 hours
fmt(x.clone().add(48, 'hours')); // "2018-08-13T01:00:00-03:00 America/Santiago"

Meio Ambiente:

  • Versão 68.0.3440.84 (versão oficial) (64 bits)
  • Mac OSX El Capitan 10.11.6 (15G31)

Outras informações que podem ser úteis:

  • Fuso horário da máquina: fuso horário do leste dos EUA, horário de verão
  • O código de hora e data foi executado: 15h do dia 22 de agosto de 2018
  • Reproduzi esse problema nas ferramentas de desenvolvimento do Chrome na página da web do Moment.js

Saída de data JS

(novo Date ()). toString ()

  • Quarta, 22 de agosto de 2018, 15:13:40 GMT-0400 (horário de verão do leste)

(novo Date ()). toLocaleString ()

  • 22/08/2018, 15:13:40

(novo Date ()). getTimezoneOffset ()

  • 240

navigator.userAgent

  • Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit / 537.36 (KHTML, como Gecko) Chrome / 68.0.3440.84 Safari / 537.36

moment.version

  • 2.22.2
Bug DST Pending Next Release Up-For-Grabs

Comentários muito úteis

O comportamento aqui parece que foi alterado entre o fuso horário v0.5.4 e v0.5.26.
Na versão antiga, adicionar 1 dia a '11/08/2018' dava 23h no mesmo dia
Na nova versão, mostra '12/08/2018' à meia-noite. Mas esse tempo não existe de fato, e adicionar 1 minuto retrocede uma hora - mas essa alteração de horário de verão deveria adicionar 1 hora.

Versão antiga, se comporta conforme descrito neste problema:
https://jsfiddle.net/eqyvuxht/1/

Nova versão, comportamento alterado, mas ainda errado, eu acho?
https://jsfiddle.net/0h6atn4b/4/

Aqui está uma solução alternativa:

function switchZone(m, zone) {
  let arr = [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second()];
  if(zone) {
    return moment.tz(arr, zone);
  }
  return moment(arr);
}

function safeAddDays(m, days) {
   let oldZone = m.tz();
   let utc = switchZone(m, 'UTC');
   utc.add(days, 'days');
   return switchZone(utc, oldZone);
}

Todos 8 comentários

Tentei reproduzir isso no fuso horário Europa / Roma e não consegui:

Europa / Roma - https://www.timeanddate.com/time/change/italy/rome

28 de outubro de 2018 - Fim do horário de verão
Quando o horário de verão local está prestes a chegar
Domingo, 28 de outubro de 2018, 3h00, os relógios estão atrasados ​​1 hora para
Domingo, 28 de outubro de 2018, às 2h00 do horário local padrão.

Não há bug neste código.

d => d.format() + ' ' + d.tz()

x = moment.tz(new Date('10/27/2018 00:00:00'), 'Europe/Rome');
fmt(x)                       // "2018-10-27T06:00:00+02:00 Europe/Rome"
fmt(x.clone().add(1, 'day')) // "2018-10-28T06:00:00+01:00 Europe/Rome" - tz offset changed, hour preserved as expected
fmt(x.clone().add(2, 'day')) // "2018-10-29T06:00:00+01:00 Europe/Rome"

Você pode ver que o TZ muda, mas as horas permanecem constantes.

Com base nas etapas de reprodução, parece que há uma discrepância no código quando a data adicionada cai perfeitamente no corte do DST. No fuso horário de Santiago, a regra do DST é que à meia-noite os relógios voltem uma hora.

Parece que esse caso extremo pode ser especificamente para fusos horários em que o corte do horário de verão é à meia-noite, porque não consegui reproduzir com um fuso horário em que o corte é às 3h, com x data às 3h.

Europa / Roma - https://www.timeanddate.com/time/change/italy/rome

28 de outubro de 2018 - Fim do horário de verão
Quando o horário de verão local está prestes a chegar
Domingo, 28 de outubro de 2018, 3h00, os relógios estão atrasados ​​1 hora para
Domingo, 28 de outubro de 2018, às 2h00 do horário local padrão.

Não há bug neste código.

fmt = d => d.format() + ' ' + d.tz()

x = moment.tz(new Date('October 27, 2018 03:00:00'), 'Europe/Rome');
fmt(x);                       // "2018-10-27T09:00:00+02:00 Europe/Rome"
fmt(x.clone().add(1, 'days')) // "2018-10-28T09:00:00+01:00 Europe/Rome" - tz offset changed, hour preserved as expected
fmt(x.clone().add(2, 'days')) // "2018-10-29T09:00:00+01:00 Europe/Rome"

Consegui confirmar que esse bug só existe para fusos horários em que o corte do horário de verão é à meia-noite e você está adicionando um horário a uma data que faz com que a data pare exatamente no corte.

America / Punta_Arenas - https://www.timeanddate.com/time/zone/chile/punta-arenas
Em 2016, esse fuso horário também teve corte à meia-noite.

Este código parece ter um bug. Veja o comentário após cada linha

fmt = d => d.format() + ' ' + d.tz()

x = moment.tz(new Date('08/13/2016 00:00:00'), 'America/Punta_Arenas')
fmt(x);                        // "2016-08-13T00:00:00-04:00 America/Punta_Arenas"
fmt(x.clone().add(1, 'days')); // "2016-08-13T23:00:00-04:00 America/Punta_Arenas" - 23 hours added, not 1 day, no tz offset change
fmt(x.clone().add(2, 'days')); // "2016-08-15T00:00:00-03:00 America/Punta_Arenas"

Descobri o que parece ser um problema semelhante ao subtrair os mesmos tipos de fusos horários (DST à meia-noite).

Ao subtrair as datas exatamente em um turno do horário de verão, a hora não é preservada apenas naquele dia, mas a data é alterada.

fmt = d => d.format() + ' ' + d.tz()

x = moment.tz(new Date('08/13/2018 23:00:00'), 'America/Santiago');
fmt(x);                             // "2018-08-14T00:00:00-03:00 America/Santiago"
fmt(x.clone().subtract(1, 'days')); // "2018-08-13T00:00:00-03:00 America/Santiago"
fmt(x.clone().subtract(2, 'days')); // "2018-08-12T01:00:00-03:00 America/Santiago" - hour not preserved, but date changed
fmt(x.clone().subtract(3, 'days')); // "2018-08-11T00:00:00-04:00 America/Santiago" - original hour preserved now

Recebemos outro relatório sobre esse problema, de um usuário com America/Asuncion como fuso horário.

A função que produz o bug em nosso aplicativo tem a seguinte aparência:

function generateDayRange(start, end) {
    const days = [];
    let current = start.clone();

    while (current <= end) {
        days.push(current.clone());
        current = current.add(1, 'days');
    }

    return days;
}

Quando usamos esta função, passamos uma data de início que está no início do dia e uma data de término em que a hora não é tão significativa:

generateDayRange(startDate.clone().startOf('week'), startDate.clone().endOf('week'));
generateDayRange(getAppDate(startDate), getAppDate(endDate));
generateDayRange(startRange, startRange.clone().add(27, 'days'));

O problema é que a matriz resultante de datas de momento contém um dia duplicado, o que se reflete na interface do usuário de nosso aplicativo (um calendário tem um dia duplicado).

Minha maior preocupação é que, como a biblioteca não se comporta conforme o esperado, pode haver problemas mais sutis passando despercebidos, como perda de dados percebida, solicitações incorretas, etc.

Vou trabalhar para obter um conjunto de testes em um codepen ou algo assim, para que os leitores curiosos possam tentar resolver esse problema.

O problema relacionado # 4785 contém links para o código:
exemplo de momento https://runkit.com/embed/1r62d83amq7x
ainda assim, luxon lida com isso corretamente
https://runkit.com/embed/t49achvensqf

Eu sei que houve mudanças recentes de horário de verão, então devemos verificar o código atualmente em develop (mas não lançado). Ou podemos esperar até o próximo lançamento para ver se as coisas ainda são as mesmas.

O comportamento aqui parece que foi alterado entre o fuso horário v0.5.4 e v0.5.26.
Na versão antiga, adicionar 1 dia a '11/08/2018' dava 23h no mesmo dia
Na nova versão, mostra '12/08/2018' à meia-noite. Mas esse tempo não existe de fato, e adicionar 1 minuto retrocede uma hora - mas essa alteração de horário de verão deveria adicionar 1 hora.

Versão antiga, se comporta conforme descrito neste problema:
https://jsfiddle.net/eqyvuxht/1/

Nova versão, comportamento alterado, mas ainda errado, eu acho?
https://jsfiddle.net/0h6atn4b/4/

Aqui está uma solução alternativa:

function switchZone(m, zone) {
  let arr = [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second()];
  if(zone) {
    return moment.tz(arr, zone);
  }
  return moment(arr);
}

function safeAddDays(m, days) {
   let oldZone = m.tz();
   let utc = switchZone(m, 'UTC');
   utc.add(days, 'days');
   return switchZone(utc, oldZone);
}

Estamos enfrentando o mesmo problema neste mês (setembro de 2019), em que nossos usuários de Santiago, usando nosso selecionador de data, estão vendo um calendário com datas 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, para o mês de setembro ... Nos bastidores, também estamos usando .add(1, 'days') . Ele ocorrerá novamente no próximo ano, em setembro de 2020, se não tivermos uma solução para ele até então.

A solução alternativa do

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

mj1856 picture mj1856  ·  118Comentários

gayankasun picture gayankasun  ·  47Comentários

llacroix picture llacroix  ·  186Comentários

fernandoacorreia picture fernandoacorreia  ·  31Comentários

electrobabe picture electrobabe  ·  48Comentários