Moment: Добавление и вычитание из дат не сохраняет часы, если часовой пояс имеет правила полуночного летнего времени.

Созданный на 22 авг. 2018  ·  8Комментарии  ·  Источник: moment/moment

Описание проблемы:

Я пытаюсь создать диапазон дат в (массив дат моментов), где каждая дата на 1 день позже предыдущей. Это работает должным образом для всех пользователей, но мы получили отчет об ошибке для пользователя в часовом поясе Америка / Сантьяго, который видит повторяющиеся даты в пользовательском интерфейсе. Я заметил, что добавление 1 дня к определенной дате на самом деле добавляет только 23 часа.

При дате в 8/11/2018 00:00:00 в часовом поясе Америки / Сантьяго добавление 1 дня увеличивает время до 8/11/2018 23:00:00 вместо 8/12/2018 00:00:00 .

В документации объясняется, что часы должны сохраняться при переходе на летнее время _ при использовании дней_, но здесь это не похоже на правду.

Есть также особые соображения, которые следует учитывать при добавлении времени, пересекающего летнее время. Если вы добавляете годы, месяцы, недели или дни, исходный час всегда будет соответствовать добавленному часу.

// 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

Действия по воспроизведению

В следующем коде для используемого часового пояса установлено правило летнего времени в полночь. У меня есть дата в полночь за день до того, как это правило вступит в силу. Я добавил к этой дате 1 день и ожидаю, что он должен быть на следующий день в полночь / 0 часов (часы должны быть сохранены), но на самом деле это только добавляет 23 часа.

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

Здесь вы можете видеть, что добавление 24 часов сместило его на 25 часов, а смещение часового пояса было изменено.

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"

Окружающая обстановка:

  • Версия 68.0.3440.84 (Официальная сборка) (64-бит)
  • Mac OSX El Capitan 10.11.6 (15G31)

Другая информация, которая может быть полезна:

  • Часовой пояс машины: Восточный часовой пояс США, переход на летнее время
  • Код времени и даты был запущен: 15:00 22 августа 2018 г.
  • Я воспроизвел эту проблему в Chrome Dev Tools на веб-странице Moment.js

Вывод даты JS

(новая дата ()). toString ()

  • Среда, 22 августа 2018 г., 15:13:40 GMT-0400 (восточное летнее время)

(новая дата ()). toLocaleString ()

  • 22.08.2018, 15:13:40

(новая дата ()). getTimezoneOffset ()

  • 240

navigator.userAgent

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

момент.версия

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

Самый полезный комментарий

Поведение здесь выглядит так, как будто оно было изменено между моментом-часовым поясом v0.5.4 и v0.5.26.
В старой версии добавление 1 дня к '2018-08-11' давало 23:00 того же дня.
В новой версии в полночь отображается «2018-08-12». Но на самом деле этого времени не существует, и добавление 1 минуты возвращает нас на час, но это изменение летнего времени должно было добавить 1 час.

Старая версия ведет себя так, как описано в этой проблеме:
https://jsfiddle.net/eqyvuxht/1/

Новая версия, измененное поведение, но я все еще не прав?
https://jsfiddle.net/0h6atn4b/4/

Вот обходной путь:

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);
}

Все 8 Комментарий

Я попытался воспроизвести это в часовом поясе Европа / Рим, и не смог:

Европа / Рим - https://www.timeanddate.com/time/change/italy/rome

28 октября 2018 г. - Окончание летнего времени
Когда наступит местное летнее время
Воскресенье, 28 октября 2018 г., 3:00:00 часы переводятся на 1 час назад
Воскресенье, 28 октября 2018 г., вместо 2:00:00 по местному времени.

В этом коде нет ошибки.

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"

Вы можете видеть, что TZ меняется, но часы остаются неизменными.

Судя по этапам воспроизведения, похоже, что в коде есть несоответствие, когда добавленная дата идеально совпадает с отсечкой летнего времени. В часовом поясе Сантьяго правило летнего времени заключается в том, что в полночь часы возвращаются на час назад.

