JavaRush /Курси /Swift SELF /Calendar і DateComponents: додавання днів і місяців

Calendar і DateComponents: додавання днів і місяців

Swift SELF
Рівень 32 , Лекція 1
Відкрита

1. Вступ

Коли ви вперше стикаєтеся із завданням «зробити дату на 7 днів пізнішою», мозок — особливо після уроків математики — підказує: «Гаразд, доба — це 24 години, 24*60*60 = 86400 секунд, тож рушаймо». І це справді іноді працює, а іноді перетворює ваш застосунок на генератор загадок рівня «чому дедлайн зсунувся на годину».

Проблема в тому, що «день» у календарному сенсі не завжди дорівнює 24 годинам. У деяких часових поясах бувають переходи на літній або зимовий час, і конкретна доба може містити 23 або 25 годин. Тому «додати 86400 секунд» означає «додати рівно 24 години», а «додати 1 день» — «перейти до наступного календарного дня за правилами календаря й часового поясу».

Порівняймо ці дві операції у вигляді схеми:

flowchart LR
    A["Date (момент часу)"] -->|"+ 86400 секунд"| B["Date (через 24 години)"]
    A -->|"+ 1 день через Calendar"| C["Date (наступний календарний день)"]

Обидві стрілки повертають Date, але сенс у них різний. Саме тут на сцену виходить Calendar: він знає, що таке день, місяць і рік, а також як вони змінюються за правилами календаря.

2. Calendar: правила календарних дат

Якщо Date — це «координата на часовій осі», то Calendar — це спосіб перевести її в зрозумілу людині форму. На Calendar зручно дивитися як на інструктора з виживання у світі «місяців різної довжини», «високосних років» та інших радощів, які люди вигадали для складнішого життя.

Сам Date не зберігає всередині себе «місяць = січень». Він зберігає лише момент часу. А календар уже каже: «За моїми правилами цей момент відповідає такому-то року, місяцю й дню». Саме тому у Foundation прийнято мислити так: Date — це зберігання, а Calendar/TimeZone — інтерпретація.

У цій лекції ми використовуватимемо календар так:

  • витягнемо з Date компоненти (рік, місяць, день),
  • створимо Date з компонентів, коли користувач вводить дату,
  • додамо дні й місяці календарно, а не «секундно».

Для прикладів ми часто братимемо:


let calendar = Calendar(identifier: .gregorian)

Це робить поведінку передбачуванішою: «григоріанський календар» — саме той, яким користується більшість ваших майбутніх користувачів (і викладачів).

3. Компоненти дати: витягаємо, збираємо й перевіряємо

DateComponents: контейнер, де майже все — Optional

DateComponents — це контейнер для частин дати: року, місяця, дня, години, хвилини тощо. З ним зручно працювати, коли ви хочете або зібрати дату з фрагментів, які ввів користувач, або задати зсув на кшталт «додай 2 місяці й 3 дні».

І так, важливий момент: поля в DateComponents часто є Optional. Це не тому, що розробники Foundation люблять ускладнювати студентам життя. Це тому, що компоненти можуть бути частковими: наприклад, у вас може бути лише рік і місяць, але без дня. Така модель чесніша.

Невелика таблиця-пам’ятка:

Компонент Що означає Приклад значення
year
календарний рік
2026
month
місяць (1...12) 1 (січень)
day
день місяця
16
hour
година
14
minute
хвилини
30

І одразу побутове уточнення: DateComponents — це не Date. Це лише набір деталей. А зібрати з них реальний момент часу вміє календар.

Витягаємо компоненти з Date: «який сьогодні день?»

Поки ми не навчилися красиво форматувати дату в рядок (це окрема тема), найзрозуміліший спосіб показати результат новачкові — витягти компоненти й надрукувати їх.

Після цього розділу ви зможете відповідати на запитання на кшталт: «Який зараз рік?», «Який сьогодні день місяця?» — і не перетворювати код на ворожіння з print(Date()).

import Foundation

