Казалось бы, зачем нужна еще одна библиотека для работы с датами и временем когда есть всем известная библиотека Moment?! Тем интереснее, что альтернатива предложена самой командой Moment.

Библиотека Luxon заявлена как мощное, современное и удобное средство для работы с датами и временем в JavaScript. Библиотеку создал Айзек Камброн (Isaac Cambron), входящий в команду разработчиков Moment с 2013 года.

У автора было много идей по развитию Moment, которые он не мог сделать в рамках существующего кода. Вот основные моменты, которые хотелось реализовать:

  • опробовать некоторые идеи как сделать API более логичным (но эти идеи были не совместимы с подходом, принятым в Moment),
  • реализовать «из коробки» работу с временными зонами без дополнительных расширений,
  • полностью переосмыслить работу с интернационализацией с учетом появления Intl API,
  • перейти на современный набор инструментов и подходов при формирования JS кода.

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

Принципы Luxon


  1. Цепочки вызовов как в Moment.
  2. Все типы иммутабельными.
  3. Более ясный и очевидный API: для разных объектов — разные методы с четко определенными параметрами.
  4. Intl API для обеспечения интернационализации (откат к английскому варианту, если браузер не поддерживает Intl API).
  5. Intl API для обеспечения работы с временными зонами.
  6. Более полная поддержка расчета длительности.
  7. Встроенная поддержка работы с интервалами.
  8. Инлайн документация кода.

Эти принципы привели к следующим улучшениям:

  • Код Luxon намного проще в понимании и отладке.
  • Использование встроенных возможностей браузера для интернационализации улучшает поведение библиотеки и, опять же, облегчает отладку.
  • Поддержка временных зон реализована лучше чем в любой другой JS библиотеке.
  • Luxon предоставляет одновременно простой и очень мощный инструмент для работы с длительностью.
  • У библиотеки хорошая документация.

Но у Luxon есть и свои недостатки:

  • Упор на использование встроенных возможностей браузера приводит к сложностям в поддержке старых браузеров.
  • Некоторые возможности интернационализации, которые еще не поддерживаются браузерами не реализованы и в библиотеке (необходимо ожидать, когда в браузерах такая поддержка появится).
  • Реализация Intl API в разных браузерах может различаться, соответственно, будет различаться и поведение Luxon.

Установка


Luxon предоставляет модули под все современные платформы JavaScript.

В документации есть полный список поддерживаемых браузеров с указанием ограничений применения. Для браузеров у которых отсутствует или ограничена поддержка Intl рекомендуется использовать полифил (в частности это касается IE 10 или 11).

При работе с Node.js (6+), если нужна работа с локалями, то потребуется дополнительно установить пакет full-icu и задать переменную окружения, чтобы включить использование этого пакета.

Стандартный способ установки из npm:
npm install --save luxon

У Luxon есть поддержка как TypeScript так и Flow, так же есть модуль в формате ES6.

Быстрый обзор


Библиотека Luxon состоит из пяти основных классов:

DateTime — дата и время с часовым поясом и настройками отображения, а так же сопутствующие методы.
Duration — период времени (длительность), например, «2 месяца» или «1 день, 3 часа».
Info — статические методы для получения общих данных о времени и дате.
Interval — интервал времени и методы для работы с ним.
Settings — статические методы, которые задают общее поведение Luxon.

import {DateTime, Duration, Info, Interval, Settings} from 'luxon';

Ваш первый DateTime


Самый важный класс в Luxon — DateTime. DateTime представляет дату+время вместе с часовым поясом и локалью. Вот так можно задать 15 мая 2017 года 08:30 в локальном часовом поясе:

var dt = DateTime.local(2017, 5, 15, 8, 30);

Вот вызов для определения текущего времени:

var now = DateTime.local();

Создание из объекта


DateTime.fromObject({
  month:12, 
  day: 22, 
  hour: 12, 
  minutes: 20, 
  zone: 'Europe/Kaliningrad'
}); //=> 2018-12-22T12:20:00.000+02:00

Создание из строки в формате ISO 8601


DateTime.fromISO("2017-05-15");          //=> May 15, 2017 at 0:00
DateTime.fromISO("2017-05-15T08:30:00"); //=> May 15, 2017 at 8:30

При преобразовании в строку Luxon так же возвращает строку в формате ISO 8601:

DateTime.local().toString(); //=> "2018-12-18T20:58:29.995+03:00"

Получение отдельных компонентов:


var dt = DateTime.local();
dt.year;     //=> 2018
dt.month;    //=> 12
dt.day;      //=> 18
dt.second;   //=> 27
dt.weekday;  //=> 2
dt.zoneName; //=> "Europe/Moscow"
dt.offset;   //=> 180
dt.daysInMonth;  //=> 31

