Сортировка дат

Проблемы нативной сортировки дат в JavaScript

Метод Array.prototype.sort() по умолчанию работает как строковая сортировка, что приводит к некорректным результатам при обработке дат, если отсутствует явный компаратор. Даже при работе с объектами Date поведение может быть неоднозначным, поскольку происходит неявное приведение типов.

Типичный пример проблемного поведения:

const dates = [
  new Date('2024-12-01'),
  new Date('2023-01-10'),
  new Date('2025-05-20')
];

dates.sort();

Результат зависит от преобразования объектов в строки, а не от их числового значения времени.

Корректная сортировка требует явного компаратора, основанного на временных значениях.


Числовая природа дат

В JavaScript объект Date представляет момент времени как количество миллисекунд, прошедших с 1 января 1970 года (UTC). Это значение доступно через:

date.getTime()

Именно числовое представление используется для корректного сравнения дат.


Базовая сортировка через getTime

Наиболее прямолинейный способ сортировки:

const sorted = dates.sort((a, b) => a.getTime() - b.getTime());

Для обратного порядка:

const sortedDesc = dates.sort((a, b) => b.getTime() - a.getTime());

Этот подход является базовым, но при работе с большим количеством дат или разными форматами становится неудобным.


Сортировка с Date-fns

Библиотека Date-fns предоставляет специализированные функции для сравнения дат, упрощающие создание компараторов.

compareAsc

Функция compareAsc выполняет сравнение двух дат в порядке возрастания:

import { compareAsc } from 'date-fns';

const result = compareAsc(
  new Date('2024-01-01'),
  new Date('2023-01-01')
);

Возвращаемые значения:

  • -1 — первая дата меньше второй
  • 1 — первая дата больше второй
  • 0 — даты равны

Использование в сортировке:

import { compareAsc } from 'date-fns';

const sorted = dates.sort(compareAsc);

compareDesc

Функция compareDesc реализует обратный порядок:

import { compareDesc } from 'date-fns';

const sorted = dates.sort(compareDesc);

Эквивалент ручного компаратора:

(b, a) => a.getTime() - b.getTime()

Сортировка ISO-строк дат

В реальных данных даты часто представлены строками ISO 8601:

const dates = [
  '2024-01-10',
  '2023-05-20',
  '2025-02-15'
];

Date-fns поддерживает корректную обработку через parseISO:

import { compareAsc, parseISO } from 'date-fns';

const sorted = dates.sort((a, b) =>
  compareAsc(parseISO(a), parseISO(b))
);

Смешанные форматы данных

В массивах могут встречаться строки, Date и числовые timestamp-значения. Для унификации используется нормализация:

import { compareAsc, parseISO, isDate } from 'date-fns';

function normalizeDate(value) {
  if (isDate(value)) return value;
  if (typeof value === 'string') return parseISO(value);
  return new Date(value);
}

const sorted = mixedDates.sort((a, b) =>
  compareAsc(normalizeDate(a), normalizeDate(b))
);

Обработка некорректных значений

Некорректные даты приводят к Invalid Date, что ломает сравнение. Проверка осуществляется через isValid:

import { compareAsc, isValid } from 'date-fns';

function safeCompare(a, b) {
  const aValid = isValid(a);
  const bValid = isValid(b);

  if (!aValid && !bValid) return 0;
  if (!aValid) return 1;
  if (!bValid) return -1;

  return compareAsc(a, b);
}

Такой подход позволяет перемещать некорректные значения в конец массива.


Сортировка по timestamp без объектов Date

Для оптимизации часто используется хранение числовых значений времени:

const timestamps = [
  1704067200000,
  1672444800000,
  1735689600000
];

timestamps.sort((a, b) => a - b);

При необходимости обратного преобразования:

const dates = timestamps.map(t => new Date(t));

Производительность сортировки

Сортировка имеет сложность O(n log n), но основная нагрузка в контексте дат связана с:

  • созданием объектов Date
  • парсингом строк
  • вызовами getTime() или parseISO

Оптимизация достигается предварительным преобразованием:

const prepared = dates
  .map(d => ({
    original: d,
    time: new Date(d).getTime()
  }))
  .sort((a, b) => a.time - b.time)
  .map(x => x.original);

Стабильность сортировки

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

Добавление вторичного ключа:

const sorted = items
  .map((item, index) => ({ item, index }))
  .sort((a, b) => {
    const diff = compareAsc(a.item.date, b.item.date);
    return diff !== 0 ? diff : a.index - b.index;
  })
  .map(x => x.item);

Сортировка по временным зонам

Date-fns оперирует объектами Date, которые всегда хранят UTC-основанное время. Однако входные строки могут содержать смещения:

const dates = [
  '2024-01-01T10:00:00+03:00',
  '2024-01-01T08:00:00Z'
];

После парсинга:

import { compareAsc, parseISO } from 'date-fns';

dates.sort((a, b) =>
  compareAsc(parseISO(a), parseISO(b))
);

Сравнение происходит уже в единой временной шкале.


Группировка после сортировки

Сортировка часто используется как предварительный этап перед группировкой:

import { compareAsc, parseISO } from 'date-fns';

const sorted = data.sort((a, b) =>
  compareAsc(parseISO(a.date), parseISO(b.date))
);

const grouped = sorted.reduce((acc, item) => {
  const key = item.date;
  if (!acc[key]) acc[key] = [];
  acc[key].push(item);
  return acc;
}, {});

Использование timestamp как универсального ключа

В высоконагруженных системах применяется стратегия хранения времени в виде числа:

const events = [
  { id: 1, time: 1700000000000 },
  { id: 2, time: 1600000000000 }
];

events.sort((a, b) => a.time - b.time);

Такой подход исключает необходимость повторного парсинга.


Комбинированные компараторы

При сортировке сложных структур данных применяется многоуровневое сравнение:

import { compareAsc } from 'date-fns';

function compareEvents(a, b) {
  const byDate = compareAsc(a.date, b.date);
  if (byDate !== 0) return byDate;

  return a.priority - b.priority;
}

Типовые ошибки при сортировке дат

  • отсутствие явного компаратора
  • сравнение строк вместо дат
  • игнорирование временных зон
  • повторный парсинг внутри sort, ухудшающий производительность
  • отсутствие обработки Invalid Date

Унифицированный подход к сортировке

Практическая модель сортировки данных с датами в Date-fns сводится к трём этапам:

  1. нормализация входных значений (parseISO, new Date)
  2. использование compareAsc или compareDesc
  3. добавление вторичных критериев при необходимости
import { compareDesc, parseISO } from 'date-fns';

const sorted = data.sort((a, b) =>
  compareDesc(parseISO(a.date), parseISO(b.date))
);