JavaRush /Курсы /Swift SELF /Date как точка во времени: сравнение и сортировка

Date как точка во времени: сравнение и сортировка

Swift SELF
32 уровень , 0 лекция
Открыта

1. Дата — не строка

Когда мы начинаем писать программы, почти всегда хочется думать о дате как о тексте: "2026-01-16" выглядит понятно, можно вывести на экран — и вроде бы всё. Но у текста нет «встроенного смысла времени»: строку можно случайно написать в другом формате, сравнить «по алфавиту» и получить уверенно неправильный результат.

В Swift (точнее, в Foundation) есть тип Date, который представляет момент времени на временной шкале. Это не «календарный день» и не «строка даты», а именно точка «вот здесь» на линии времени. Такой объект можно сравнивать, сортировать и считать разницу между моментами — и это то, что нам нужно, если мы хотим писать надёжную логику.

Небольшой ориентир, чтобы не путаться:

Что это Пример Для чего годится Главный риск
String
"2026-01-16"
вывод пользователю, ввод пользователя сравнение строк и разные форматы
Date
Date()
сравнение «раньше/позже», сортировка, интервалы без форматтера выглядит «некрасиво»
TimeInterval
2.5
длительность в секундах перепутать секунды и миллисекунды

В этой лекции мы сознательно держим фокус на Date как на значении для логики. Красивый вывод и парсинг из строк — это следующие лекции дня.

2. import Foundation и создание Date()

Прежде чем мы начнём писать код, важно понимать, почему вообще требуется Foundation. Swift как язык и стандартная библиотека дают нам базовые типы (Int, String, Array и т.д.). А вот типы для «жизни реального мира» — даты, URL, файлы и многое другое — живут в Foundation. Поэтому почти любой пример с датами начинается одинаково: import Foundation.

Самый простой способ получить дату — это вызвать Date() без аргументов. Это создаст объект «текущий момент».

import Foundation

let now = Date()
print(now) // например: 2026-01-16 18:42:10 +0000

Обратите внимание на две вещи. Во‑первых, вывод выглядит «технически» и может показаться странным — это нормально. Во‑вторых, Date() создаёт момент, а не «дату на календаре». Мы ещё не говорим «январь/февраль/день недели» — мы говорим «сейчас».

Если вам хочется представить это визуально, можно вообразить такую схему:

flowchart LR
    A["... прошлое ..."] --> B["Date: now"] --> C["... будущее ..."]

Date — это точка B на этой линии.

Кстати, полезный факт из мира Foundation: Date в Swift — это value type (структура), а внутри у неё хранится число (секунды/интервал), то есть по размеру она «маленькая» и удобная для передачи как значение.

3. Сравнение Date: кто раньше, кто позже

Когда мы говорим «момент времени», логичный следующий шаг — научиться сравнивать моменты. И вот тут Date ведёт себя очень приятно: его можно сравнивать обычными операторами <, >, ==.

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

import Foundation

let now = Date()
let inFiveSeconds = now.addingTimeInterval(5)

print(now < inFiveSeconds) // true
print(now == inFiveSeconds) // false

Здесь сразу прячется важная идея: addingTimeInterval(_:) добавляет секунды, а не «календарные дни/месяцы». То есть это чистая математика на временной шкале. Сегодня этого достаточно и даже идеально: мы обсуждаем «точку времени», а не «прибавить 1 день по календарю» (это будет через Calendar в следующей лекции).

Ещё один пример, чтобы закрепить: «что раньше — старт или финиш?»

import Foundation

let start = Date()
let finish = start.addingTimeInterval(2)

print(start < finish) // true
print(finish > start) // true

Если вы когда-нибудь сравнивали строки вида "2026-1-9" и "2026-01-10" и получали странные результаты — вы уже видели, почему Date полезнее строки.

4. Интервалы: TimeInterval и разница между моментами

Почти всегда рядом с датами возникает вопрос «сколько прошло времени?». Для этого в API используется тип TimeInterval. На практике это Double, выраженный в секундах: 1.0 — одна секунда, 2.5 — две с половиной секунды. В Foundation этот подход закреплён очень давно, поэтому вы будете встречать TimeInterval буквально везде.

Разницу между двумя датами удобно считать методом timeIntervalSince(_:).

import Foundation

let start = Date()
let end = start.addingTimeInterval(2.5)

let seconds = end.timeIntervalSince(start)
print(seconds) // 2.5

Это один из самых «честных» примеров: мы не парсим строки, не думаем о календарях — мы измеряем расстояние между двумя точками на линии времени.

Очень важно: если вы поменяете порядок, знак изменится.

import Foundation

let start = Date()
let end = start.addingTimeInterval(10)

print(start.timeIntervalSince(end)) // -10.0

Отрицательное значение — не ошибка, а смысл: «start был раньше end на 10 секунд».

