JavaRush /Курсы /Swift SELF /DateFormatter — локаль и таймзона

DateFormatter — локаль и таймзона

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

1. Время ≠ строка

Если вы когда-нибудь писали print(Date()) и видели там что-то вроде 2026-01-16 20:31:12 +0000, то вы уже сталкивались с главной проблемой: техническое представление даты не обязано быть удобным человеку. Человеку хочется «16 января 2026, 12:30», а программе хочется стабильности: чтобы строка всегда читалась одинаково и не ломалась из‑за языка системы.

DateFormatter — это переводчик между двумя мирами:

  • мир данных: Date (момент времени);
  • мир текста: String (то, что мы показываем или читаем).

Технически это тип из Foundation (а исторически — Swift-обёртка над NSDateFormatter, который «потерял NS‑префикс» в современном Swift). Поэтому почти в любом примере вам понадобится import Foundation.

Схема простая (и полезно держать её в голове, чтобы не пытаться «сравнивать строки дат»):

flowchart LR
    A["Date (момент)"] -->|"DateFormatter.string(from:)"| B["String (человек видит)"]
    C["String (пользователь ввёл)"] -->|"DateFormatter.date(from:)"| D["Date (момент)"]

Форматирование и парсинг

У DateFormatter есть два основных метода, и они зеркальны:

  • string(from: Date) -> Stringформатирование (Date → String)
  • date(from: String) -> Date?парсинг (String → Date)

Обратите внимание на маленькую, но очень важную деталь: при парсинге возвращается Date?, то есть Optional. Это честное признание API: строка может быть кривой, формат может не совпасть, локаль может быть другая — и вместо падения программа просто скажет: «Не смог распознать».

Начнём с самых маленьких примеров — буквально «подержать в руках» API.

import Foundation

let now = Date()

let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .short

let text = f.string(from: now)
print(text) // Примерно: "Jan 16, 2026 at 12:30 PM" (зависит от окружения)

И обратное направление (с проверкой nil):

import Foundation

let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd"

let input = "2026-01-16"
let parsed = f.date(from: input)

print(parsed as Any) // Optional(2026-01-16 00:00:00 +0000) или nil

Пока выглядит легко. А теперь важное уточнение: без настроек форматтер почти всегда бесполезен. Настройки — это половина смысла.

2. Стильный вывод: dateStyle и timeStyle

Когда вы делаете приложение для людей, вы чаще всего хотите, чтобы дата выглядела привычно для пользователя: на русском — «16 янв. 2026 г.», в США — «Jan 16, 2026», в Японии — по‑другому. Именно для этого у DateFormatter есть «режим стилей»: dateStyle и timeStyle.

Это удобно тем, что вы не думаете про токены формата (типа yyyy/MM), а просто выбираете уровень подробности. Это похоже на настройку «крупность помола» у кофе: вы говорите «мелко/средне/крупно», а не описываете каждую крупинку.

Вот небольшой ориентир:

Стиль Пример смысла Когда уместно
.short
коротко списки, таблицы, компактный вывод
.medium
нормально читаемо большинство UI/сообщений
.long
подробно отчёты, письма, «для красоты»
.full
максимально подробно редко, когда прям надо «официально»

Пример:

import Foundation

let f = DateFormatter()
f.dateStyle = .long
f.timeStyle = .short

print(f.string(from: Date())) // зависит от локали системы

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

Но если вы хотите стабильную техническую строку, то стилей мало.

3. Фиксированный формат: dateFormat и дисциплина

Когда вы делаете формат для данных (например, пользователь вводит дату в заданном виде, или вы сохраняете дату как строку в файле), вам нужен фиксированный шаблон. Для этого используется dateFormat.

Идея такая: вы задаёте строку-шаблон, а DateFormatter по ней либо строит строку, либо пытается её распарсить.

Пример «технической» даты без локальных слов:

import Foundation

let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm"

let now = Date()
print(f.string(from: now)) // например: "2026-01-16 12:30"

Мини-таблица самых популярных токенов (только самые базовые — без «тонких ловушек», они будут в следующей лекции дня):

Токен Значение Пример
yyyy
год
2026
MM
месяц (01–12)
01
dd
день месяца (01–31)
16
HH
часы (00–23)
09
mm
минуты (00–59)
30

Важно привыкнуть к мысли: регистр букв — часть смысла. MM и mm — это разные вещи. Да, это похоже на то, что мир дат решил немного потроллить человечество. Но так исторически сложилось.

