Вычисление времени BST из объекта Date?

Я уже рассмотрел несколько вопросов на похожие темы, но ни один из них не касается расчета часового пояса пункта назначения с учетом его летнего времени (летнего времени). Я пытаюсь написать простой виджет, который отображает текущее местное время определенного часового пояса для всех, кто посещает страницу. Интересующий часовой пояс — BST (что такое BST?), но если возможно, я хотел бы увидеть общую реализацию для любой локали. Вот моя попытка с использованием ванильного JavaScript:

function getBST() {
    var date = new Date(),
        utc = date.getTime() + date.getTimezoneOffset() * 60000,
        local = new Date(utc), // this is GMT, not BST
        day = local.getDate(),
        mon = local.getMonth() + 1,
        year = local.getFullYear(),
        hour = local.getHours(),
        minute = ('0' + local.getMinutes()).slice(-2),
        second = ('0' + local.getSeconds()).slice(-2),
        suffix = hour < 12 ? 'AM' : 'PM';

    hour = (hour - 24) % 12 + 12;

    return mon + '/' + day + '/' + year + ', ' + hour + ':' + minute + ':' + second + ' ' + suffix;
}

setInterval(function () {
  document.body.textContent = getBST();
}, 1000);

Основываясь на ответе @RobG, я написал этот код:

function resetDay(date) {
  date.setUTCMilliseconds(0);
  date.setUTCSeconds(0);
  date.setUTCMinutes(0);
  date.setUTCHours(1);

  return date;
}

function lastDay(date, day) {
  while (date.getUTCDay() !== day) {
    date.setUTCDate(date.getUTCDate() - 1);
  }

  return date;
}

function adjustDST(date, begin, end) {
  if (date >= begin && date < end) {
    date.setUTCHours(date.getUTCHours() + 1);
  }

  return date;
}

function updateBST() {
  var date = new Date();
  var begin = resetDay(new Date());

  begin.setUTCMonth(3, -1);
  begin = lastDay(begin, 0);

  var end = resetDay(new Date());

  end.setUTCMonth(10, -1);
  end = lastDay(end, 0);

  date = adjustDST(date, begin, end);

  var day = date.getUTCDate(),
    mon = date.getUTCMonth() + 1,
    year = date.getUTCFullYear(),
    hour = date.getUTCHours(),
    minute = ('0' + date.getUTCMinutes()).slice(-2),
    second = ('0' + date.getUTCSeconds()).slice(-2),
    suffix = hour < 12 ? 'AM' : 'PM';

  hour = (hour - 24) % 12 + 12;

  return mon + '/' + day + '/' + year + ', ' + hour + ':' + minute + ':' + second + ' ' + suffix;
}

setInterval(function () {
  document.body.textContent = updateBST();
}, 1000);

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