5. Timestamp: timeIntervalSince1970 и единицы измерения

Иногда дату надо хранить в «чистом виде» — например, записать в файл, передать по сети или просто вывести в лог так, чтобы можно было сравнивать без форматтеров. Для этого часто используют timestamp: число секунд, прошедших с 1 января 1970 года (UTC). В Swift это доступно как timeIntervalSince1970.

import Foundation

let now = Date()
let ts = now.timeIntervalSince1970

print(ts) // например: 1768588930.12345

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

В мире API часто встречается timestamp в миллисекундах (особенно в JavaScript). Там «сейчас» выглядит как число порядка 1700000000000, то есть в тысячу раз больше. В Swift timeIntervalSince1970 — в секундах. Если вы получили миллисекунды и передали их как секунды — вы улетите в далёкое будущее (иногда настолько далёкое, что даже ваш баг-репорт постареет).

Давайте сделаем маленькую защиту: функция, которая «угадывает», секунды это или миллисекунды. Это не идеальная стратегия для продакшена, но как учебный приём — отлично показывает проблему.

import Foundation

func dateFromPossiblyMilliseconds(_ value: Double) -> Date {
    // Если число слишком большое, считаем, что это миллисекунды
    if value > 10_000_000_000 {
        return Date(timeIntervalSince1970: value / 1000.0)
    }
    return Date(timeIntervalSince1970: value)
}

Заметьте, мы используем Date(timeIntervalSince1970:) — это конструктор, который создаёт момент по timestamp.

6. Date и сортировка: даты и события

Сравнивать даты мы уже умеем. Сортировка — это просто «массовое сравнение», когда мы хотим навести порядок в массиве: от старых к новым или наоборот.

Поскольку Date поддерживает сравнение, массив дат можно сортировать очень просто:

import Foundation

let a = Date(timeIntervalSince1970: 100)
let b = Date(timeIntervalSince1970: 50)
let c = Date(timeIntervalSince1970: 200)

let dates = [a, b, c]
let sortedDates = dates.sorted()

print(sortedDates[0].timeIntervalSince1970) // 50.0

Здесь sorted() возвращает новый массив, не меняя старый. Это удобно, когда вы не хотите «ломать» исходные данные.

Если нужно отсортировать «на месте», используем sort():

import Foundation

var dates = [
    Date(timeIntervalSince1970: 3),
    Date(timeIntervalSince1970: 1),
    Date(timeIntervalSince1970: 2)
]

dates.sort()
print(dates.map { $0.timeIntervalSince1970 }) // [1.0, 2.0, 3.0]

А если хочется обратный порядок (самые новые сверху), можно указать правило сортировки через замыкание — вы уже умеете это с sorted(by:):

import Foundation

let dates = [
    Date(timeIntervalSince1970: 10),
    Date(timeIntervalSince1970: 30),
    Date(timeIntervalSince1970: 20)
]

let newestFirst = dates.sorted(by: >)
print(newestFirst.map { $0.timeIntervalSince1970 }) // [30.0, 20.0, 10.0]

Тут важно почувствовать: сортировка — это не «магия дат». Это обычная сортировка, просто элементами массива являются Date.

Сортировка событий по времени

Иногда студенты спотыкаются о мысль: «Я умею сортировать даты, но как сортировать события?» Ответ: сортируется не «событие», а массив, и мы сами задаём правило, по какому полю сравниваем.

Базовый паттерн выглядит так: берём tuple/модель и сравниваем его часть. Например, если событие хранит at: Date, то сортировка «по времени» — это сравнение at.

Чтобы сделать «новые сверху», достаточно поменять знак сравнения:

import Foundation

func printEventsNewestFirst(_ events: [(at: Date, text: String)]) {
    let sorted = events.sorted { $0.at > $1.at }

    for e in sorted {
        print("\(e.at.timeIntervalSince1970): \(e.text)")
    }
}

Если бы мы сейчас писали реальный логгер, «новые сверху» часто удобнее. Но в учебных примерах полезно держать обе версии в голове и видеть, что разница ровно в одном символе.

7. Мини‑приложение: журнал событий с временем

Чтобы не оставаться на уровне «просто тип», давайте встроим Date в маленькое консольное приложение. Сегодня наша цель простая: научиться записывать события с временем и показывать их в правильном порядке.

Мы пока не используем struct (это будет позже по курсу), поэтому возьмём знакомый и лёгкий инструмент: массив tuple’ов. Каждый элемент — это (at: Date, text: String).

Заготовка хранилища и добавление события

Сначала заведём массив и сделаем функцию, которая добавляет событие «сейчас».

import Foundation

var events: [(at: Date, text: String)] = []

func addEvent(_ text: String) {
    events.append((at: Date(), text: text))
}

Здесь всё намеренно минималистично: одно действие — одно событие — фиксируем момент времени.

Показ списка событий в хронологическом порядке