Вывод в форматированном виде


Luxon имеет множество методов для преобразования DateTime в строку, два из них наиболее важны toLocaleString и toISO, первый преобразует в формат с учетом лакали браузера, а второй готовит текст для программной обработки (к примеру, для передачи на сервер):

dt.toLocaleString(); //=> "18.12.2018"
dt.toLocaleString(DateTime.DATETIME_MED); //=> "18 дек. 2018 г., 21:46"
dt.toISO(); //=> "2018-12-18T21:46:55.013+03:00"

Для форматированного вывода в Luxon есть два десятка готовых «пресетов» (таких как DATETIME_MED и TIME_WITH_LONG_OFFSET).

Так же можно сформировать собственный вариант форматирования на основе токенов:

dt.setLocale('ru').toFormat('d MMMM tt - ZZZZZ'); 
//=> "18 декабря 21:46:55 - Москва, стандартное время"

Преобразования DateTime


Важное замечание: объекты Luxon — иммутабельны, т.е. любые изменяющие методы примененные к ним возвращают измененную копию не изменяя исходного объекта. Поэтому все термины в этой статье (как и в документации Luxon) такие как «изменить», «установить», «переопределить» надо понимать как «создать новый экземпляр с другими свойствами».

Математические преобразования


var dt = DateTime.local(2018, 12, 18, 20, 30); //=> "18.12.2018, 20:30"
dt.plus({hours: 3, minutes: 2}); //=> "18.12.2018, 23:32"
dt.minus({days: 7}); //=> "11.12.2018, 20:30"
dt.startOf('day'); //=> "18.12.2018, 0:00"
dt.endOf('hour');  //=> "18.12.2018, 20:00"

Переопределение отдельных параметров


var dt = DateTime.local();
dt.set({hour: 3}).hour   //=> 3

Преобразования Intl


Luxon поддерживает несколько разных преобразований Intl, одним из наиболее важных является форматирование под различные локали:

var dt = DateTime.local();
var f = {month: 'long', day: 'numeric'};
dt.setLocale('fr').toLocaleString(f); //=> "18 decembre"
dt.setLocale('en-GB').toLocaleString(f); //=> "18 December"
dt.setLocale('en-US').toLocaleString(f); //=> "December 18"

Класс Info может возвращать списки месяцев и дней недели в заданной локали:

Info.months('long', {locale: 'it'}); //=> ["gennaio", "febbraio", "marzo", ...]
Info.weekdays ('short', {locale: 'de'}); //=> ["Mo", "Di", "Mi", ...]

Часовые пояса


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

var dt = DateTime.local(2018, 12, 18, 20, 00); //=> 2018-12-18T20:00:00.000+03:00
dt.zone.name; //=> "Europe/Moscow"
dt.setZone('Asia/Vladivostok'); //=> 2018-12-19T03:00:00.000+10:00

Luxon также поддерживает работу с датой и временем в формате UTC:

DateTime.utc(2018, 5, 15); //=> 2018-05-15T00:00:00.000Z
DateTime.utc(); //=> 2018-12-18T17:58:29.995Z
DateTime.local().toUTC(); //=> 2018-12-18T17:58:29.995Z
DateTime.utc().toLocal(); //=> 2018-12-18T20:58:29.995+03:00

Длительность


Класс Duration предоставляет возможность работать с длительностью, например, «2 часа и 7 минут». Создать длительность можно так:

var dur = Duration.fromObject({hours: 2, minutes: 7});

Длительности могут складываться и вычитаться. Длительность может иметь отрицательное значение.

dur.minus(dur).minus(dur); //=> {hours: -2, minutes: -7}

Подобным образом длительность может быть добавлена или вычтена из DateTime.

DateTime.local().plus(dur);

У длительности есть геттеры (похожие на геттеры DateTime):

dur.hours;   //=> 2
dur.minutes; //=> 7
dur.seconds; //=> 0
dur.zone; //=> undefined

Так же у длительности есть и другие полезные методы:

dur.as('seconds'); //=> 7620
dur.toObject();    //=> { hours: 2, minutes: 7 }
dur.toISO();       //=> 'PT2H7M'

Интервалы


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

var today = DateTime.local(2018, 12, 18);
var later = DateTime.local(2020, 10, 12);
var interval = Interval.fromDateTimes(today, later);
interval.toString(); 
//=> "[2018-12-18T00:00:00.000+03:00 – 2020-10-12T00:00:00.000+03:00)"
interval.toISO(); 
//=> "2018-12-18T00:00:00.000+03:00/2020-10-12T00:00:00.000+03:00"