4. Локаль: Locale и эффект «работало у меня»

Представьте, что вы парсите строку 16 Jan 2026. На английской локали это читается. На русской — может перестать. А если вы парсите 16 янв. 2026, то наоборот.

Locale — это набор правил «как в этом языке/регионе принято писать даты, месяцы, разделители, AM/PM и т.д.». Если вы используете dateStyle/timeStyle, локаль особенно важна, потому что форматтер пытается быть «естественным» для пользователя.

Когда вы парсите технический формат, вы почти всегда хотите предсказуемость. Классический практический приём: ставить фиксированную локаль "en_US_POSIX". Это такая «нейтральная» локаль, которая помогает стабильно парсить технические строки (особенно если в формате есть названия месяцев, AM/PM и подобное).

Пример: парсим английскую строку месяца, но принудительно задаём локаль.

import Foundation

let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.dateFormat = "dd MMM yyyy"

let d = f.date(from: "16 Jan 2026")
print(d as Any) // Optional(...)

А теперь важный вывод на уровне привычки: если у вас формат фиксирован, не полагайтесь на локаль по умолчанию. По умолчанию она берётся из системы, а это значит — «в разных местах мира по‑разному».

5. Таймзона: TimeZone и разное отображение одного Date

Теперь ещё один «слой реальности», который ломает мозг новичкам: Date — это момент. Но когда вы превращаете момент в строку, вам надо решить: в какой таймзоне показать время?

Если вы не задаёте timeZone, форматтер обычно использует таймзону устройства. Это логично для UI, но может быть неожиданно для данных.

Сначала пример: создадим «полдень UTC» и покажем его в двух таймзонах.

import Foundation

let calendar = Calendar(identifier: .gregorian)
var comps = DateComponents()
comps.year = 2026
comps.month = 1
comps.day = 16
comps.hour = 12
comps.minute = 0
comps.timeZone = TimeZone(secondsFromGMT: 0)

let date = calendar.date(from: comps) ?? Date()

let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm ZZZZ"

f.timeZone = TimeZone(secondsFromGMT: 0)
print(f.string(from: date)) // "2026-01-16 12:00 +0000"

f.timeZone = TimeZone(secondsFromGMT: -8 * 3600)
print(f.string(from: date)) // "2026-01-16 04:00 -0800"

Важная мысль: date один и тот же, а строки разные — потому что «локальное время» зависит от таймзоны. Это не баг и не «Swift опять сломался». Это нормальная математика времени.

Практическое правило:

  • если вы делаете строку «для человека на его устройстве», часто можно оставить таймзону по умолчанию;
  • если вы делаете строку «как данные / для протокола / для одинакового поведения», задавайте таймзону явно (часто UTC).

6. Пример: мини‑планировщик чтения

Сейчас соберём маленькую «живую» историю, чтобы DateFormatter не был просто набором методов. Пусть у нас будет консольный планировщик чтения: пользователь вводит книги и дедлайны, а программа печатает список красивым текстом.

Мы не трогаем сложный CLI‑парсинг (он будет в следующих днях), а используем то, что у нас уже есть: readLine(), split, Optional, массивы и struct.

Модель данных: ReadingItem

Начнём с простого типа.

import Foundation

struct ReadingItem {
    let title: String
    let dueDate: Date
}

Здесь dueDate — именно Date, а не String. Строка — это «упаковка», а Date — нормальные данные. Это позволит нам сортировать, сравнивать, считать «сколько осталось» и т.д. (да, даже если пока мы это не делаем).

Форматтеры для ввода и вывода

Создавать DateFormatter() в каждом чихе — дорого и неудобно: легко случайно настроить его по‑разному в разных местах. Поэтому заведём один форматтер для ввода и один — для вывода.

import Foundation

let inputFormatter: DateFormatter = {
    let f = DateFormatter()
    f.locale = Locale(identifier: "en_US_POSIX")
    f.timeZone = TimeZone(secondsFromGMT: 0)
    f.dateFormat = "yyyy-MM-dd"
    return f
}()

И форматтер для вывода «чуть более дружелюбно»:

import Foundation

let outputFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateStyle = .medium
    f.timeStyle = .none
    return f
}()

Обратите внимание: для ввода мы сделали фиксированный формат "yyyy-MM-dd" (и зафиксировали locale/timeZone, чтобы парсинг был предсказуем), а для вывода выбрали dateStyle = .medium, чтобы пользователь видел привычный вид на своём устройстве.