let calendar = Calendar(identifier: .gregorian)
let now = Date()

let parts = calendar.dateComponents([.year, .month, .day], from: now)

print(parts.year as Any)   // наприклад: Optional(2026)
print(parts.month as Any)  // наприклад: Optional(1)
print(parts.day as Any)    // наприклад: Optional(16)

Тут as Any — це проста «шпаргалка», щоб можна було друкувати Optional без розгортання. Але в реальній логіці, особливо якщо ви щось обчислюєте, краще акуратно розгортати значення.

Приклад із if let:

import Foundation

let calendar = Calendar(identifier: .gregorian)
let parts = calendar.dateComponents([.year, .month, .day], from: Date())

if let y = parts.year, let m = parts.month, let d = parts.day {
    print("\(y)-\(m)-\(d)") // наприклад: 2026-1-16
}

Створюємо Date з компонентів: «дата руками користувача»

Зсув дат майже завжди починається з того, що у нас є стартова точка. Іноді це «зараз», але частіше — дата, яку ввів користувач: дата видачі книги, дата початку підписки, дата відпустки, яка в мріях завжди на місяць раніше.

Ми зробимо маленьку основу для нашого навчального консольного застосунку: користувач вводить рік, місяць і день, а ми намагаємося створити Date. І тут важливо звикнути до думки: calendar.date(from:) повертає Date? — тому що користувач може ввести неможливу дату, наприклад 31 лютого, класика жанру.

import Foundation

var comps = DateComponents()
comps.year = 2026
comps.month = 2
comps.day = 31

let calendar = Calendar(identifier: .gregorian)
let date = calendar.date(from: comps)

print(date as Any) // nil (ймовірно), тому що 31 лютого не буває

Якщо результат nil, це не помилка компілятора. Це календар чесно сказав: «Я не можу зібрати такий день у межах своїх правил».

4. Додаємо дні правильно: calendar.date(byAdding:value:to:)

Тепер — головне: календарне додавання днів. У Foundation для цього є метод:

calendar.date(byAdding: .day, value: N, to: someDate)

Він повертає Date?, і це логічно: деякі операції можуть залежати від контексту календаря й часового поясу. А ще це просто чесний контракт Foundation: якщо не можна — поверну nil.

Приклад «через тиждень»:

import Foundation

let calendar = Calendar(identifier: .gregorian)
let now = Date()

let nextWeek = calendar.date(byAdding: .day, value: 7, to: now)
print(nextWeek as Any) // Optional(....)

Щоб зробити приклад наочнішим, додамо вивід компонентів:

import Foundation

let calendar = Calendar(identifier: .gregorian)
let now = Date()

guard let nextWeek = calendar.date(byAdding: .day, value: 7, to: now) else {
    print("Не вдалося додати 7 днів")
    exit(0)
}

let a = calendar.dateComponents([.year, .month, .day], from: now)
let b = calendar.dateComponents([.year, .month, .day], from: nextWeek)

print(a.year!, a.month!, a.day!) // приклад: 2026 1 16
print(b.year!, b.month!, b.day!) // приклад: 2026 1 23

Зверніть увагу: тут я використав ! після year/month/day. У реальному коді краще розгортати значення обережно, але в цьому місці ми припускаємо, що календар для Date() ці компоненти дасть. Просто тримайте в голові: ! — це обіцянка, і її краще давати лише тоді, коли ви справді впевнені.

5. Додаємо місяці: «31 січня + 1 місяць»

Додавання місяців виглядає майже так само просто:

calendar.date(byAdding: .month, value: 1, to: date)

Але саме тут починаються запитання: «Що робити, якщо поточний день не існує в наступному місяці?» Наприклад, 31 січня + 1 місяць… а в лютому зазвичай немає 31 числа.

І ось тут важливо запам’ятати: календар сам обирає коректну дату за своїми правилами. Зазвичай це означає, що він «з’їде» на кінець місяця, наприклад на 28 або 29 лютого, але конкретна поведінка може залежати від календаря.