interval.length();                             //=> 57369600000
interval.length('years', true);                //=> 1.8169398907103824
interval.contains(DateTime.local(2019));       //=> true

Интервалы можно сравнивать между собой и комбинировать друг с другом:

var nextYear = Interval.after(DateTime.local(), {year: 1});
var prevYear = Interval.before(DateTime.local(), {year: 1});
prevYear.overlaps(nextYear); //false
prevYear.abutsStart(nextYear); //true
nextYear.union(prevYear).length('years'); //=> 2

Luxon и Moment


Библиотека Luxon «обитает» в проекте «Moment», но не является полной заменой библиотеки Moment. Luxon не предоставляет полной функциональности Moment, к примеру, относительное форматирование дат только совсем недавно было реализовано в браузере Chrome версии 71, в других браузерах пока не работает и в Luxon поддержка для него еще не реализована (хотя и ожидается). Но даже если браузеры поддерживают требуемый функционал, то надо понимать, что он будет доступен только в этих новых средах. В устаревших браузерах Luxon будет работать с проблемами, в то время как Moment работает всегда и везде.

Кроме этого API Luxon был полностью переработан и он совершенно не совпадает с API Moment.

Отметим главные различия между Moment и Luxon.

Иммутабельность


Объекты Luxon иммутабельны, а у Moment — нет.
В приведенном ниже примере m1 и m2 — это один и тот же объект, который был изменен методом add.

var m1 = moment();
var m2 = m1.add(1, 'hours');
m1 === m2; //=> true

В случае Luxon метод plus возвращает новый объект d2 не изменяя исходный d1.

var d1 = DateTime.local();
var d2 = d1.plus({ hours: 1 });
d1 === d2; //=> false
d1.valueOf() === d2.valueOf(); //=> false

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

Основные функциональные различия


  1. Отсчет месяцев в Luxon начинается с 1, а не с нуля как в Moment (и нативноv js-объекте Date).
  2. Локализация и временные зоны реализованы с помощью нативного Intl API (или полифила), а не встроены в библиотеку.
  3. Luxon имеет встроенные типы Duration и Interval.
  4. Luxon пока не поддерживает относительное форматирование дат.
  5. В Luxon так же пока нет метода humanize для представления длительности в «очеловеченном» стиле (к примеру, «a few seconds»).

Различия в стиле API


  • В методах API Luxon опционные параметры обычно располагаются последними.
  • Luxon имеет множество отдельных методов для создания объектов(например, fromISO), в отличие от Moment, который имеет для этого одну функцию, а тип объекта задается параметрами.
  • У Luxon очень строгие парсеры, в то время как у Moment они более либеральные, т.е. если формат входной строки будет отличаться от стандартного, то Luxon сразу выдаст ошибку, а Moment какие-то ошибки в формате попробует исправить.
  • Для получения значения внутренних полей Luxon использует геттеры (dt.year, dt.isValid), а не методы как Moment (m.year(), m.isValid()).
  • Luxon позволяет одним методом сразу установить все необходимые параметры dt.set({year: 2016, month: 4}), в Moment они задаются только по одному — цепочкой вызовов m.year(2016).month(4).
  • Длительность в Luxon — это отдельный класс верхнего уровня Duration.

В остальном Luxon заимствовал много идей из Moment, в документации даже приводятся таблицы эквивалентности методов Moment и Luxon.

Размеры файлов библиотек


Luxon (v. 1.8.2)
luxon.min.js — 61 KB

Moment (v. 2.23.0)
moment.min.js — 51 KB
moment.min.js + locale/ru.js — 59 KB
moment-with-locales.min.js — 323 KB

Как видим, без локалей Moment по размеру на 10 KB меньше чем Luxon, но с добавлением нескольких локалей размер становится примерно равным.

Если же требуется поддержка сразу всех локалей, то тут существенный выигрыш у Luxon.

Резюме


Библиотека полностью готова к использованию и автор обещает ее поддержку. У библиотеки уже 7k+ звезд на гитхабе и популярность ее только растет. В ее код делает коммиты не только сам автор, но еще не менее 6 разработчиков.

Предположу, что библиотека Luxon это ответ на появление в браузерах поддержки Intl API. Разработчики Moment понимают, что работа с датами на вебе может существенно измениться и пытаются подготовиться к этим изменениям. Но точно предсказать развитие веба, а вместе с ним и нового проекта (который сами называют Moment labs project) они не могут. Будут ли идеи Luxon перенесены в Moment 3? Перейдет ли в какой то момент большинство пользователей с Moment на Luxon? Может быть Luxon будет переименован в Moment? Сами разработчики признаются, что не могут сейчас ответить на эти вопросы.

