Avatar ·

Как вычислить разницу между двумя датами в месяцах, днях, часах, минутах и секундах в Swift?

Привет! Я новичок в Swift и никак не могу разобраться с датами. Мне нужно посчитать точную разницу между двумя датами и вывести её в читаемом виде: сколько прошло месяцев, дней, часов, минут и секунд. Например, между 15 марта 2023 года, 10:30:00 и 20 июля 2024 года, 14:45:30.

Я пробовал использовать Calendar и DateComponents, но получаю странные результаты. Вот мой текущий код:

import Foundation

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
let date1 = dateFormatter.date(from: "2023/03/15 10:30:00")!
let date2 = dateFormatter.date(from: "2024/07/20 14:45:30")!

let diff = date2.timeIntervalSince(date1)
let days = Int(diff) / 86400
let hours = (Int(diff) % 86400) / 3600
let minutes = (Int(diff) % 3600) / 60
let seconds = Int(diff) % 60

print("\(days) дней, \(hours) часов, \(minutes) минут, \(seconds) секунд")

Проблема в том, что этот способ не учитывает месяцы и даёт только общее количество дней. А мне нужно что-то вроде: "1 год, 4 месяца, 5 дней, 4 часа, 15 минут, 30 секунд". Как правильно посчитать разницу с учётом календарных месяцев и лет? Я слышал про DateComponentsFormatter, но не понимаю, как его настроить. Подскажите, пожалуйста, лучший способ для iOS/macOS.

<<>>

Использование Calendar и DateComponents для точного вычисления разницы

В Swift правильный способ получить разницу между датами в месяцах, днях, часах и т.д. — использовать Calendar и метод dateComponents(_:from:to:). Этот подход учитывает реальную календарную разницу, а не просто арифметику секунд. Вот как это сделать:

import Foundation

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"

guard let startDate = dateFormatter.date(from: "2023/03/15 10:30:00"),
      let endDate = dateFormatter.date(from: "2024/07/20 14:45:30") else {
    fatalError("Неверный формат даты")
}

let calendar = Calendar.current
let components: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let difference = calendar.dateComponents(components, from: startDate, to: endDate)

if let years = difference.year,
   let months = difference.month,
   let days = difference.day,
   let hours = difference.hour,
   let minutes = difference.minute,
   let seconds = difference.second {
    print("\(years) год, \(months) месяцев, \(days) дней, \(hours) часов, \(minutes) минут, \(seconds) секунд")
    // Вывод: 1 год, 4 месяцев, 5 дней, 4 часов, 15 минут, 30 секунд
}

Как это работает?

  • Calendar.current — использует текущий календарь пользователя (грегорианский по умолчанию).
  • Set<Calendar.Component> — указывает, какие компоненты разницы нас интересуют.
  • dateComponents(_:from:to:) — вычисляет разницу «календарно»: сначала годы, потом месяцы, дни и т.д., корректно обрабатывая високосные годы и разную длину месяцев.

Альтернатива: DateComponentsFormatter для локализованного вывода

Если вам нужно просто показать разницу пользователю в локализованном виде, используйте DateComponentsFormatter:

let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.year, .month, .day, .hour, .minute, .second]
formatter.unitsStyle = .full
formatter.maximumUnitCount = 6 // Показать все единицы

if let formattedString = formatter.string(from: startDate, to: endDate) {
    print(formattedString) // "1 year, 4 months, 5 days, 4 hours, 15 minutes, 30 seconds"
}

Важные замечания

  • Метод dateComponents(_:from:to:) возвращает опциональные значения — всегда проверяйте их через if let или guard let.
  • Для обратной совместимости с iOS 7+ используйте NSCalendar, но современный код пишется на Calendar.
  • Если нужно получить разницу только в секундах (например, для таймера), используйте Date().timeIntervalSince(referenceDate).
  • Для macOS и iOS код идентичен — Foundation работает везде одинаково.

Этот подход решает проблему «сырого» подсчёта через timeIntervalSince и даёт именно то, что нужно: календарную разницу с учётом реальных месяцев и лет.

<<>>

Расширение для Date: удобный метод разницы

Если вам нужно часто вычислять разницу, создайте расширение для Date. Это сделает код чище и переиспользуемым:

extension Date {
    func difference(to endDate: Date) -> DateComponents {
        let calendar = Calendar.current
        let components: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
        return calendar.dateComponents(components, from: self, to: endDate)
    }
}

// Использование:
let start = dateFormatter.date(from: "2023/03/15 10:30:00")!
let end = dateFormatter.date(from: "2024/07/20 14:45:30")!
let diff = start.difference(to: end)

print("\(diff.year!) лет, \(diff.month!) мес, \(diff.day!) дн, \(diff.hour!) ч, \(diff.minute!) мин, \(diff.second!) сек")

Дополнительный совет: обработка отрицательных разниц

Если startDate больше endDate, компоненты будут отрицательными. Чтобы избежать этого, можно отсортировать даты:

func safeDifference(from date1: Date, to date2: Date) -> DateComponents {
    let (earlier, later) = date1 < date2 ? (date1, date2) : (date2, date1)
    return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: earlier, to: later)
}

Этот подход особенно полезен, если даты приходят из внешних источников и порядок может быть любым. Для простых задач используйте DateComponentsFormatter, для точных расчётов — Calendar.dateComponents.