Похоже, что этот пограничный случай может быть специально для часовых поясов, где переход на летнее время приходится на полночь, потому что я не смог воспроизвести с часовым поясом, где отсечка находится в 3 часа ночи, с датой x в 3 часа ночи.

Европа / Рим - https://www.timeanddate.com/time/change/italy/rome

28 октября 2018 г. - Окончание летнего времени
Когда наступит местное летнее время
Воскресенье, 28 октября 2018 г., 3:00:00 часы переводятся на 1 час назад
Воскресенье, 28 октября 2018 г., вместо 2:00:00 по местному времени.

В этом коде нет ошибки.

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"

Я смог подтвердить, что эта ошибка существует только для часовых поясов, когда переход на летнее время наступает в полночь, и вы добавляете время к дате, которая приводит к тому, что дата приземляется точно на отсечке.

America / Punta_Arenas - https://www.timeanddate.com/time/zone/chile/punta-arenas
В 2016 году в этом часовом поясе также было отключение в полночь.

В этом коде есть ошибка. См. Комментарий после каждой строки

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"

Я обнаружил похожую проблему при вычитании одного и того же часового пояса (летнее время в полночь).

При вычитании дат точно на смену летнего времени час не сохраняется только в этот день, но дата изменяется.

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

Мы получили еще один отчет об этой проблеме от пользователя с America/Asuncion качестве часового пояса.

Функция, которая выдает ошибку в нашем приложении, выглядит так:

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

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

    return days;
}

Когда мы используем эту функцию, мы передаем дату начала, которая находится в начале дня, и дату окончания, когда время не так важно:

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

Проблема в том, что результирующий массив дат моментов содержит повторяющийся день, который отражается в пользовательском интерфейсе нашего приложения (в календаре есть повторяющийся день).

Меня больше беспокоит то, что, поскольку библиотека ведет себя не так, как ожидалось, могут возникнуть более тонкие проблемы, которые останутся незамеченными, например, предполагаемая потеря данных, неверные запросы и т. Д.

Я буду работать над набором тестов в коде или что-то в этом роде, чтобы любопытные читатели могли попытаться решить эту проблему.

Связанная проблема # 4785 содержит ссылки на код:
пример момента https://runkit.com/embed/1r62d83amq7x
но luxon справляется с этим правильно
https://runkit.com/embed/t49achvensqf

Я знаю, что были недавние изменения летнего времени, поэтому мы должны проверить код, который сейчас находится на develop (но не выпущен). Или мы можем подождать до следующего выпуска, чтобы увидеть, все ли по-прежнему.

Поведение здесь выглядит так, как будто оно было изменено между моментом-часовым поясом v0.5.4 и v0.5.26.
В старой версии добавление 1 дня к '2018-08-11' давало 23:00 того же дня.
В новой версии в полночь отображается «2018-08-12». Но на самом деле этого времени не существует, и добавление 1 минуты возвращает нас на час, но это изменение летнего времени должно было добавить 1 час.

Старая версия ведет себя так, как описано в этой проблеме:
https://jsfiddle.net/eqyvuxht/1/

Новая версия, измененное поведение, но я все еще не прав?
https://jsfiddle.net/0h6atn4b/4/

Вот обходной путь:

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);
}

В этом месяце (сентябрь 2019 г.) мы столкнулись с той же проблемой, когда наши пользователи из Сантьяго, использующие наш выбор даты, видят календарь с датами 1, 2, 3, 4, 5, 6, 7, 7, 7, 7 для сентябрь месяц ... Под капотом мы также используем .add(1, 'days') . Это произойдет снова в следующем году в сентябре 2020 года, если к тому времени у нас не будет исправления.

Обходной путь помогает нам. Благодаря!

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

RobinvanderVliet picture RobinvanderVliet  ·  3Комментарии

dogukankotan picture dogukankotan  ·  3Комментарии

tanepiper picture tanepiper  ·  3Комментарии

Shoroh picture Shoroh  ·  3Комментарии

ninigix picture ninigix  ·  3Комментарии