Здравствуйте, меня зовут Дмитрий Карловский и я… очень стар. Годы уже не те, чтобы с лёгкостью разбираться в хитросплетениях мудрёных интерфейсов. Хочется чего-то относительно простого, но и достаточно мощного, чтобы не чувствовать себя калекой, который еле-еле пишет простейшую программу.
В любом приложении рано или поздно появляется необходимость работы со временем: распарсить, как-то модифицировать, что-то вычислить, сериализовать. Дата и время — это довольно сложные штуки, которые подстраиваются под солнечные, лунные и земные циклы одновременно. При этом в году может быть разное число дней, а в дне — разное число часов, даже в минуте не всегда 60 секунд. Из-за этого работа со временем требует от программиста повышенной аккуратности и всё-равно баги будут всплывать ещё очень долго.
Нет, я слишком стар для того, чтобы считать года миллисекундами — скоро мой возраст будет исчисляться уже миллиардами секунд. Пришло время воспользоваться чем-то более высокоуровневым. Тем, что наши предки называли стандартом ISO8601, но многие до сих пор не в курсе что это такое и через какое место это стоит употреблять.
Далее вы узнаете, как я избавился от геморроя путём смены городского минивена на спортивный велосипед :-)
В любом JS движке есть стандартное api для работы со временем — Date. Пусть его название не вводит вас в заблуждение: объекты Date — это не даты, а самые что ни на есть метки времени (моменты), отмеренные в миллисекундах от начала эпохи UNIX. API это предоставляет объектный интерфейс, позволяющий получить для момента различную информацию: от временных компонент (год, месяц, день, час, минута, секунда), до соответствующего ей дня недели. К сожалению, информация эта доступна либо для локального времени, либо для UTC. Если вам нужны другие часовые пояса, то у меня для вас плохие новости — Date может лишь распарсить iso8601 строку вида «2015-07-20T00:22:32+01:00», получить из неё метку времени и благополнучно забыть о часовых поясах как о страшном сне. Ну и чёрт бы с ним, если бы не пара нюансов:
1. Иногда часовой пояс имеет значение. Например, когда вы пишете серверное приложение, которое должно понимать когда у клиента утро, а когда вечер, когда ещё вчера, а когда уже завтра. То есть нужна возможность полноценной работы с любым часовым поясом.
2. Иногда часовой пояс только мешает. Например, когда вы рисуете календарик и оперируете датами безотносительно времени, то вмешательство часовых поясов запросто может попортить вам кровушки. То есть нужна возможность не указывать те или иные компоненты времени, если в них нет необходимости.
В ISO8601 если вы пишете «2015-07-20», то это 20 июля в любом часовом поясе. Моменты начала и конца этого дня в разных часовых поясах тем не менее будут различны. Если же вы воспользуетесь «new Date( '2015-07-20' )», то получите метку времени начала этой даты по UTC: «2015-07-20T00:00:00.000Z», а если напишете казалось бы эквивалентный код «new Date( 2015, 06, 20 )», то результатом будет уже «2015-07-19T21:00:00.000Z». Date развивался стихийно, как и все api в javascript, так что не стоит удивляться таком разброду и множеству способов сделать одно и тоже, но чуть-чуть по разному, а также куче бесполезных методов.
Популярная библиотека MomentJS пытается решить проблему беспорядочного интерфейса, удваивая при этом число методов, которые вы никогда не будете использовать. Нет, ну правда, зачем кому-то в трезвом уме и здоровой памяти использовать ASP.NET JSON Date вида "/Date(1198908717056-0700)/"? И не смотря на то, что она реализует много необходимых вещей, которых вообще нет в нативной Date, всё же она имеет ту же родовую травму, так как является всего лишь обёрткой над нативным api, которое предоставляет лишь абстракцию «метки времени». Так что «moment('2015-07-20')» вернёт вам метку «2015-07-19T21:00:00.000Z» со всеми вытекающими отсюда проблемами.
Другая родовая травма, свойственная обоим api — это мутабельность объектов. Если вы собираетесь как-то изменить объект, то вы должны не забыть склонировать его, иначе где-то в другом конце приложения у вас внезапно может всё сломаться.
Но самое печальное — это то, что MomentJS без плагинов весит аж 100 килобайт и при этом тормозит как болтающийся на околосветовой скорости близнец, не смотря даже на пятидесятикратное ускорение:
Ну как, чувствуете жжение чуть ниже спины? Тогда приступим к лечению.
Прежде всего стоит определиться как хранить данные внутри. Дата может быть указана со временем и без. Время может быть указано со смещением и без. Как дата так и время могут быть указаны как в полной так и обрезанной форме (год-месяц, например, или час-минута). То есть любая компонента времени может отсутствовать, если её значение не имеет смысла. Чтобы закодировать «Июль 2015-го», нам нужно только две компоненты: год, месяц и больше ничего. То есть имеет смысл хранить компоненты в отдельных полях, что несколько увеличит потребление памяти, но с другой стороны благотворно скажется на скорости работы.
Далее, стоит как следует разобраться в формате iso8601. Ведь незачем изобретать колесо, когда есть неплохой стандарт в котором уже много предусмотрено. В частности, он позволяет описывать моменты времени с различной точностью (от миллисекунд до годов), временные продолжительности в различных единицах измерения (от секунд до годов), временные промежутки в различных формах (начало-конец, начало-продолжительность, конец-продолжительность) и даже повторяющиеся промежутки (но они мало пригодны, к сожалению).
Теперь мы готовы написать библиотеку $jin.time, которая предоставляет 3 функции создающие соответствующе своим именам объекты: moment, duration, range. Каждая из них способна принимать параметры для конструирования в различных JSON представляениях: в виде iso8601 строки, в виде массива из значений компонент, в виде конфигурационного объекта вида { имя_компонента: значение_компонента }. Кроме того, моменты могут создаваться из нативных Date объектов и временных меток, а продолжительности из числа миллисекунд.
Для иллюстрации, давайте посчитаем сколько же мне уже стукнуло лет.
Я пока не нашёл элегантного алгоритма, чтобы вычислять продолжительности сразу в нужных единицах с учётом високосности, летнего времени и тп. Если у вас есть идеи на этот счёт — буду рад их услышать. А пока просто выдаётся число секунд.
Усложним задачу: какой день недели будет, когда мне стукнет миллиард секунд?
А который час сейчас в Японии?
А давайте распечатаем все дни недели в текущем месяце по порядку:
Далее я, пожалуй, оставлю вас с со страничкой, где можно поиграть с $jin.time и буду с нетерпением ждать жёсткую критику, ведь с тех пор как я начал пользоваться этой библиотекой, на моей голове изрядно поубавилось седых волос, кожа на ней стала гладкая и шелковистая, а без геморроя жить стало даже немного скучно :-)
В любом приложении рано или поздно появляется необходимость работы со временем: распарсить, как-то модифицировать, что-то вычислить, сериализовать. Дата и время — это довольно сложные штуки, которые подстраиваются под солнечные, лунные и земные циклы одновременно. При этом в году может быть разное число дней, а в дне — разное число часов, даже в минуте не всегда 60 секунд. Из-за этого работа со временем требует от программиста повышенной аккуратности и всё-равно баги будут всплывать ещё очень долго.
Нет, я слишком стар для того, чтобы считать года миллисекундами — скоро мой возраст будет исчисляться уже миллиардами секунд. Пришло время воспользоваться чем-то более высокоуровневым. Тем, что наши предки называли стандартом ISO8601, но многие до сих пор не в курсе что это такое и через какое место это стоит употреблять.
Далее вы узнаете, как я избавился от геморроя путём смены городского минивена на спортивный велосипед :-)
В любом JS движке есть стандартное api для работы со временем — Date. Пусть его название не вводит вас в заблуждение: объекты Date — это не даты, а самые что ни на есть метки времени (моменты), отмеренные в миллисекундах от начала эпохи UNIX. API это предоставляет объектный интерфейс, позволяющий получить для момента различную информацию: от временных компонент (год, месяц, день, час, минута, секунда), до соответствующего ей дня недели. К сожалению, информация эта доступна либо для локального времени, либо для UTC. Если вам нужны другие часовые пояса, то у меня для вас плохие новости — Date может лишь распарсить iso8601 строку вида «2015-07-20T00:22:32+01:00», получить из неё метку времени и благополнучно забыть о часовых поясах как о страшном сне. Ну и чёрт бы с ним, если бы не пара нюансов:
1. Иногда часовой пояс имеет значение. Например, когда вы пишете серверное приложение, которое должно понимать когда у клиента утро, а когда вечер, когда ещё вчера, а когда уже завтра. То есть нужна возможность полноценной работы с любым часовым поясом.
2. Иногда часовой пояс только мешает. Например, когда вы рисуете календарик и оперируете датами безотносительно времени, то вмешательство часовых поясов запросто может попортить вам кровушки. То есть нужна возможность не указывать те или иные компоненты времени, если в них нет необходимости.
В ISO8601 если вы пишете «2015-07-20», то это 20 июля в любом часовом поясе. Моменты начала и конца этого дня в разных часовых поясах тем не менее будут различны. Если же вы воспользуетесь «new Date( '2015-07-20' )», то получите метку времени начала этой даты по UTC: «2015-07-20T00:00:00.000Z», а если напишете казалось бы эквивалентный код «new Date( 2015, 06, 20 )», то результатом будет уже «2015-07-19T21:00:00.000Z». Date развивался стихийно, как и все api в javascript, так что не стоит удивляться таком разброду и множеству способов сделать одно и тоже, но чуть-чуть по разному, а также куче бесполезных методов.
Популярная библиотека MomentJS пытается решить проблему беспорядочного интерфейса, удваивая при этом число методов, которые вы никогда не будете использовать. Нет, ну правда, зачем кому-то в трезвом уме и здоровой памяти использовать ASP.NET JSON Date вида "/Date(1198908717056-0700)/"? И не смотря на то, что она реализует много необходимых вещей, которых вообще нет в нативной Date, всё же она имеет ту же родовую травму, так как является всего лишь обёрткой над нативным api, которое предоставляет лишь абстракцию «метки времени». Так что «moment('2015-07-20')» вернёт вам метку «2015-07-19T21:00:00.000Z» со всеми вытекающими отсюда проблемами.
Другая родовая травма, свойственная обоим api — это мутабельность объектов. Если вы собираетесь как-то изменить объект, то вы должны не забыть склонировать его, иначе где-то в другом конце приложения у вас внезапно может всё сломаться.
Но самое печальное — это то, что MomentJS без плагинов весит аж 100 килобайт и при этом тормозит как болтающийся на околосветовой скорости близнец, не смотря даже на пятидесятикратное ускорение:
if (0 < m.year() && m.year() <= 9999) {
if ('function' === typeof Date.prototype.toISOString) {
// native implementation is ~50x faster, use it when we can
return this.toDate().toISOString();
} else {
return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
}
} else {
return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
}
Ну как, чувствуете жжение чуть ниже спины? Тогда приступим к лечению.
Прежде всего стоит определиться как хранить данные внутри. Дата может быть указана со временем и без. Время может быть указано со смещением и без. Как дата так и время могут быть указаны как в полной так и обрезанной форме (год-месяц, например, или час-минута). То есть любая компонента времени может отсутствовать, если её значение не имеет смысла. Чтобы закодировать «Июль 2015-го», нам нужно только две компоненты: год, месяц и больше ничего. То есть имеет смысл хранить компоненты в отдельных полях, что несколько увеличит потребление памяти, но с другой стороны благотворно скажется на скорости работы.
Далее, стоит как следует разобраться в формате iso8601. Ведь незачем изобретать колесо, когда есть неплохой стандарт в котором уже много предусмотрено. В частности, он позволяет описывать моменты времени с различной точностью (от миллисекунд до годов), временные продолжительности в различных единицах измерения (от секунд до годов), временные промежутки в различных формах (начало-конец, начало-продолжительность, конец-продолжительность) и даже повторяющиеся промежутки (но они мало пригодны, к сожалению).
Теперь мы готовы написать библиотеку $jin.time, которая предоставляет 3 функции создающие соответствующе своим именам объекты: moment, duration, range. Каждая из них способна принимать параметры для конструирования в различных JSON представляениях: в виде iso8601 строки, в виде массива из значений компонент, в виде конфигурационного объекта вида { имя_компонента: значение_компонента }. Кроме того, моменты могут создаваться из нативных Date объектов и временных меток, а продолжительности из числа миллисекунд.
Для иллюстрации, давайте посчитаем сколько же мне уже стукнуло лет.
Math.floor( $jin.time.range( '1984-08-04/' ).duration.second / 60 / 60 / 24 / 365 )
Я пока не нашёл элегантного алгоритма, чтобы вычислять продолжительности сразу в нужных единицах с учётом високосности, летнего времени и тп. Если у вас есть идеи на этот счёт — буду рад их услышать. А пока просто выдаётся число секунд.
Усложним задачу: какой день недели будет, когда мне стукнет миллиард секунд?
$jin.time.moment['ru']( '1984-08-04' ).shift({ second : 1e9 }).toString( 'WeekDay' )
А который час сейчас в Японии?
$jin.time.moment().toOffset( '+09:00' ).toString( 'hh:mm' )
А давайте распечатаем все дни недели в текущем месяце по порядку:
var current = $jin.time.moment().merge({ day : 0 })
var end = current.shift( 'P1M' )
while( current < end ) {
console.log( current.toString( 'DD - WD' ).toLowerCase() )
current = current.shift( 'P1D' )
}
Далее я, пожалуй, оставлю вас с со страничкой, где можно поиграть с $jin.time и буду с нетерпением ждать жёсткую критику, ведь с тех пор как я начал пользоваться этой библиотекой, на моей голове изрядно поубавилось седых волос, кожа на ней стала гладкая и шелковистая, а без геморроя жить стало даже немного скучно :-)
Комментарии (31)
konsoletyper
20.07.2015 13:44+4А который час сейчас в Японии?
$jin.time.moment().toOffset( '+09:00' ).toString( 'hh:mm' )
Неправильно, потому что часовой пояс — это не просто смещение. Это целая история изменения смещения относительно UTC, т.к.:
1. Летнее время. Причём, летнее время задаётся как правило не точной датой, а правилом «последнее воскресенье октября» и т.п. А ещё бывает двойное летнее время.
2. Изменение смещения на законодательном уровне.
3. Переход города из одной временной зоны в другую, в т.ч. из-за перехода города в другую страну.
Такая информация хранится в timezone database, и обычно эта база уже есть в браузерах (или они её берут из ОС), а текущая временная зона на уровне ОС задаётся так же в терминах местности. Браузер имеет доступ ко всей этой информации, но не предоставляет её разработчику, а это может быть очень критично в некоторых сценариях. Библиотеки вроде moment.js обычно тащат с собой tzdata, и даже пытаются эвристически определить временную зону, выставленную в настройках у пользователя, но, разумеется, у них это получается не всегда хорошо. Но делать-то больше нечего, лучше способов всё равно нет. Вот и слушай после этого обвинения в сторону «дырявой Java», «дырявого Flash», и «этих никому не нужных аплетов».vintage Автор
20.07.2015 14:07+1Я намеренно даже не касался часовых поясов, ограничившись исключительно смещениями и арифметикой с ними.
xGromMx
20.07.2015 14:45+1А что вы скажете про эту альтернативу для moment? github.com/taylorhakes/fecha
nazarpc
Иногда (постоянно), я хочу чтобы в JS появилась встроенная функция
date()
, полностью аналогичная одноименной из PHP. Иначе любое форматирование дат это адские боль и унижение.А формат вида
YYYY-MM-DD hh:mm
убивает мои глаза и руки, сравните сY-m-d H:i
, так что очередная библиотека пройдет мимо меня. Хорошая попытка, но увы…vintage Автор
i — это мнимая единица?
nazarpc
Нет, https://secure.php.net/manual/en/function.date.php:
vintage Автор
Спасибо, распечатаю и повешу на стенку, а то вечно путаю.
konsoletyper
Вроде же это стандарт такой. Есть подраздел Unicode, называемый CLDR, где описано, как в различных локалях форматировать числа, даты, названия стран и т.п. Так там даты именно в таком виде идут. Такой формат, пусть и более многословный, значительно удобнее, т.к. нужно меньше букв помнить, а для разной «ширины» компонент просто писать одну и ту же букву несколько раз.
k12th
Формат вида Y-m-d H:i убивает мой мозг. Y — это, видимо, годы. Сколько их выведется, последняя цифра, две последние, все 4? m — это название месяца, его сокращенное название, его номер, его индекс от 0, август 08 или 8? Аналогично с d и с H. А i вообще ни с чем не ассоциируется.
vintage Автор
minute
k12th
Тогда почему бы, например, не minute?:) как и большинство PHP-шных решений, понять это невозможно, можно только зазубрить или постоянно смотреть в документацию.
NeLexa
встройте phpjs.org/functions/date и ни в чём себе не отказывайте
nazarpc
Я в курсе, вот только вместе с часовыми поясами получается огромный объем, а если учитывать что часовые пояса меняются — тогда всё становится совсем печально.