Приклад, наочний крайовий випадок:

import Foundation

let calendar = Calendar(identifier: .gregorian)

var comps = DateComponents()
comps.year = 2026
comps.month = 1
comps.day = 31

let start = calendar.date(from: comps) ?? Date()
let plusMonth = calendar.date(byAdding: .month, value: 1, to: start)

print(plusMonth as Any) // Optional(...), але дня не буде "31 лютого"

Щоб зрозуміти, що вийшло, знову витягнемо компоненти:

import Foundation

let calendar = Calendar(identifier: .gregorian)

var comps = DateComponents()
comps.year = 2026
comps.month = 1
comps.day = 31

guard let start = calendar.date(from: comps),
      let plusMonth = calendar.date(byAdding: .month, value: 1, to: start) else {
    print("Не вдалося зібрати або зсунути дату")
    exit(0)
}

let s = calendar.dateComponents([.year, .month, .day], from: start)
let p = calendar.dateComponents([.year, .month, .day], from: plusMonth)

print("\(s.year!)-\(s.month!)-\(s.day!)") // 2026-1-31
print("\(p.year!)-\(p.month!)-\(p.day!)") // 2026-2-28 (часто так)

Сенс такий: додаючи місяці, ви працюєте за календарними правилами, а отже маєте бути готові до «підрізання» дня до кінця місяця. Це не баг — це життя.

6. Додаємо кілька компонентів одразу: DateComponents як «зсув»

Іноді зручно додати не «лише 10 днів», а «2 місяці й 10 днів». Для цього є перевантаження, де ми додаємо цілий DateComponents:

calendar.date(byAdding: components, to: date)

Це дуже схоже на «вектор зміщення».

import Foundation

let calendar = Calendar(identifier: .gregorian)
let now = Date()

var delta = DateComponents()
delta.month = 2
delta.day = 10

let shifted = calendar.date(byAdding: delta, to: now)
print(shifted as Any) // Optional(...)

І це ще одна причина, чому DateComponents зроблено з Optional: ви можете задати лише ті частини зсуву, які вам потрібні. Не задаєте годину — отже, годину не змінюємо.

7. Мінізастосунок: обчислюємо дату повернення книги

Тепер зберімо все в невеликий консольний застосунок. Уявімо, що ми пишемо просту утиліту для бібліотечної рутини: користувач вводить дату видачі книги, а застосунок обчислює дату повернення. Іноді строк задають у днях (наприклад, 14), іноді — «на 1 місяць» (так, так теж буває, особливо для абонементів).

Ми не використовуватимемо DateFormatter, щоб не забігати наперед: введення буде трьома числами (рік, місяць, день), а вивід теж зробимо через компоненти.

Функція читання числа: маленька, але важлива цеглинка

Почнімо з безпечного читання Int, тому що користувач — істота непередбачувана.

import Foundation

func readInt() -> Int {
    let line = readLine() ?? ""
    return Int(line) ?? 0
}

Так, це дуже «м’яка» версія: якщо ввели не число — отримаємо 0. Для навчального прикладу нормально: ми ще вчимося «зшивати» частини застосунку.

Збираємо дату видачі з компонентів

import Foundation

let calendar = Calendar(identifier: .gregorian)

print("Рік видачі:")
let y = readInt()

print("Місяць видачі (1-12):")
let m = readInt()

print("День видачі:")
let d = readInt()

var issue = DateComponents()
issue.year = y
issue.month = m
issue.day = d

let issueDate = calendar.date(from: issue)
print(issueDate as Any) // Optional(...) або nil

Якщо issueDate == nil, отже користувач увів неможливу дату, і далі рахувати немає сенсу.

Додаємо строк у днях і місяцях

Зробімо мінімальну логіку: користувач вводить строк у днях і строк у місяцях. Якщо введено обидва значення — додамо обидва. Чому б і ні.

import Foundation

guard let issueDate = issueDate else {
    print("Некоректна дата видачі")
    exit(0)
}

print("Скільки днів додати?")
let days = readInt()