Комментарии (13)


  1. justboris
    21.12.2018 02:35

    Ну и как тут не вспомнить что есть еще и вторая по популярности библиотека – date-fns.org

    Отличается от moment и luxon тем, что в ней используются не объекты-обертки, а чистые функции, работающие с нативным Date.


    1. TheShock
      21.12.2018 03:54

      О! Название функции «format». Ещё одно название в мой список идиотских очень понятных, правильных и емких названий, которыми так популярны модные и молодежные библиотеки.


      1. justboris
        21.12.2018 10:56

        Не вижу ничего непонятного в этом имени. И в Moment, и в нативном Intl.DateTimeFormat тоже есть метод format.


        Более того, с date-fns вы получаете просто набор функций, которые можно переименовать при импорте по своему желанию.


        import {format as formatDateByPattern} from 'date-fns';
        
        formatDateByPattern(new Date(2014, 1, 11), 'MM/DD/YYYY');


    1. Leopotam
      21.12.2018 12:15

      А главное — не настолько жирная, как momentjs.


  1. TheShock
    21.12.2018 03:56

    Авторы молодцы. Не стали дожидаться пока люди сделают библиотеку-конкурента и взяли быка за рога.


    1. vintage
      21.12.2018 10:11
      -1

      Да 4 года назад уже сделали.


      • Имеет всё те же преимущества, что и Luxon. Даже больше: offset является объектом duration, а не просто числом; интервалы могут задаваться не только двумя датами, но и одной из дат + продолжительность, и всё это правильно сериализуется в ISO, а не в какой-то свой формат с открытыми интервалами; везде, где ожидается момент/интервал/продолжительность может передаваться как объект, так и json с компонентами, так и iso8601 представление.
      • Разработана за два месяца одним разработчиком, а не два года шестью.
      • Весит 20кб в неминифицированном виде, а не 60 в минифицированном.
      • По скорости на порядок быстрее Moment, сомневаюсь, что Luxon сильно быстрее.

      Но кому это всё надо? Никому, там же всего полторы сотни звёздочек.


      1. triton Автор
        21.12.2018 10:53

        API Luxon намного больше той части, что приведена в статье.

        • Для создания интервалов есть несколько разных методов в том числе Interval.after и Interval.before, которые принимают два параметра: дату и продолжительность
        • Сериализовать интервал в ISO можно выполнив один метод interval.toISO()
        • У Luxon есть методы, которые так же могут принимать разные типы данных, вот к примеру описание метода Interval.after:
          Interval.after(start: DateTime | Date | Object, duration: Duration | Object | number): Interval

        В документации много еще всего есть, если, конечно, интересно.


        1. vintage
          21.12.2018 11:33
          -1

          API Luxon намного больше

          А API $mol_time намного меньше, при той же выразительности.


          Для создания интервалов есть несколько разных методов

          Которые сразу же забывают про переданную продолжительность.


          интервал в ISO можно

          Только вот это уже будет не тот интервал, что мы создавали. В ISO их 3 вида. И это именно разные сущности с разной семантикой, а не просто 3 способа задать одну сущность. Разница ощущается, когда начинается арифметика, где внезапно оказывается, что месяц — это не всегда 30 дней и тп.


          start: DateTime | Date | Object

          Как обычно статическая типизация сделана для галочки. Вот так это должно выглядеть:


          start : number | Date | string | {
              year? : number
              month? : number
              day? : number
              hour? : number
              minute? : number
              second? : number
              offset? : $mol_time_duration_config
          }


  1. mayorovp
    21.12.2018 11:20

    Интересно было бы увидеть сравнение luxon и js-joda.


  1. thekip
    21.12.2018 12:21

    Размер luxon стоит сравнивать не только с moment+locales, но с moment+locales+moment-timezone
    И вот там выигрыши получается настолько большой, что выбор очевиден. (В основном потому что в moment-timezone необходимо грузить БД с таймзонами, а в luxon используется intl API)


  1. irsick
    21.12.2018 17:12

    В moment.js до сих пор не запилили tree shaking? Как с этим дела у Luxon и date-fns?


    1. triton Автор
      21.12.2018 17:28

      Как я понимаю, какие то доработки по tree shaking стоят в планах для версии Luxon 2.0.


    1. justboris
      21.12.2018 17:35
      +2

      В Luxon получше чем у moment, потому что теперь есть 3 отдельные импорты DateTime, Duration, Interval, вместо одного супер-объекта moment.


      У date-fns c этим дела обстоят лучше всех, потому что там экспортируются отдельные функции, как в lodash