1. Дата — не строка
Когда мы начинаем писать программы, почти всегда хочется думать о дате как о тексте: "2026-01-16" выглядит понятно, можно вывести на экран — и вроде бы всё. Но у текста нет «встроенного смысла времени»: строку можно случайно написать в другом формате, сравнить «по алфавиту» и получить уверенно неправильный результат.
В Swift (точнее, в Foundation) есть тип Date, который представляет момент времени на временной шкале. Это не «календарный день» и не «строка даты», а именно точка «вот здесь» на линии времени. Такой объект можно сравнивать, сортировать и считать разницу между моментами — и это то, что нам нужно, если мы хотим писать надёжную логику.
Небольшой ориентир, чтобы не путаться:
| Что это | Пример | Для чего годится | Главный риск |
|---|---|---|---|
|
|
вывод пользователю, ввод пользователя | сравнение строк и разные форматы |
|
|
сравнение «раньше/позже», сортировка, интервалы | без форматтера выглядит «некрасиво» |
|
|
длительность в секундах | перепутать секунды и миллисекунды |
В этой лекции мы сознательно держим фокус на 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. Тогда код читает даже человек, который пришёл через месяц (включая вас самих).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