Чтобы отформатировать момент публикации в другом часовом поясе, сохраните исходный момент и передайте форматтеру целевой часовой пояс. Это руководство по работе с Терминалом на Mac поможет создать удобную для чтения метку публикации, не разбирая отображаемый текст и не прибавляя часы вручную.

Какие данные нужны перед преобразованием?

Предположим, вы готовите документ на Mac и получили синтетическую временную метку публикации 2025-07-15T00:30:00Z. Вам нужны дата и время публикации, отображаемые в America/Los_Angeles. Задача — описать тот же момент, указав календарную дату и время по часам в целевом часовом поясе.

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

Понятие Синтетический пример Значение для этой задачи
Календарная дата 2025-07-15 День в календаре без времени и часового пояса
Момент времени Момент, обозначенный 2025-07-15T00:30:00Z Точка во времени, которую нужно сохранить
Смещение относительно UTC -07:00 Разница между местным временем и UTC в конкретный момент
Именованный часовой пояс America/Los_Angeles Правила для определения применимого смещения

RFC 3339 определяет временные метки, содержащие полную дату, время и смещение относительно UTC. RFC 9557 поясняет различие: именованный часовой пояс задаёт правила, сопоставляющие моменту времени смещение, тогда как одно лишь числовое смещение во временной метке не описывает, как смещение в конкретном месте меняется в другое время. RFC 3339, раздел 5.6, RFC 9557, раздел 1.2.

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

  • У вас уже есть объект Date из Foundation: передайте его напрямую форматтеру, настроенному для целевого часового пояса.
  • У вас есть временная метка с Z или явным числовым смещением: один раз разберите исходную временную метку, а затем отформатируйте полученный момент времени.
  • У вас есть только календарная дата: сохраняйте её как дату, если в задаче явно не указаны время и часовой пояс.
  • У вас есть местные дата и время без смещения: выясните исходный часовой пояс и разрешите неоднозначность, прежде чем считать их конкретным моментом времени.

Например, 2025-07-15 не позволяет определить, произошло ли событие публикации до или после полуночи в другом часовом поясе. Назначение полуночи внесло бы дополнительное предположение. Аналогично, местному времени на часах может соответствовать ноль или несколько моментов времени при переходе на другое смещение; эту особенность границ описывает RFC 9557. Преобразование уже установленного момента времени позволяет избежать такой проблемы обратного сопоставления. RFC 9557, раздел 1.2.

Почему не следует повторно разбирать отображаемый текст?

Согласно документации Apple, Date из Foundation — это конкретный момент времени, не зависящий от календаря или часового пояса. Чтобы отобразить его в другом часовом поясе, не нужно создавать другой объект Date. Форматирование использует календарь и часовой пояс для создания текста. Date.

В метке могут отсутствовать некоторые сведения. Если отформатировать момент времени как дату без времени, а затем разобрать эту метку, восстановить из неё отсутствующее время не удастся. В документации Apple по форматированию показан пример преобразования туда и обратно только для даты, при котором восстанавливается полночь. Это полезно, если нужна именно полночь, но не позволяет сохранить произвольный момент публикации. Date.FormatStyle.

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

  1. Прочитайте исходную временную метку или получите существующий объект Date.
  2. Сохраните этот момент времени как рабочее значение.
  3. Выберите целевой часовой пояс, календарь и локаль отображения.
  4. Отформатируйте сохранённый момент времени в итоговую метку.

Ручное прибавление смещения меняет суть задачи. В документации Apple указано, что addingTimeInterval(_:) создаёт новую дату, прибавляя прошедшее время. Поэтому прибавление семи часов создаёт другой момент времени, а выбор часового пояса в форматтере меняет только представление исходного момента. Date.

Выполните на Mac полный пример форматирования времени публикации

Для примера нужна macOS 12 или более поздняя версия, в которой доступен API Date.FormatStyle от Apple, и работающий набор инструментов Swift. Пример использует Foundation и не требует дополнительных пакетов. Date.FormatStyle.

Откройте Терминал и проверьте набор инструментов:

swift --version

Если вам нужны инструменты командной строки Apple, выполните следующую команду и завершите установку в появившемся диалоговом окне. Apple описывает этот способ установки и отмечает, что полная версия Xcode уже включает инструменты командной строки. Затем повторите проверку версии. Установка инструментов командной строки.

xcode-select --install

Создайте новый документ в TextEdit и выберите Формат > Сделать обычным текстом. Вставьте в него всю приведённую ниже программу на Swift и сохраните её на Рабочем столе под именем publication-time.swift. Убедитесь, что имя файла заканчивается на .swift, без добавленного расширения .txt. В руководстве Apple по TextEdit описано создание, преобразование и сохранение документов с обычным текстом. Создание, открытие и преобразование документов в TextEdit на Mac.

Программа принимает указанную временную метку с точностью до секунды, разбирает её один раз и создаёт три представления из одного и того же значения instant. В документации Apple описан ISO8601DateFormatter для разбора и создания представлений ISO 8601, а его параметр withInternetDateTime — для формата RFC 3339. ISO8601DateFormatter, withInternetDateTime.

import Foundation