person Patrick Roberts    schedule 15.01.2016    source источник
comment
Я бы посоветовал взглянуть на библиотеку, например Moment и Часовой пояс момента. Даты и часовые пояса в JavaScript, как известно, сложны, и написание собственных может быть подвержено ошибкам.   -  person Heretic Monkey    schedule 15.01.2016
comment
@MikeMcCaughan—С датами и часовыми поясами в JavaScript, как известно, сложно…. Нет, это не так, просто информации очень мало, и люди не тратят время на то, чтобы ее понять. Даты на самом деле очень простые объекты.   -  person RobG    schedule 15.01.2016
comment
Итак, если они не так сложны, ответьте на вопрос: P. На чистом JavaScript, заметьте, без привязки к внешней библиотеке.   -  person Heretic Monkey    schedule 15.01.2016
comment
@MikeMcCaughan — сейчас у меня нет времени писать код, но у меня есть ответы. Я вернусь примерно через 10 часов.   -  person RobG    schedule 16.01.2016
comment
Чтобы быть педантичным, позвольте мне сказать, что BST - это конкретно UTC+1. Великобритания иногда находится по BST и иногда по Гринвичу. Если вы пытаетесь согласовать переходы для одного часового пояса, вы должны называть его британским или лондонским временем. IANA использует идентификатор Europe/London. Идея преобразования в BST заставила бы меня подумать, что вам нужен только UTC + 1, даже если BST не действует.   -  person Matt Johnson-Pint    schedule 16.01.2016
comment
@MattJohnson спасибо за разъяснение. Я просто имел в виду британское время.   -  person Patrick Roberts    schedule 16.01.2016
comment
Еще один момент: строка, которую вы создаете, имеет формат m/d/y, но в Великобритании они используют d/m/y.   -  person Matt Johnson-Pint    schedule 16.01.2016
comment
@RobG - трудно - это относительный термин в отношении понимания. Такие ребята, как вы и я, могут найти эти вещи легкими, но большинству это не так - отсюда и большое количество вопросов в этой области. Позвольте мне как-нибудь купить вам напиток на ваш выбор и рассказать вам несколько историй! Ваше здоровье! :)   -  person Matt Johnson-Pint    schedule 16.01.2016
comment
@PatrickRoberts - Еще раз просмотрите свой код - сравнение должно быть закрыто на начальной стороне: (date >= begin && date < end). Кроме того, время меняется в 1:00 UTC, а не в полночь, и убедитесь, что вы работаете только с датами с 1981 года. До этого правила перехода на летнее время были другими. (См. исходники tzdb или timeanddate.com, если вам нужны все подробности.)   -  person Matt Johnson-Pint    schedule 16.01.2016
comment
О, и я почти пропустил это - но вы рассчитываете только на основе текущего года - так что то, что вы разместили здесь, прекрасно, но если date на самом деле происходит откуда-то еще (например, из базы данных), тогда вы можете просматривать старые данные за предыдущий год, и в этом случае даты, которые вы выбрали для перехода на летнее время в этом году, не помогут. (начинаем понимать, почему библиотека может быть лучшим вариантом? Если нет, подумайте, что происходит в других часовых поясах, например, в южном полушарии, где переход на летнее время превышает границу года)   -  person Matt Johnson-Pint    schedule 16.01.2016
comment
@MattJohnson спасибо за все эти указатели. Я определенно исправлю проблему полуночи и проблему равенства, поскольку их легко исправить. Но остальные — крайние случаи, которые не применимы к моей ситуации, поэтому я проигнорирую их для своего решения.   -  person Patrick Roberts    schedule 16.01.2016
comment
Конечно - просто хотел позвать их для других, которые могут прийти и прочитать это обсуждение.   -  person Matt Johnson-Pint    schedule 16.01.2016
comment
@MattJohnson - отличные комментарии, я думаю, что даты намного проще, чем побитовые операторы и регулярные выражения с опережением и позади!!   -  person RobG    schedule 16.01.2016


Ответы (2)


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

Дата javascript состоит из значения времени в формате UTC и смещения часового пояса в зависимости от настроек хост-системы. Так что все, что вам нужно сделать, это применить смещение часового пояса, которое вы хотите, ко времени UTC, и вуаля, ваше время в любом часовом поясе.

Стандартной системы сокращения часовых поясов не существует, хотя существуют некоторые стандарты де-факто (например, часовой пояс IATA коды и часовые пояса IANA). Я предполагаю, что под BST вы имеете в виду британское летнее время, также известное как британское летнее время (BDT) и британское летнее время (BDST). Это также может быть стандартное время Бангладеш или стандартное время Бугенвиля, которые также известны как «BST».

Существуют различные библиотеки (например, Moment Timezone), которые используют коды IANA и могут укажите время для любого поддерживаемого часового пояса в течение (более или менее) любого времени.

BST начинается в 01:00 UTC в последнее воскресенье марта и заканчивается в 01:00 UTC в последнее воскресенье октября каждого года, поэтому алгоритм таков:

  1. Создайте новую дату на основе локальных настроек системы (например, new Date())
  2. Создайте даты начала и окончания BST на основе значений UTC (для этого года 2016-03-27T01:00:00Z и 2016-03-30T02:00:00Z)
  3. Посмотрите, попадает ли текущее время UTC в этот диапазон
  4. Если это так, добавьте 1 час ко времени UTC даты, созданной в # 1.
  5. Вывести отформатированную строку на основе значений даты в формате UTC.

Вот и все, единственная трудность — найти подходящие даты воскресенья. Вам вообще не нужно учитывать смещение местного часового пояса, поскольку все основано на UTC и объектах Date.

Сейчас у меня нет времени предоставлять код, так что попробуйте, и я свяжусь с вами примерно через 10 часов.

Редактировать

