Полное руководство по манипулированию DateTime
Опубликовано: 2022-03-11Как разработчик программного обеспечения, вы не можете убежать от манипуляций с датами. Почти каждое приложение, создаваемое разработчиком, будет иметь некоторый компонент, в котором дата/время должны быть получены от пользователя, сохранены в базе данных и отображены пользователю.
Спросите любого программиста об его опыте работы с датами и часовыми поясами, и он, вероятно, поделится некоторыми историями о войне. Обработка полей даты и времени, конечно, не является ракетостроением, но часто может быть утомительной и подверженной ошибкам.
Существуют сотни статей на эту тему, однако большинство из них либо слишком академичны, сосредотачиваясь на мельчайших деталях, либо слишком фрагментарны, предоставляя короткие фрагменты кода без особых пояснений, сопровождающих их. Это подробное руководство по манипулированию DateTime должно помочь вам понять концепции программирования и лучшие практики, относящиеся к времени и дате, без необходимости просматривать море информации по этой теме.
В этой статье я собираюсь помочь вам разобраться в полях даты и времени и предложить некоторые рекомендации, которые помогут вам избежать ада даты/времени. Здесь мы рассмотрим некоторые ключевые понятия, необходимые для правильного управления значениями даты и времени, форматы, удобные для хранения значений DateTime и их передачи через API, и многое другое.
Во-первых, правильный ответ для производственного кода почти всегда состоит в том, чтобы использовать правильную библиотеку, а не создавать свою собственную. Потенциальные трудности с вычислением даты и времени, обсуждаемые в этой статье, являются лишь верхушкой айсберга, но о них все же полезно знать, независимо от того, есть ли у них библиотека.
Библиотеки DateTime помогают, если вы правильно их понимаете
Библиотеки дат во многих отношениях помогают облегчить вашу жизнь. Они значительно упрощают синтаксический анализ даты, арифметические и логические операции с датами, а также форматирование даты. Вы можете найти надежную библиотеку дат как для внешнего, так и для внутреннего интерфейса, которая сделает за вас большую часть тяжелой работы.
Однако мы часто используем библиотеки дат, не задумываясь о том, как на самом деле работает дата/время. Дата/время — сложная концепция. Баги, возникающие из-за его неправильного понимания, бывает крайне сложно понять и исправить даже с помощью библиотек дат. Как программист, вы должны понимать основы и уметь оценивать проблемы, которые решают библиотеки дат, чтобы извлечь из них максимальную пользу.
Кроме того, библиотеки даты/времени могут только помочь вам. Все библиотеки дат работают, предоставляя вам доступ к удобным структурам данных для представления DateTime. Если вы отправляете и получаете данные через REST API, вам в конечном итоге потребуется преобразовать дату в строку и наоборот, поскольку JSON не имеет собственной структуры данных для представления DateTime. Концепции, которые я изложил здесь, помогут вам избежать некоторых распространенных проблем, которые могут возникнуть при выполнении этих преобразований даты в строку и строки в дату.
Примечание. Несмотря на то, что я использовал JavaScript в качестве языка программирования, обсуждаемого в этой статье, это общие концепции, которые в значительной степени применимы практически ко всем языкам программирования и их библиотекам дат. Поэтому, даже если вы никогда раньше не писали ни строчки на JavaScript, не стесняйтесь продолжать чтение, так как я вряд ли предполагаю, что в статье есть какие-либо предварительные знания JavaScript.
Стандартизация времени
DateTime — это очень конкретный момент времени. Давайте подумаем об этом. Пока я пишу эту статью, часы на моем ноутбуке показывают 21 июля, 13:29. Это то, что мы называем «местным временем», время, которое я вижу на настенных часах вокруг себя и на своих наручных часах.
Плюс-минус несколько минут, если я попрошу свою подругу встретиться со мной в ближайшем кафе в 15:00, я могу ожидать увидеть ее там примерно в это время. Точно так же не было бы никакой путаницы, если бы вместо этого я сказал, например, «давайте встретимся через полтора часа». Обычно мы так говорим о времени с людьми, живущими в одном городе или часовом поясе.
Давайте представим другой сценарий: я хочу сказать другу, живущему в Уппсале, Швеция, что я хочу поговорить с ним в 17:00. Я отправляю ему сообщение: «Привет, Антон, давай поговорим в 17:00». Я тут же получаю ответ: «Ваше время или мое?»
Антон говорит мне, что он живет в часовом поясе Центральной Европы, то есть UTC+01:00. Я живу в UTC+05:45. Это означает, что когда я живу в 17:00, это 17:00 - 05:45 = 11:15 UTC, что переводится как 11:15 UTC + 01:00 = 12:15 в Уппсале, идеально подходит для обоих из нас.
Также помните о разнице между часовым поясом (центральноевропейское время) и смещением часового пояса (UTC+05:45). Страны также могут принять решение об изменении смещения часовых поясов для перехода на летнее время по политическим причинам. Почти каждый год в правила вносятся изменения по крайней мере в одной стране, а это означает, что любой код с запеченными этими правилами должен поддерживаться в актуальном состоянии — стоит учитывать, от чего зависит ваша кодовая база для каждого уровня вашего приложения.
Это еще одна веская причина, по которой мы рекомендуем, чтобы часовыми поясами в большинстве случаев занимался только внешний интерфейс. Если это не так, что происходит, когда правила, используемые вашим механизмом базы данных, не совпадают с правилами вашего внешнего или внутреннего интерфейса?
Эта проблема управления двумя разными версиями времени, относительно пользователя и относительно общепринятого стандарта, сложна, особенно в мире программирования, где точность является ключевым моментом, и даже одна секунда может иметь огромное значение. Первым шагом к решению этих проблем является хранение DateTime в формате UTC.
Стандартизация формата
Стандартизация времени — это прекрасно, потому что мне нужно хранить только время в формате UTC, и, пока я знаю часовой пояс пользователя, я всегда могу преобразовать его в его время. И наоборот, если я знаю местное время пользователя и его часовой пояс, я могу преобразовать его в UTC.
Но даты и время могут быть указаны в различных форматах. В качестве даты вы можете написать «30 июля», или «30 июля», или «7/30» (или 30/7, в зависимости от того, где вы живете). Для времени вы можете написать «21:30» или «21:30».
Ученые со всего мира объединились для решения этой проблемы и выбрали формат для описания времени, который очень нравится программистам, потому что он короткий и точный. Нам нравится называть его «формат даты ISO», который является упрощенной версией расширенного формата ISO-8601 и выглядит следующим образом:
Вместо 00:00 или UTC мы используем «Z», что означает время Зулу, другое название UTC.
Манипуляции с датами и арифметика в JavaScript
Прежде чем мы начнем с лучших практик, мы узнаем об манипулировании датами с помощью JavaScript, чтобы понять синтаксис и общие концепции. Хотя мы используем JavaScript, вы можете легко адаптировать эту информацию к своему любимому языку программирования.
Мы будем использовать арифметику дат для решения распространенных проблем, связанных с датами, с которыми сталкивается большинство разработчиков.
Моя цель — сделать так, чтобы вам было удобно создавать объект даты из строки и извлекать из нее компоненты. Это то, с чем вам может помочь библиотека дат, но всегда лучше понять, как это делается за кулисами.
Как только мы испачкаем руки датой/временем, нам будет легче думать о проблемах, с которыми мы сталкиваемся, извлекать лучшие практики и двигаться вперед. Если вы хотите перейти к лучшим практикам, не стесняйтесь делать это, но я настоятельно рекомендую вам хотя бы бегло просмотреть раздел арифметики дат ниже.
Объект даты JavaScript
Языки программирования содержат полезные конструкции, облегчающие нашу жизнь. Объект Date в JavaScript — одна из таких вещей. Он предлагает удобные методы для получения текущей даты и времени, сохранения даты в переменной, выполнения арифметических операций с датами и форматирования даты в зависимости от языкового стандарта пользователя.
Из-за различий между реализациями браузеров и неправильной обработки летнего времени (DST) зависимость от объекта Date для критически важных приложений не рекомендуется, и вам, вероятно, следует использовать библиотеку DateTime, например Luxon, date-fns или dayjs. (Что бы вы ни использовали, избегайте когда-то популярного Moment.js — часто называемого просто moment , как он появляется в коде, — поскольку теперь он устарел.)
Но в образовательных целях мы будем использовать методы, предоставляемые объектом Date(), чтобы узнать, как JavaScript обрабатывает DateTime.
Получение текущей даты
const currentDate = new Date();Если вы ничего не передаете конструктору Date, возвращаемый объект даты содержит текущую дату и время.
Затем вы можете отформатировать его, чтобы извлечь только часть даты следующим образом:
const currentDate = new Date(); const currentDayOfMonth = currentDate.getDate(); const currentMonth = currentDate.getMonth(); // Be careful! January is 0, not 1 const currentYear = currentDate.getFullYear(); const dateString = currentDayOfMonth + "-" + (currentMonth + 1) + "-" + currentYear; // "27-11-2020"Примечание. Ловушка «Январь — это 0» распространена, но не универсальна. Стоит перепроверить документацию любого языка (или формата конфигурации: например, cron основан на 1), прежде чем вы начнете его использовать.
Получение текущей метки времени
Если вместо этого вы хотите получить отметку текущего времени, вы можете создать новый объект Date и использовать метод getTime().
const currentDate = new Date(); const timestamp = currentDate.getTime();В JavaScript отметка времени — это количество миллисекунд, прошедших с 1 января 1970 года.
Если вы не собираетесь поддерживать <IE8, вы можете использовать Date.now() для прямого получения метки времени без необходимости создавать новый объект Date.
Разбор даты
Преобразование строки в объект даты JavaScript выполняется разными способами.
Конструктор объекта Date принимает широкий спектр форматов даты:
const date1 = new Date("Wed, 27 July 2016 13:30:00"); const date2 = new Date("Wed, 27 July 2016 07:45:00 UTC"); const date3 = new Date("27 July 2016 13:30:00 UTC+05:45");Обратите внимание, что вам не нужно указывать день недели, потому что JS может определить день недели для любой даты.
Вы также можете передать год, месяц, день, часы, минуты и секунды в качестве отдельных аргументов:
const date = new Date(2016, 6, 27, 13, 30, 0);Конечно, вы всегда можете использовать формат даты ISO:
const date = new Date("2016-07-27T07:45:00Z");Однако вы можете столкнуться с проблемами, если не укажете часовой пояс явно!
const date1 = new Date("25 July 2016"); const date2 = new Date("July 25, 2016");Любой из них даст вам 25 июля 2016 года 00:00:00 по местному времени.
Если вы используете формат ISO, даже если вы укажете только дату, а не время и часовой пояс, он автоматически примет часовой пояс как UTC.
Это означает, что:
new Date("25 July 2016").getTime() !== new Date("2016-07-25").getTime() new Date("2016-07-25").getTime() === new Date("2016-07-25T00:00:00Z").getTime()Форматирование даты
К счастью, в современном JavaScript есть несколько удобных функций интернационализации, встроенных в стандартное пространство имен Intl , которые упрощают форматирование даты.
Для этого нам понадобятся два объекта: Date и Intl.DateTimeFormat , инициализированные нашими настройками вывода. Предположим, мы хотим использовать американский формат (M/D/YYYY), это будет выглядеть так:
const firstValentineOfTheDecade = new Date(2020, 1, 14); // 1 for February const enUSFormatter = new Intl.DateTimeFormat('en-US'); console.log(enUSFormatter.format(firstValentineOfTheDecade)); // 2/14/2020 Если вместо этого нам нужен голландский формат (D/M/YYYY), мы просто передаем другой код культуры в конструктор DateTimeFormat :
const nlBEFormatter = new Intl.DateTimeFormat('nl-BE'); console.log(nlBEFormatter.format(firstValentineOfTheDecade)); // 14/2/2020Или более длинная форма американского формата с прописанным названием месяца:
const longEnUSFormatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); console.log(longEnUSFormatter.format(firstValentineOfTheDecade)); // February 14, 2020 Теперь, если нам нужен правильный порядковый формат дня месяца, то есть «14-й» вместо просто «14», к сожалению, это требует некоторого обходного пути, потому что на момент написания этой статьи единственными допустимыми значениями day являются "numeric" или "2-digit" . Заимствуя версию кода Матиаса Байненса от Флавио Копеса, чтобы использовать для этого другую часть Intl , мы можем настроить вывод дня месяца с помощью formatToParts() :
const pluralRules = new Intl.PluralRules('en-US', { type: 'ordinal' }) const suffixes = { 'one': 'st', 'two': 'nd', 'few': 'rd', 'other': 'th' } const convertToOrdinal = (number) => `${number}${suffixes[pluralRules.select(number)]}` // At this point: // convertToOrdinal("1") === "1st" // convertToOrdinal("2") === "2nd" // etc. const extractValueAndCustomizeDayOfMonth = (part) => { if (part.type === "day") { return convertToOrdinal(part.value); } return part.value; }; console.log( longEnUSFormatter.formatToParts(firstValentineOfTheDecade) .map(extractValueAndCustomizeDayOfMonth) .join("") ); // February 14th, 2020 К сожалению, formatToParts вообще не поддерживается Internet Explorer (IE) на момент написания этой статьи, но все остальные настольные, мобильные и серверные технологии (например, Node.js) поддерживаются. Для тех, кому нужна поддержка IE и абсолютно необходимы порядковые номера, ответ ниже (или, лучше, правильная библиотека дат) дает ответ.
Если вам нужна поддержка старых браузеров, таких как IE до версии 11, форматирование даты в JavaScript сложнее, потому что в Python или PHP не было стандартных функций форматирования даты, таких как strftime .
Например, в PHP функция strftime("Today is %b %d %Y %X", mktime(5,10,0,12,30,99)) дает вам Today is Dec 30 1999 05:10:00 .
Вы можете использовать другую комбинацию букв, которым предшествует % , чтобы получить дату в разных форматах. (Осторожно, не во всех языках каждой букве присваивается одно и то же значение — в частности, «М» и «м» могут быть заменены минутами и месяцами.)
Если вы уверены в формате, который хотите использовать, лучше всего извлечь отдельные биты с помощью функций JavaScript, которые мы рассмотрели выше, и создать строку самостоятельно.
var currentDate = new Date (); var date = currentDate.getDate(); var month = currentDate.getMonth(); var year = currentDate.getFullYear();Мы можем получить дату в формате ММ/ДД/ГГГГ как
var monthDateYear = (month+ 1 ) + "/" + date + "/" + year;Проблема с этим решением заключается в том, что оно может давать непостоянную длину дат, потому что некоторые месяцы и дни месяца являются однозначными, а другие — двузначными. Это может быть проблематично, например, если вы отображаете дату в столбце таблицы, потому что даты не совпадают.
Мы можем решить эту проблему, используя функцию «pad», которая добавляет начальный 0.
function pad ( n ) { return n< 10 ? '0' +n : n; }Теперь мы получаем правильную дату в формате ММ/ДД/ГГГГ, используя:
var mmddyyyy = pad(month + 1 ) + "/" + pad(date) + "/" + year;Если вместо этого мы хотим ДД-ММ-ГГГГ, процесс аналогичен:
var ddmmyyyy = pad(date) + "-" + pad(month + 1 ) + "-" + year;Поднимем ставку и попробуем напечатать дату в формате «Месяц Дата, Год». Нам понадобится сопоставление индексов месяцев с именами:
var monthNames = [ "January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" ]; var dateWithFullMonthName = monthNames[month] + " " + pad(date) + ", " + year; Некоторым людям нравится отображать дату как 1 января 2013 года. Нет проблем, все, что нам нужно, это ordinal вспомогательной функции, который возвращает 1-й для 1, 12-й для 12, 103-й для 103 и т. д., а остальное просто:
var ordinalDate = ordinal(date) + " " + monthNames[month] + ", " + year;По объекту даты легко определить день недели, поэтому добавим это в:
var daysOfWeek = [ "Sun" , "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat" ]; ordinalDateWithDayOfWeek = daysOfWeek[currentDate.getDay()] + ", " + ordinalDate;Самое главное здесь то, что после того, как вы извлекли числа из даты, форматирование в основном связано со строками.
Изменение формата даты
Как только вы узнаете, как анализировать дату и форматировать ее, изменение даты из одного формата в другой — это просто вопрос их объединения.
Например, если у вас есть дата в формате 21 июля 2013 г. и вы хотите изменить формат на 21-07-2013, это можно сделать следующим образом:
const myDate = new Date("Jul 21, 2013"); const dayOfMonth = myDate.getDate(); const month = myDate.getMonth(); const year = myDate.getFullYear(); function pad(n) { return n<10 ? '0'+n : n } const ddmmyyyy = pad(dayOfMonth) + "-" + pad(month + 1) + "-" + year; // "21-07-2013"Использование функций локализации объекта даты JavaScript
Рассмотренные выше методы форматирования даты должны работать в большинстве приложений, но если вы действительно хотите локализовать форматирование даты, я предлагаю вам использовать метод toLocaleDateString() объекта Date :
const today = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', }); … дает нам что-то вроде 26 Jul 2016 .