// Synthetic publication data. Change these two values as needed.
let sourceTimestamp = "2025-07-15T00:30:00Z"
let destinationIdentifier = "America/Los_Angeles"

enum ConversionError: Error {
    case invalidTimestamp(String)
    case unknownTimeZone(String)
}

let inputFormatter = ISO8601DateFormatter()
inputFormatter.formatOptions = [.withInternetDateTime]
inputFormatter.timeZone = TimeZone(secondsFromGMT: 0)

guard let instant = inputFormatter.date(from: sourceTimestamp) else {
    throw ConversionError.invalidTimestamp(sourceTimestamp)
}

guard let destination = TimeZone(identifier: destinationIdentifier) else {
    throw ConversionError.unknownTimeZone(destinationIdentifier)
}

let destinationFormatter = ISO8601DateFormatter()
destinationFormatter.formatOptions = [.withInternetDateTime]
destinationFormatter.timeZone = destination

let publicationLabelStyle = Date.FormatStyle(
    date: .long,
    time: .complete,
    locale: Locale(identifier: "en_GB"),
    calendar: Calendar(identifier: .gregorian),
    timeZone: destination
)

let publicationDateStyle = Date.FormatStyle(
    date: .long,
    time: .omitted,
    locale: Locale(identifier: "en_GB"),
    calendar: Calendar(identifier: .gregorian),
    timeZone: destination
)

print("Source instant: \(inputFormatter.string(from: instant))")
print("Destination zone: \(destination.identifier)")
print("Destination timestamp: \(destinationFormatter.string(from: instant))")
print("Publication label: \(instant.formatted(publicationLabelStyle))")
print("Publication date: \(instant.formatted(publicationDateStyle))")

Запустите сохранённую программу:

swift "$HOME/Desktop/publication-time.swift"

Для указанных значений временная метка в целевом часовом поясе должна быть 2025-07-14T17:30:00-07:00, а дата публикации должна соответствовать 14 июля 2025 года. Читаемая метка также содержит время и сведения о часовом поясе; точная формулировка и пунктуация зависят от системы форматирования.

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

Какие настройки определяют результат?

Инициализатор форматтера принимает отдельные настройки локали, календаря и часового пояса. Здесь en_GB задаёт правила форматирования британского английского, григорианский календарь определяет систему дат, а destination — местную календарную дату и время на часах. Измените идентификатор часового пояса, чтобы выбрать другой целевой пояс, и локаль, чтобы изменить правила представления. init(date:time:locale:calendar:timeZone:capitalizationContext:).

Результат, содержащий только дату, по-прежнему напрямую получается из instant. Установка time: .omitted убирает время из итоговой метки. Создавать новый момент времени, соответствующий полуночи, не нужно.

Проверьте переходы через полночь и сезонные смещения

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

Исходный момент времени Целевой часовой пояс Представление в целевом часовом поясе
2025-01-15T00:30:00Z America/Los_Angeles 2025-01-14T16:30:00-08:00
2025-07-15T00:30:00Z America/Los_Angeles 2025-07-14T17:30:00-07:00
2025-07-15T00:30:00Z Asia/Tokyo 2025-07-15T09:30:00+09:00
2025-07-15T23:30:00Z Asia/Tokyo 2025-07-16T08:30:00+09:00

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

Именованные часовые пояса используют правила из базы данных часовых поясов. IANA сообщает, что macOS использует свою базу данных, а обновления обычно поступают на устройства вместе с обновлениями операционной системы. Если результаты различаются на разных компьютерах, перед изменением исходной временной метки проверьте идентификаторы часовых поясов и наличие доступных обновлений. Данные о часовых поясах и переходах на летнее время.

Что делать, если входные данные или результат неожиданны?

Выполните следующие проверки по порядку:

  1. Убедитесь, что источник задаёт конкретный момент времени. Сохраните его Z или числовое смещение. Для значения только с датой или местного времени на часах нужны дополнительные сведения.
  2. Проверьте идентификатор целевого часового пояса. Используйте идентификатор, например America/Los_Angeles. Инициализатор Apple возвращает nil для неизвестного идентификатора; эта программа останавливается с ошибкой и не подставляет другой часовой пояс.
  3. Проверьте, поддерживается ли формат временной метки. Пример настроен на интернет-временные метки с точностью до секунды. Для дробных секунд или другого представления входных данных может потребоваться соответствующая настройка анализатора. При этом сохраняйте исходные данные.
  4. Сравните числовую временную метку в целевом часовом поясе. Сначала проверьте дату, время и смещение, чтобы оценить результат преобразования, а затем разбирайтесь с пунктуацией отображения.
  5. Убедитесь, что для всех результатов используется исходное значение instant. Удалите все промежуточные шаги, в которых метка форматируется, а затем разбирается для следующего преобразования. init(identifier:), ISO8601DateFormatter.

Согласно RFC 9557, в расширенной временной метке можно передавать вместе именованный часовой пояс и смещение. Само существование этого стандарта не гарантирует, что анализатор в данном примере поддерживает аннотации часового пояса в квадратных скобках. Если вы намеренно не реализовали и не проверили обработку расширенного формата, передавайте идентификатор целевого часового пояса в отдельном значении destinationIdentifier, как показано выше. RFC 9557.

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

Источники