Итак, вот код. Первые две функции являются вспомогательными, одна получает последнее воскресенье месяца, другая форматирует строку ISO 8601 со смещением. Большая часть работы приходится на эти две функции. Надеюсь, комментариев достаточно, если требуется больше объяснений, просто спросите.

Я не включил миллисекунды в строку, не стесняйтесь добавлять их, если хотите, добавьте + '.' + ('00' + d.getUTCMilliseconds()).slice(-3) перед частью смещения отформатированной строки.

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

/* Return a Date for the last Sunday in a month
** @param {number} year - full year number (e.g. 2015)
** @param {number} month - calendar month number (jan=1)
** @returns {Date} date for last Sunday in given month
*/
function getLastSunday(year, month) {
  // Create date for last day in month
  var d = new Date(year, month, 0);
  // Adjust to previous Sunday
  d.setDate(d.getDate() - d.getDay());
  return d;
}

/* Format a date string as ISO 8601 with supplied offset
** @param {Date} date - date to format
** @param {number} offset - offset in minutes (+east, -west), will be
**                          converted to +/-00:00
** @returns {string} formatted date and time
**
** Note that javascript Date offsets are opposite: -east, +west but 
** this function doesn't use the Date's offset.
*/
function formatDate(d, offset) {
  function z(n){return ('0'+n).slice(-2)}
  // Default offset to 0
  offset = offset || 0;
  // Generate offset string
  var offSign = offset < 0? '-' : '+';
  offset = Math.abs(offset);
  var offString = offSign + ('0'+(offset/60|0)).slice(-2) + ':' + ('0'+(offset%60)).slice(-2);
  // Generate date string
  return d.getUTCFullYear() + '-' + z(d.getUTCMonth()+1) + '-' + z(d.getUTCDate()) +
         'T' + z(d.getUTCHours()) + ':' + z(d.getUTCMinutes()) + ':' + z(d.getUTCSeconds()) +
         offString;
}

/* Return Date object for current time in London. Assumes
** daylight saving starts at 01:00 UTC on last Sunday in March
** and ends at 01:00 UTC on the last Sunday in October.
** @param {Date} d - date to test. Default to current
**                   system date and time
** @param {boolean, optional} obj - if true, return a Date object. Otherwise, return
**                        an ISO 8601 formatted string
*/
function getLondonTime(d, obj) {
  // Use provided date or default to current date and time
  d = d || new Date();

  // Get start and end dates for daylight saving for supplied date's year
  // Set UTC date values and time to 01:00
  var dstS = getLastSunday(d.getFullYear(), 3);
  var dstE = getLastSunday(d.getFullYear(), 10);
  dstS = new Date(Date.UTC(dstS.getFullYear(), dstS.getMonth(), dstS.getDate(),1));
  dstE = new Date(Date.UTC(dstE.getFullYear(), dstE.getMonth(), dstE.getDate(),1));
  // If date is between dstStart and dstEnd, add 1 hour to UTC time
  // and format using +60 offset
  if (d > dstS && d < dstE) {
    d.setUTCHours(d.getUTCHours() +1);
    return formatDate(d, 60);
  }
  // Otherwise, don't adjust and format with 00 offset
  return obj? d : formatDate(d);
}

document.write('Current London time: ' + getLondonTime(new Date()));