Теперь сделаем функцию, которая печатает события отсортированными по времени. Поскольку Date сравним, сортировка делается без боли.

import Foundation

func printEventsOldestFirst() {
    let sorted = events.sorted { $0.at < $1.at }

    for e in sorted {
        print("\(e.at.timeIntervalSince1970): \(e.text)")
    }
}

Мы выводим timestamp, потому что форматирование «красивой даты» мы ещё не проходили. Да, это не «для человека», зато честно и стабильно для логики.

8. Простой CLI-цикл: "add", "list", "exit"

Сейчас хочется, чтобы приложение было «живым»: пользователь пишет команды, а программа реагирует. Мы не делаем сложный парсер команд (это будет отдельный большой день про CLI parsing), поэтому договоримся о простом формате:

  • add <текст события> — добавить событие с текущим временем,
  • list — вывести события,
  • exit — выйти.

Сделаем цикл чтения команд. Обратите внимание: здесь мы используем readLine() и аккуратно обрабатываем Optional.

import Foundation

while true {
    print("Команда (add/list/exit):", terminator: " ")

    guard let line = readLine() else { break }
    let parts = line.split(separator: " ", maxSplits: 1)

    let cmd = parts.first?.lowercased() ?? ""

    if cmd == "add" {
        let text = parts.count > 1 ? String(parts[1]) : ""
        addEvent(text.isEmpty ? "Пустое событие" : text)
    } else if cmd == "list" {
        printEventsOldestFirst()
    } else if cmd == "exit" {
        break
    } else {
        print("Неизвестная команда")
    }
}

Тут много маленьких «привычек хорошего тона». Мы не форсим readLine()!, не лезем в parts[1] без проверки, не падаем из‑за пустой строки. Программа может быть простой, но пусть она будет простой и живучей.

Нюанс мышления: Date — момент, а «вид даты» — отдельно

Очень легко попасть в ловушку и начать ожидать от Date ответов в стиле «какой сегодня день недели?» или «какой сейчас месяц?». Но у Date другая роль: хранить и сравнивать момент. Человеческие компоненты (год/месяц/день), часовые пояса и форматирование — это отдельные инструменты, и они специально вынесены в другие типы.

Если держать эту границу, код получается спокойнее. Date — это то, что вы храните, сравниваете и сортируете. А всё «как это показать человеку» вы решаете позже и централизованно. В Foundation это разделение считается принципиальным: момент времени отдельно, календарная интерпретация отдельно.

Сегодня мы не форматируем «красиво» — и это не потому, что мы не умеем, а потому что мы учимся правильно разделять ответственность в коде.

9. Типичные ошибки при работе с Date

Ошибка №1: путать Date и String и сравнивать строки вместо дат.
Когда вы храните даты строками, у вас очень быстро появляется два формата («в этом месте YYYY-MM-DD, а тут DD.MM.YYYY»), и сравнение превращается в лотерею. Если логика зависит от времени, храните момент как Date, а строку делайте только для ввода/вывода.

Ошибка №2: считать, что addingTimeInterval(86400) — это «+1 день».
Это «+86400 секунд», и в обычной жизни часто похоже на сутки, но это не то же самое, что календарный день. Сегодня мы просто фиксируем правило: addingTimeInterval — про секунды и интервалы, а календарные прибавления решаются другими инструментами.

Ошибка №3: перепутать секунды и миллисекунды в timestamp.
timeIntervalSince1970 возвращает секунды. Многие внешние системы дают миллисекунды. Если вы не проверили единицы, дата будет выглядеть «валидной», но окажется не там, где ожидали, и этот баг особенно коварен тем, что компилятор его не поймает.

Ошибка №4: форсить readLine()! и падать на пустом вводе.
Работа с датами часто идёт рядом с вводом/выводом: пользователь вводит timestamp, команда — что угодно. Если вы в таких местах пишете !, вы получаете программу, которая падает не из‑за «сложных дат», а из‑за пустой строки. Используйте guard let и дефолты через ??.

Ошибка №5: сортировать события «как получится» и не фиксировать правило сортировки.
Когда у вас массив tuple’ов, сортировка без явного правила может быть неочевидна (и в других типах тоже). Хорошая привычка: в sorted { ... } явно показывать, что вы сравниваете at, а не text. Тогда код читает даже человек, который пришёл через месяц (включая вас самих).

1
Задача
Swift SELF, 32 уровень, 0 лекция
Недоступна
Дуэль таймштампов
Дуэль таймштампов
1
Задача
Swift SELF, 32 уровень, 0 лекция
Недоступна
Финишный расчёт
Финишный расчёт
1
Задача
Swift SELF, 32 уровень, 0 лекция
Недоступна
Очередь дедлайнов
Очередь дедлайнов
1
Задача
Swift SELF, 32 уровень, 0 лекция
Недоступна
Лента событий
Лента событий
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