$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( '1984-08-04' ).shift({ second : 1000000000 }).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 и буду с нетерпением ждать жёсткую критику, ведь с тех пор как я начал пользоваться этой библиотекой, на моей голове изрядно поубавилось седых волос, кожа на ней стала гладкая и шелковистая, а без геморроя жить стало даже немного скучно :-)

© Habrahabr.ru