person RobG    schedule 15.01.2016
comment
Спасибо за алгоритм, я не знал, что вы можете игнорировать локальное смещение, потому что большинство решений, которые я видел, действительно его учитывают. - person Patrick Roberts; 16.01.2016
comment
Да, просто методы, отличные от UTC, используют его по умолчанию. ;-) - person RobG; 16.01.2016
comment
Итак, вам нужна внешняя ссылка для этих смещений часовых поясов (особенно для общего случая, когда вы не знаете дату заранее, поскольку даты изменения смещения зависят от исторического года), в чем заключается трудность , и почему я упомянул, что это сложно, и почему я сослался на ту же библиотеку, что и вы, Moment Timezone. - person Heretic Monkey; 16.01.2016
comment
Я обновил свой вопрос, чтобы показать свою ванильную реализацию предоставленного вами алгоритма. В очередной раз благодарим за помощь. - person Patrick Roberts; 16.01.2016
comment
@RobG. Проблема с этим подходом заключается в том, что объект Date в JavaScript по-прежнему будет думать, что он находится в местном часовом поясе, и, следовательно, он может решить настроить значения, которые находятся рядом с переходами на летнее время в местном часовом поясе. Если правила перехода на летнее время для целевого часового пояса существенно отличаются, это может привести к неверному результату. По сути, недостаток находится между шагами 4 и 5 - если вы добавите за пределами объекта Date, все в порядке, но если вы вернетесь к объекту Date для получения строкового результата, это может привести к сбою. (Момент избегает этого BTW) - person Matt Johnson-Pint; 16.01.2016
comment
К счастью, код, опубликованный с обновлением OP, не страдает от этой проблемы, поскольку он сам строит строку из частей из свойств на основе UTC, вместо того, чтобы полагаться на объект Date для этого. Это довольно обходной путь, но он работает. Просто не передавайте этот объект Date какому-то другому API, иначе он, вероятно, сделает с ним что-то не то. - person Matt Johnson-Pint; 16.01.2016
comment
@MattJohnson — я думаю, что мой алгоритм работает нормально, поскольку я использую только значения UTC. Это также можно сделать, отрегулировав значение времени с помощью смещения часового пояса и обработав его как дату UTC, но я хотел этого избежать. Только слегка протестировано. - person RobG; 17.01.2016
comment
Да, согласен. Единственная опасность с общим подходом (иногда называемым сдвигом эпохи) заключается в том, что вы помещаете его обратно в объект Date и полагаетесь на этот объект для форматирования с помощью toString или других методов. Иногда люди передают объект Date в другую библиотеку или в элемент управления пользовательского интерфейса, и в этих сценариях он перехватывает крайние случаи вблизи границ перехода на летнее время. Так что при очень осторожном использовании это работает, но в общем случае может вызвать проблемы. - person Matt Johnson-Pint; 19.01.2016

Это невозможно в чистом JS, просто используя методы Date, но для этого есть (конечно) библиотека: http://momentjs.com/timezone/

Пример:

moment.tz("Europe/London").format(); // 2016-01-15T09:21:08-07:00
person Ilya    schedule 15.01.2016
comment
Это тоже не мой минус, но это возможно, и moment.js и Moment Timezone - это чистый JS. ;-) - person RobG; 16.01.2016
comment
@RobG они могут быть чистыми, но они не ванильные - person Patrick Roberts; 16.01.2016
comment
@RobG Ну, каждая библиотека JS - это чистый JS, я думаю ... может быть, чистый - не лучший выбор слова, что бы вы предложили? - person Ilya; 16.01.2016
comment
@ Илья Прошу прощения, я не знаю, кто вас минусует, но я больше не могу принять ваш ответ, потому что RobG предоставил именно то, что мне было нужно. - person Patrick Roberts; 16.01.2016
comment
Что ж, это отстой, потому что на практике перекодирование moment.js — очень плохая идея, если только у вас нет слишком много свободного времени. - person Ilya; 16.01.2016
comment
Прежде чем вы добавили пример, ваш ответ был просто в другом месте, что не является хорошим ответом. - person Heretic Monkey; 16.01.2016
comment
@MikeMcCaughan, хорошо, хорошо, но полезнее комментировать, чтобы ОП мог улучшить свой ответ, чем просто минусовать без объяснения причин. Кроме того, я думаю, что легко кодировать себя - это заблуждение. Посмотрите на код moment.js... - person Ilya; 16.01.2016
comment
Я согласен с тем, что не кодирую его самостоятельно (см. мои комментарии к вопросу и принятый ответ). Существует много информации о том, что делает ответ хорошим (и плохим) (например, Как ответить), поэтому Я не чувствовал, что мне нужно повторять это. - person Heretic Monkey; 16.01.2016
comment
@MikeMcCaughan чего не хватает, так это того, что делает полезную часть отрицательного голоса ;-). Во всяком случае, я обновил ответ из его грубой первой версии. Спасибо за объяснение. - person Ilya; 16.01.2016
comment
@Ilya - FWIW - я проголосовал. Но вы должны использовать Europe/London, чтобы соответствовать OP, а также вы можете опустить объект date, так как moment.tz по умолчанию соответствует текущему времени, когда вы опускаете первый параметр. - person Matt Johnson-Pint; 16.01.2016