Парсинг даты из строки

Теперь напишем маленькую функцию: на вход String, на выход Date?.

import Foundation

func parseDueDate(_ text: String) -> Date? {
    return inputFormatter.date(from: text)
}

Коротко, но здесь важна сама идея: раз парсинг может не получиться, мы честно возвращаем Optional.

Чтение элемента и валидация

Пусть пользователь вводит строку в формате:

Название книги | 2026-02-01

Просто и предсказуемо.

import Foundation

func readItem() -> ReadingItem? {
    guard let line = readLine() else { return nil }

    let parts = line.split(separator: "|").map { $0.trimmingCharacters(in: .whitespaces) }
    guard parts.count == 2 else { return nil }

    guard let date = parseDueDate(parts[1]) else { return nil }
    return ReadingItem(title: parts[0], dueDate: date)
}

Если что-то не так — возвращаем nil. Это честно: «не смогли прочитать элемент».

Печать списка «по‑человечески»

Теперь выведем элементы так, чтобы читалось приятно.

import Foundation

func printItems(_ items: [ReadingItem]) {
    for item in items {
        let dateText = outputFormatter.string(from: item.dueDate)
        print("- \(item.title) до \(dateText)")
    }
}

На практике это будет что-то вроде: - Clean Code до Jan 16, 2026 (или по‑русски, если системная локаль русская).

И вот здесь становится видно, почему нам выгодно хранить Date, а не строку: вывод можно менять без переписывания данных.

Минимальная диагностика: если парсинг возвращает nil

Когда работа с датами «не работает», ощущения обычно такие: «ну он же должен распарсить, я же написал дату!». А DateFormatter молча возвращает nil. В этот момент полезно действовать как инженер, а не как шаман.

Первое: убедитесь, что формат совпадает с вводом. Если dateFormat = "yyyy-MM-dd", то строка должна быть ровно вида 2026-01-16, без времени.

Второе: убедитесь, что вы не парсите «человеческий формат» техническим форматтером. Например, Jan 16, 2026 не распарсится форматтером "yyyy-MM-dd". Это нормально.

Третье: если в строке есть слова (название месяца), фиксируйте Locale(identifier: "en_US_POSIX") (или другую подходящую), иначе Jan в русской локали может стать проблемой.

Четвёртое: если вы работаете с датой на границе суток (00:00) и внезапно «съехал день», проверьте TimeZone. Даже если мы сегодня не уходим в совсем тонкие кейсы, таймзона — первая вещь, которую стоит подозревать.

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

Ошибка №1: сравнивать строки дат вместо Date.
Строка — это картинка, а не данные. Сегодня у вас формат 16.01.2026, завтра Jan 16, 2026, и сравнение строк начнёт давать странные результаты. Сравнивать и сортировать нужно Date, а строку получать уже в момент вывода.

Ошибка №2: ожидать, что date(from:) всегда распарсит.
Парсинг возвращает Date? не из вредности, а потому что строка может не совпасть с форматом. Если вы делаете inputFormatter.date(from: text)!, то вы подписываете контракт «строка всегда валидна». В учебных примерах так иногда делают, но в реальном коде это почти гарантированный краш.

Ошибка №3: использовать dateStyle/timeStyle для технических строк.
Стильный вывод удобен для пользователя, но он зависит от локали и настроек устройства. Если вы сохраняете такие строки в данные или передаёте между системами, вы создаёте себе проблему «у меня читалось, у него не читается». Для технических строк используйте фиксированный dateFormat и фиксируйте Locale.

Ошибка №4: не учитывать TimeZone и получать «съехавшее» время.
Один и тот же Date в UTC и в таймзоне пользователя будет выглядеть по-разному. Если вы парсите в одной таймзоне, а форматируете в другой (или полагаетесь на дефолт), можно внезапно увидеть другое число или другой час. Как минимум в тех местах, где важна точность, таймзону стоит задавать явно.

Ошибка №5: создавать DateFormatter() внутри цикла или в каждом вызове функции.
Форматтер — объект с настройками, и его создание не бесплатное. Кроме производительности, появляется риск, что вы случайно настроите разные форматтеры по‑разному и получите «плавающее» поведение. Гораздо спокойнее держать один форматтер (или пару: для ввода и вывода) и переиспользовать.

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