Удаление дубликатов

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

new Date('2024-01-01') !== new Date('2024-01-01') // true

В контексте обработки временных данных это приводит к необходимости нормализации и применения специальных алгоритмов сравнения.


Особенности сравнения дат

Объекты Date нельзя корректно сравнивать через === или ==, так как сравнение идёт по ссылке. Для работы с временными значениями используется сравнение по числовому представлению времени:

date.getTime()

Однако в прикладных сценариях часто требуется не абсолютное сравнение, а логическое: по дню, месяцу, неделе или произвольному интервалу. Именно здесь используется функциональность date-fns.


Базовая нормализация перед удалением дубликатов

Перед устранением повторов массив дат приводится к единому виду. Наиболее распространённая стратегия — приведение к timestamp:

const timestamps = dates.map(d => d.getTime())

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


Удаление дубликатов через Set и getTime

Самый прямолинейный способ удаления дубликатов основан на Set:

const uniqueDates = Array.from(
  new Set(dates.map(date => date.getTime()))
).map(time => new Date(time))

Преимущество метода — высокая производительность. Недостаток — отсутствие гибкости при сравнении по частям даты (например, только день без времени).


Использование date-fns для точного сравнения

Библиотека date-fns предоставляет функции для семантического сравнения дат. Основной инструмент — isEqual:

import { isEqual } from 'date-fns'

const unique = []

for (const date of dates) {
  if (!unique.some(d => isEqual(d, date))) {
    unique.push(date)
  }
}

isEqual учитывает точное совпадение времени до миллисекунд, что делает его подходящим для строгой дедупликации.


Удаление дубликатов по календарному дню

Часто дубликаты определяются не по точному времени, а по дню. В этом случае используется isSameDay:

import { isSameDay } from 'date-fns'

const uniqueByDay = []

for (const date of dates) {
  if (!uniqueByDay.some(d => isSameDay(d, date))) {
    uniqueByDay.push(date)
  }
}

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


Удаление дубликатов по неделе и месяцу

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

import { isSameWeek, isSameMonth } from 'date-fns'

Дедупликация по неделе

const uniqueByWeek = []

for (const date of dates) {
  if (!uniqueByWeek.some(d => isSameWeek(d, date))) {
    uniqueByWeek.push(date)
  }
}

Дедупликация по месяцу

const uniqueByMonth = []

for (const date of dates) {
  if (!uniqueByMonth.some(d => isSameMonth(d, date))) {
    uniqueByMonth.push(date)
  }
}

Использование нормализации через startOfDay

Альтернативный подход — предварительное приведение дат к началу дня:

import { startOfDay } from 'date-fns'

const unique = Array.from(
  new Map(
    dates.map(date => [startOfDay(date).getTime(), date])
  ).values()
)

Здесь ключом становится нормализованное значение времени, что позволяет эффективно устранять дубликаты без вложенных циклов.


Сортировка перед удалением дубликатов

При работе с большими массивами сортировка упрощает алгоритм дедупликации:

import { compareAsc } from 'date-fns'

const sorted = [...dates].sort(compareAsc)

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

const result = []

for (let i = 0; i < sorted.length; i++) {
  if (i === 0 || sorted[i].getTime() !== sorted[i - 1].getTime()) {
    result.push(sorted[i])
  }
}

Сложность такого подхода — O(n log n) из-за сортировки, но последующая обработка линейна.


Функциональный подход с reduce

Дедупликация может быть выражена через reduce, что повышает читаемость в функциональных цепочках:

import { isEqual } from 'date-fns'

const unique = dates.reduce((acc, date) => {
  if (!acc.some(d => isEqual(d, date))) {
    acc.push(date)
  }
  return acc
}, [])

Этот подход сохраняет гибкость, позволяя легко менять критерий сравнения.


Дедупликация через Map как индекс времени

Map используется как структура индексации уникальных значений:

import { startOfDay } from 'date-fns'

const map = new Map()

for (const date of dates) {
  const key = startOfDay(date).getTime()
  map.set(key, date)
}

const unique = Array.from(map.values())

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


Обработка временных зон и скрытых дубликатов

Дубликаты могут возникать из-за различий в временных зонах:

new Date('2024-01-01T00:00:00Z')
new Date('2023-12-31T23:00:00-01:00')

Обе даты представляют один момент времени, но визуально различаются. В таких случаях используется строгое сравнение через getTime() или isEqual, исключающее влияние локального представления времени.


Комбинированные стратегии дедупликации

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

  • сортировка (compareAsc)
  • нормализация (startOfDay, startOfMonth)
  • сравнение (isEqual, isSameDay)
  • индексирование (Map, Set)

Пример гибридного подхода:

import { compareAsc, startOfDay, isEqual } from 'date-fns'

const sorted = [...dates].sort(compareAsc)

const unique = []

for (const date of sorted) {
  const normalized = startOfDay(date)

  if (!unique.some(d => isEqual(startOfDay(d), normalized))) {
    unique.push(date)
  }
}

Выбор критерия уникальности

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

  • миллисекунды — событийные системы, логи
  • день — календарные приложения
  • неделя — отчётные периоды
  • месяц — аналитика и агрегирование

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