При изменении языкового стандарта на «en-US» вместо этого отображается «26 июля 2016 г.». Обратите внимание, как изменилось форматирование, но параметры отображения остались прежними — очень полезная функция. Как показано в предыдущем разделе, более новый метод на основе Intl.DateTimeFormat работает очень похоже на этот, но позволяет повторно использовать объект форматирования, так что вам нужно установить параметры только один раз.
С toLocaleDateString() хорошей привычкой является всегда передавать параметры форматирования, даже если вывод выглядит нормально на вашем компьютере. Это может защитить пользовательский интерфейс от взлома в неожиданных локалях с очень длинными названиями месяцев или неуклюжего вида из-за коротких.
Если бы вместо этого я хотел полный месяц «июль», все, что я делаю, это меняю параметр месяца в параметрах на «длинный». JavaScript обрабатывает все за меня. Для en-US теперь я получаю 26 июля 2016 года.
Примечание. Если вы хотите, чтобы браузер автоматически использовал языковой стандарт пользователя, вы можете передать «undefined» в качестве первого параметра.
Если вы хотите показать числовую версию даты и не хотите возиться с MM/DD/YYYY против DD/MM/YYYY для разных языков, я предлагаю следующее простое решение:
const today = new Date().toLocaleDateString(undefined, { day: 'numeric', month: 'numeric', year: 'numeric', }); На моем компьютере это выводит 7/26/2016 . Если вы хотите убедиться, что месяц и дата состоят из двух цифр, просто измените параметры:
const today = new Date().toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric', }); Это выводит 07/26/2016 . Как раз то, что мы хотели!
Вы также можете использовать некоторые другие связанные функции, чтобы локализовать способ отображения времени и даты:
| Код | Выход | Описание |
|---|---|---|
| «4:21:38» | Отображать локализованную версию только времени |
| «04:21:38» | Отображение локализованного времени на основе предоставленных опций |
| «22.07.2016, 4:21:38» | Отображение даты и времени для региона пользователя |
| "22.07.2016, 04:21" | Отображение локализованной даты и времени на основе предоставленных параметров |
Вычисление относительных дат и времени
Вот пример добавления 20 дней к дате JavaScript (т. е. определения даты через 20 дней после известной даты):
const myDate = new Date("July 20, 2016 15:00:00"); const nextDayOfMonth = myDate.getDate() + 20; myDate.setDate(nextDayOfMonth); const newDate = myDate.toLocaleString(); Исходный объект даты теперь представляет дату через 20 дней после 20 июля, а newDate содержит локализованную строку, представляющую эту дату. В моем браузере newDate содержит «09.08.2016, 15:00:00».
Чтобы рассчитать относительные метки времени с более точной разницей, чем целые дни, вы можете использовать Date.getTime() и Date.setTime() для работы с целыми числами, представляющими количество миллисекунд с определенной эпохи, а именно с 1 января 1970 года. например, если вы хотите узнать, когда сейчас 17 часов:
const msSinceEpoch = (new Date()).getTime(); const seventeenHoursLater = new Date(msSinceEpoch + 17 * 60 * 60 * 1000);Сравнение дат
Как и во всем остальном, что связано с датой, сравнение дат имеет свои особенности.
Во-первых, нам нужно создать объекты даты. К счастью, <, >, <= и >= работают. Таким образом, сравнить 19 июля 2014 г. и 18 июля 2014 г. так же просто, как:
const date1 = new Date("July 19, 2014"); const date2 = new Date("July 28, 2014"); if(date1 > date2) { console.log("First date is more recent"); } else { console.log("Second date is more recent"); }Проверка на равенство сложнее, поскольку два объекта даты, представляющие одну и ту же дату, по-прежнему являются двумя разными объектами даты и не будут равны. Сравнение строк даты — плохая идея, потому что, например, «20 июля 2014 года» и «20 июля 2014 года» представляют одну и ту же дату, но имеют разные строковые представления. Фрагмент ниже иллюстрирует первый пункт:
const date1 = new Date("June 10, 2003"); const date2 = new Date(date1); const equalOrNot = date1 == date2 ? "equal" : "not equal"; console.log(equalOrNot); Это выведет not equal .
Этот конкретный случай можно исправить, сравнив целочисленные эквиваленты дат (их метки времени) следующим образом:
date1.getTime() == date2.getTime()Я видел этот пример во многих местах, но мне он не нравится, потому что обычно вы не создаете объект даты из другого объекта даты. Поэтому я чувствую, что этот пример важен только с академической точки зрения. Кроме того, для этого требуется, чтобы оба объекта Date ссылались на одну и ту же секунду, в то время как вам может понадобиться только знать, относятся ли они к одному и тому же дню, часу или минуте.
Давайте рассмотрим более практический пример. Вы пытаетесь сравнить, совпадает ли день рождения, введенный пользователем, с счастливой датой, которую вы получаете от API.
const userEnteredString = "12/20/1989"; // MM/DD/YYYY format const dateStringFromAPI = "1989-12-20T00:00:00Z"; const dateFromUserEnteredString = new Date(userEnteredString) const dateFromAPIString = new Date(dateStringFromAPI); if (dateFromUserEnteredString.getTime() == dateFromAPIString.getTime()) { transferOneMillionDollarsToUserAccount(); } else { doNothing(); }Оба представляют одну и ту же дату, но, к сожалению, ваш пользователь не получит миллион долларов.
Вот в чем проблема: JavaScript всегда предполагает, что часовой пояс соответствует тому, который предоставляет браузер, если явно не указано иное.
Это означает, что для меня new Date ("12/20/1989") создаст дату 1989-12-20T00:00:00+5:45 или 1989-12-19T18:15:00Z, которая не совпадает с 1989-12-20T00:00:00Z с точки зрения метки времени.
Невозможно изменить только часовой пояс существующего объекта даты, поэтому теперь наша цель — создать новый объект даты, но с UTC вместо местного часового пояса.
Мы будем игнорировать часовой пояс пользователя и использовать UTC при создании объекта даты. Есть два способа сделать это:
- Создайте строку даты в формате ISO из даты, введенной пользователем, и используйте ее для создания объекта Date. Использование допустимого формата даты ISO для создания объекта Date, в то же время делая намерение UTC по сравнению с локальным очень ясным.
const userEnteredDate = "12/20/1989"; const parts = userEnteredDate.split("/"); const userEnteredDateISO = parts[2] + "-" + parts[0] + "-" + parts[1]; const userEnteredDateObj = new Date(userEnteredDateISO + "T00:00:00Z"); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDateObj.getTime() == dateFromAPI.getTime(); // trueЭто также работает, если вы не укажете время, так как по умолчанию оно равно полуночи (т.е. 00:00:00Z):
const userEnteredDate = new Date("1989-12-20"); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDate.getTime() == dateFromAPI.getTime(); // trueПомните: если конструктору даты передается строка в правильном формате даты ISO ГГГГ-ММ-ДД, он автоматически предполагает UTC.
- JavaScript предоставляет удобную функцию Date.UTC(), которую можно использовать для получения временной отметки даты в формате UTC. Мы извлекаем компоненты из даты и передаем их в функцию.
const userEnteredDate = new Date("12/20/1989"); const userEnteredDateTimeStamp = Date.UTC(userEnteredDate.getFullYear(), userEnteredDate.getMonth(), userEnteredDate.getDate(), 0, 0, 0); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDateTimeStamp == dateFromAPI.getTime(); // true ...Нахождение разницы между двумя датами
Обычный сценарий, с которым вы столкнетесь, — найти разницу между двумя датами.
Мы обсудим два варианта использования:
Нахождение количества дней между двумя датами
Преобразуйте обе даты в отметку времени UTC, найдите разницу в миллисекундах и найдите эквивалентные дни.
const dateFromAPI = "2016-02-10T00:00:00Z"; const now = new Date(); const datefromAPITimeStamp = (new Date(dateFromAPI)).getTime(); const nowTimeStamp = now.getTime(); const microSecondsDiff = Math.abs(datefromAPITimeStamp - nowTimeStamp); // Math.round is used instead of Math.floor to account for certain DST cases // Number of milliseconds per day = // 24 hrs/day * 60 minutes/hour * 60 seconds/minute * 1000 ms/second const daysDiff = Math.round(microSecondsDiff / (1000 * 60 * 60 * 24)); console.log(daysDiff);Определение возраста пользователя по дате его рождения
const birthDateFromAPI = "12/10/1989";Примечание: у нас нестандартный формат. Прочтите документацию API, чтобы определить, означает ли это 12 октября или 10 декабря. Измените формат на ISO соответственно.
const parts = birthDateFromAPI.split("/"); const birthDateISO = parts[2] + "-" + parts[0] + "-" + parts[1]; const birthDate = new Date(birthDateISO); const today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); if(today.getMonth() < birthDate.getMonth()) { age--; } if(today.getMonth() == birthDate.getMonth() && today.getDate() < birthDate.getDate()) { age--; }Я знаю, что есть более краткие способы написания этого кода, но мне нравится писать его таким образом из-за абсолютной ясности логики.
Как избежать ада свиданий
Теперь, когда мы освоились с арифметикой дат, мы можем понять лучшие практики и причины для их следования.
Получение даты и времени от пользователя
Если вы получаете дату и время от пользователя, вы, скорее всего, ищете его локальную дату и время. В разделе арифметики дат мы видели, что конструктор Date может принимать дату различными способами.
Чтобы устранить любую путаницу, я всегда предлагаю создавать дату, используя new Date(year, month, day, hours, minutes, seconds, milliseconds) , даже если у вас уже есть дата в допустимом формате для синтаксического анализа. Если все программисты в вашей команде будут следовать этому простому правилу, будет чрезвычайно легко поддерживать код в долгосрочной перспективе, поскольку он настолько явный, насколько это возможно с конструктором Date .
Самое интересное, что вы можете использовать варианты, которые позволяют опустить любой из последних четырех параметров, если они равны нулю; т. е. new Date(2012, 10, 12) совпадает с new Date(2012, 10, 12, 0, 0, 0, 0) поскольку неуказанные параметры по умолчанию равны нулю.
Например, если вы используете средство выбора даты и времени, которое дает вам дату 2012-10-12 и время 12:30, вы можете извлечь части и создать новый объект Date следующим образом:
const dateFromPicker = "2012-10-12"; const timeFromPicker = "12:30"; const dateParts = dateFromPicker.split("-"); const timeParts = timeFromPicker.split(":"); const localDate = new Date(dateParts[0], dateParts[1]-1, dateParts[2], timeParts[0], timeParts[1]);Старайтесь не создавать дату из строки, если только она не находится в формате даты ISO. Вместо этого используйте метод Дата(год, месяц, дата, часы, минуты, секунды, микросекунды).
Получение только даты
Если вы получаете только дату, например дату рождения пользователя, лучше всего преобразовать формат в действительный формат даты ISO, чтобы исключить любую информацию о часовом поясе, которая может привести к смещению даты вперед или назад при преобразовании в UTC. Например:
const dateFromPicker = "12/20/2012"; const dateParts = dateFromPicker.split("/"); const ISODate = dateParts[2] + "-" + dateParts[0] + "-" + dateParts[1]; const birthDate = new Date(ISODate).toISOString(); В случае, если вы забыли, если вы создаете объект Date с входными данными в допустимом формате даты ISO (ГГГГ-ММ-ДД), по умолчанию будет использоваться UTC, а не часовой пояс браузера.
Сохранение даты
Всегда храните DateTime в формате UTC. Всегда отправляйте строку даты ISO или отметку времени на серверную часть.
Поколения программистов осознали эту простую истину после горького опыта, пытаясь показать пользователю правильное местное время. Сохранение локального времени в бэкенде — плохая идея, лучше позволить браузеру обрабатывать преобразование в локальное время во внешнем интерфейсе.
Кроме того, должно быть очевидно, что вы никогда не должны отправлять строку DateTime, такую как «20 июля 1989 г., 12:10», на серверную часть. Даже если вы также отправляете часовой пояс, вы увеличиваете усилия других программистов, чтобы понять ваши намерения и правильно проанализировать и сохранить дату.
Используйте toISOString() или toJSON() объекта Date для преобразования локального DateTime в UTC.
const dateFromUI = "12-13-2012"; const timeFromUI = "10:20"; const dateParts = dateFromUI.split("-"); const timeParts = timeFromUI.split(":"); const date = new Date(dateParts[2], dateParts[0]-1, dateParts[1], timeParts[0], timeParts[1]); const dateISO = date.toISOString(); $.post("http://example.com/", {date: dateISO}, ...)Отображение даты и времени
- Получите отметку времени или дату в формате ISO из REST API.
- Создайте объект
Date. - Используйте
toLocaleString()илиtoLocaleDateString()иtoLocaleTimeString()или библиотеку дат для отображения местного времени.
const dateFromAPI = "2016-01-02T12:30:00Z"; const localDate = new Date(dateFromAPI); const localDateString = localDate.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric', }); const localTimeString = localDate.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit', });Когда следует также сохранять местное время?
«Иногда важно знать часовой пояс, в котором произошло событие, и преобразование в один часовой пояс безвозвратно стирает эту информацию.
«Если вы проводите маркетинговую акцию и хотите знать, какие клиенты разместили заказы в обеденное время, заказ, который, как кажется, был размещен в полдень по Гринвичу, не очень полезен, если на самом деле он был размещен за завтраком в Нью-Йорке».
Если вы столкнетесь с такой ситуацией, было бы разумнее также сохранить местное время. Как обычно, мы хотели бы создать дату в формате ISO, но сначала нам нужно найти смещение часового пояса.
Функция getTimeZoneOffset() объекта Date сообщает нам количество минут, которое при добавлении к заданному местному времени дает эквивалентное время UTC. Я предлагаю преобразовать его в формат (+-)чч:мм, потому что это делает более очевидным, что это смещение часового пояса.
const now = new Date(); const tz = now.gettime zoneOffset();Для моего часового пояса +05:45 я получаю -345, это не только противоположный знак, но и такое число, как -345, может полностью озадачить бэкенд-разработчика. Итак, мы конвертируем это в +05:45.
const sign = tz > 0 ? "-" : "+"; const hours = pad(Math.floor(Math.abs(tz)/60)); const minutes = pad(Math.abs(tz)%60); const tzOffset = sign + hours + ":" + minutes;Теперь мы получаем остальные значения и создаем допустимую строку ISO, представляющую локальный DateTime.
const localDateTime = now.getFullYear() + "-" + pad(now.getMonth()+1) + "-" + pad(now.getDate()) + "T" + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds());Если вы хотите, вы можете обернуть UTC и местные даты в объект.
const eventDate = { utc: now.toISOString(), local: localDateTime, tzOffset: tzOffset, } Теперь, в бэкенде, если вы хотите узнать, произошло ли событие до полудня по местному времени, вы можете проанализировать дату и просто использовать getHours() .
const localDateString = eventDate.local; const localDate = new Date(localDateString); if(localDate.getHours() < 12) { console.log("Event happened before noon local time"); } Здесь мы не использовали tzOffset , но все же сохранили его, потому что он может понадобиться нам в будущем для целей отладки. На самом деле вы можете просто отправить смещение часового пояса и время UTC. Но мне также нравится хранить местное время, потому что в конечном итоге вам придется хранить дату в базе данных, а отдельное хранение локального времени позволяет вам напрямую запрашивать на основе поля, а не выполнять вычисления для получения локальной даты.
Иногда, даже если сохранен местный часовой пояс, вам может понадобиться отображать даты в определенном часовом поясе. Например, время для событий может иметь больше смысла в часовом поясе текущего пользователя, если они виртуальные, или в часовом поясе, где они будут физически происходить, если они не виртуальные. В любом случае стоит заранее поискать устоявшиеся решения для форматирования с явными названиями часовых поясов.
Конфигурация сервера и базы данных
Всегда настраивайте свои серверы и базы данных для использования часового пояса UTC. (Обратите внимание, что UTC и GMT — это не одно и то же: например, GMT может подразумевать переход на BST летом, а UTC — никогда.)
Мы уже видели, насколько болезненными могут быть преобразования часовых поясов, особенно когда они непреднамеренные. Всегда отправляйте UTC DateTime и настраивайте свои серверы так, чтобы они находились в часовом поясе UTC, и это может облегчить вашу жизнь. Ваш внутренний код будет намного проще и чище, так как ему не нужно будет выполнять какие-либо преобразования часовых поясов. DateTime data coming in from servers across the world can be compared and sorted effortlessly.
Code in the back end should be able to assume the time zone of the server to be UTC (but should still have a check in place to be sure). A simple configuration check saves having to think about and code for conversions every time new DateTime code is written.
It's Time for Better Date Handling
Date manipulation is a hard problem. The concepts behind the practical examples in this article apply beyond JavaScript, and are just the beginning when it comes to properly handling DateTime data and calculations. Plus, every helper library will come with its own set of nuances—which is even true of the eventual official standard support{target=”_blank”} for these types of operations.
The bottom line is: Use ISO on the back end, and leave the front end to format things properly for the user. Professional programmers will be aware of some of the nuances, and will (all the more decidedly) use well-supported DateTime libraries on both the back end and the front end. Built-in functions on the database side are another story, but hopefully this article gives enough background to make wiser decisions in that context, too.