print("Скільки місяців додати?")
let months = readInt()

var delta = DateComponents()
delta.day = days
delta.month = months

let dueDate = calendar.date(byAdding: delta, to: issueDate)
print(dueDate as Any) // Optional(...) або nil

Виводимо результат без форматерів: друкуємо компоненти

import Foundation

guard let dueDate = dueDate else {
    print("Не вдалося обчислити дату повернення")
    exit(0)
}

let out = calendar.dateComponents([.year, .month, .day], from: dueDate)

if let y2 = out.year, let m2 = out.month, let d2 = out.day {
    print("Повернути до: \(y2)-\(m2)-\(d2)") // наприклад: Повернути до: 2026-2-13
}

На цьому етапі у вас уже є робоча річ, яка робить головне: не намагається рахувати календар через секунди, а чесно звертається до Calendar.

8. Нюанси: startOfDay(for:) і логіка «за днями»

Коли ви обчислюєте дедлайн за днями, важливо пам’ятати, що Date — це завжди момент часу, включно з годинами, хвилинами й секундами. Якщо ви створили дату з компонентів лише year/month/day, то календар зазвичай виставить час на початок дня, але це залежить від контексту. А якщо ви взяли Date() як «зараз», то час там буде поточний.

Іноді бізнес-логіка вимагає: «дедлайн — це саме календарний день, без прив’язки до часу». Тоді корисний прийом — приводити дату до початку дня через startOfDay(for:). Це допомагає уникнути сюрпризів, коли «плюс один день» начебто має означати завтра, але через час вивід виглядає дивно.

import Foundation

let calendar = Calendar(identifier: .gregorian)
let now = Date()

let dayStart = calendar.startOfDay(for: now)
let tomorrow = calendar.date(byAdding: .day, value: 1, to: dayStart)

print(dayStart)           // технічний вивід
print(tomorrow as Any)    // Optional(...)

Цей прийом особливо корисний, коли ви порівнюєте дати за днями: спочатку нормалізуєте їх до початку дня, а потім порівнюєте.

9. Типові помилки

Помилка №1: додавати дні через addingTimeInterval(86400) і вважати, що це «наступний календарний день».
На тестових прикладах це часто «ніби працює», і саме тому помилка підступна. Щойно ви потрапите на реальний часовий пояс із переходами на літній або зимовий час, виявиться, що ви додали 24 години, а не «перейшли на завтра». Для календарних днів, місяців і років використовуйте Calendar.date(byAdding:...).

Помилка №2: забувати, що calendar.date(from:) і calendar.date(byAdding:...) повертають Optional.
Новачок часто думає: «Ну я ж усе правильно написав, чому це Date??» Тому що користувач може ввести неіснуючу дату, а додавання може виявитися неможливим у межах правил календаря. Правильний стиль — guard let із зрозумілим повідомленням та раннім виходом.

Помилка №3: плутати «місяць» і «день», ніби вони однакові за складністю.
Дні йдуть один за одним, і додавання днів зазвичай інтуїтивно зрозуміле. Місяці ж мають різну довжину. Тому кейси на кшталт «31 січня + 1 місяць» треба сприймати як норму, а не як «дивний баг». Календар приведе дату до коректної, і цю поведінку потрібно враховувати.

Помилка №4: очікувати, що DateComponents завжди повний і не містить nil.
Компоненти дати й компоненти зсуву можуть бути частковими. Це нормальна модель. Якщо ви витягнули year/month/day із Date, то розгортайте значення акуратно (if let) або друкуйте їх як as Any, але не пишіть код так, ніби там завжди лежить готова трійка чисел.

Помилка №5: змішувати «момент часу» і «календарний день» в одній логіці без нормалізації.
Якщо ви порівнюєте дати за днями, але в одній даті час 00:00, а в іншій 23:59, ви можете отримати несподіваний результат. У таких сценаріях корисно приводити дати до початку дня через calendar.startOfDay(for:), а вже потім виконувати порівняння та зсуви.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