Практика · Тема 22 · Дати й час¶
Лекція: lecture.html · Тест: quiz.html · Домашнє завдання: homework.md
Наскрізний приклад той самий, що в лекції, — журнал замовлень невеликої крамниці. Замовлення №4417 оформлене 28 березня 2026 року о 14:05:09 за київським часом.
Що зробимо руками:
- розкладемо момент на поля й переконаємось, що обʼєкт незмінний;
- побачимо справжній
TypeErrorпри порівнянні наївного з обізнаним — і полагодимо його; - переведемо одну мить у чотири пояси й доведемо
assert-ом, що це та сама мить; - відтворимо помилку, через яку прибрали
utcnow(); - порахуємо вік у днях і різницю дат;
- упремося в те, що в
timedeltaнемає місяців, і напишемо «плюс місяць» самі; - виміряємо добу переходу на літній час — у ній 23 години;
- відформатуємо дату українською й розберемо рядки журналу назад;
- відсортуємо дати як рядки й побачимо, чому так робити не можна;
- напишемо власний підрахунок днів між датами й звіримо його з бібліотечним.
Усі дати тут зашиті числами. Жодного
now()— тому зошит дає однаковий вивід при кожному запуску, і його результат можна перевіряти очима.
1 · Дані, з якими працюємо¶
Журнал прийшов текстом: кожен рядок — «номер;дата;час». Дати записані звичним нам
форматом дд.мм.рррр, і саме він потім створить нам проблему при сортуванні.
рядки_журналу = [
"4417;28.03.2026;14:05:09",
"4418;02.11.2025;09:30:00",
"4419;15.01.2026;23:45:12",
"4420;31.12.2024;18:00:00",
"4421;05.03.2026;07:15:40",
"4422;09.07.2025;12:00:00",
]
print("рядків у журналі:", len(рядки_журналу))
for рядок in рядки_журналу:
print(" ", рядок)
рядків у журналі: 6 4417;28.03.2026;14:05:09 4418;02.11.2025;09:30:00 4419;15.01.2026;23:45:12 4420;31.12.2024;18:00:00 4421;05.03.2026;07:15:40 4422;09.07.2025;12:00:00
2 · Три типи: date, time, datetime¶
date знає лише календар, time — лише циферблат, datetime — і те, і те.
Зібрати перші два в третій допомагає datetime.combine.
from datetime import date, time, datetime
дата_замовлення = date(2026, 3, 28)
час_замовлення = time(14, 5, 9)
момент_замовлення = datetime.combine(дата_замовлення, час_замовлення)
print("date :", дата_замовлення)
print("time :", час_замовлення)
print("datetime :", момент_замовлення)
print("той самий datetime напряму:", datetime(2026, 3, 28, 14, 5, 9))
date : 2026-03-28 time : 14:05:09 datetime : 2026-03-28 14:05:09 той самий datetime напряму: 2026-03-28 14:05:09
Поля читаються через крапку, а сам обʼєкт незмінний¶
Спроба присвоїти полю значення дасть AttributeError. Щоб отримати змінений момент,
роблять копію методом replace — оригінал при цьому лишається цілим.
print("рік :", момент_замовлення.year)
print("місяць :", момент_замовлення.month)
print("день :", момент_замовлення.day)
print("година :", момент_замовлення.hour)
# replace повертає НОВИЙ обʼєкт — тому оригінал можна безпечно передавати у функції
перенесене = момент_замовлення.replace(hour=18, minute=0, second=0)
print()
print("після replace :", перенесене)
print("оригінал :", момент_замовлення, "— не змінився")
assert момент_замовлення.hour == 14, "replace не мав чіпати оригінал!"
print("✅ оригінал справді незмінний")
рік : 2026 місяць : 3 день : 28 година : 14 після replace : 2026-03-28 18:00:00 оригінал : 2026-03-28 14:05:09 — не змінився ✅ оригінал справді незмінний
3 · Наївний проти обізнаного¶
У наївного моменту поле tzinfo порожнє. Обʼєкт знає, що написано на циферблаті,
і не знає, чий це циферблат.
наївний = datetime(2026, 3, 28, 14, 5, 9)
print("наївний момент :", наївний)
print("його tzinfo :", наївний.tzinfo)
print("чи наївний :", наївний.tzinfo is None)
наївний момент : 2026-03-28 14:05:09 його tzinfo : None чи наївний : True
Тепер зробимо обізнаний — і спробуємо їх порівняти¶
Наступна клітинка навмисно падає. Це та сама помилка, заради якої написано половину лекції: порівняти напис без адреси з конкретною миттю неможливо.
from zoneinfo import ZoneInfo
# ZoneInfo читає системну базу поясів IANA.
# Якщо на твоїй машині її немає (типово для Windows) — постав пакет: pip install tzdata
київ = ZoneInfo("Europe/Kyiv")
обізнаний = datetime(2026, 3, 28, 14, 5, 9, tzinfo=київ)
print("обізнаний момент :", обізнаний)
print("його tzinfo :", обізнаний.tzinfo)
print("зміщення від UTC :", обізнаний.utcoffset())
обізнаний момент : 2026-03-28 14:05:09+02:00 його tzinfo : Europe/Kyiv зміщення від UTC : 2:00:00
# навмисна помилка: порівняння наївного з обізнаним
наївний < обізнаний
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[6], line 2 1 # навмисна помилка: порівняння наївного з обізнаним ----> 2 наївний < обізнаний TypeError: can't compare offset-naive and offset-aware datetimes
Та сама операція під try — і тиха пастка поруч¶
Оператор < чесно зупиняє програму. А от == помилки не кидає: він мовчки
повертає False. Саме через це перевірка на рівність із наївним моментом
ніколи не спрацьовує і ніяк про це не повідомляє.
try:
наївний < обізнаний
except TypeError as помилка:
print("TypeError:", помилка)
# а тут помилки не буде — і це найгірше
print()
print("наївний == обізнаний ->", наївний == обізнаний)
print("хоча на циферблатах у них однакові 14:05:09")
assert (наївний == обізнаний) is False, "рівність із наївним не мала спрацювати"
print("✅ пастка відтворена: мовчазний False замість помилки")
TypeError: can't compare offset-naive and offset-aware datetimes наївний == обізнаний -> False хоча на циферблатах у них однакові 14:05:09 ✅ пастка відтворена: мовчазний False замість помилки
4 · Одна мить у чотирьох поясах¶
astimezone не змінює момент — він переписує ту саму мить іншим циферблатом.
Доведемо це assert-ом: unix-час усіх чотирьох записів мусить збігатися до секунди.
пояси = {
"Київ": ZoneInfo("Europe/Kyiv"),
"Лондон": ZoneInfo("Europe/London"),
"Токіо": ZoneInfo("Asia/Tokyo"),
"Нью-Йорк": ZoneInfo("America/New_York"),
}
for місто, пояс in пояси.items():
місцевий = обізнаний.astimezone(пояс)
print(f"{місто:<10} {місцевий}")
# головна перевірка: усі чотири записи позначають ОДНУ мить
мітки = [обізнаний.astimezone(пояс).timestamp() for пояс in пояси.values()]
assert len(set(мітки)) == 1, "переведення в пояс не має зсувати мить!"
print()
print("✅ unix-час усіх чотирьох записів однаковий:", мітки[0])
Київ 2026-03-28 14:05:09+02:00 Лондон 2026-03-28 12:05:09+00:00 Токіо 2026-03-28 21:05:09+09:00 Нью-Йорк 2026-03-28 08:05:09-04:00 ✅ unix-час усіх чотирьох записів однаковий: 1774699509.0
5 · Чому прибрали utcnow()¶
datetime.utcnow() повертав наївний обʼєкт із часом за UTC — тобто мовчав про
найважливіше. Відтворимо помилку без виклику самого методу: візьмімо два наївні
моменти, які насправді позначають одну й ту саму мить.
# 15 червня 2026, 09:00 UTC — це рівно 12:00 у Києві (влітку +03:00)
так_дав_би_utcnow = datetime(2026, 6, 15, 9, 0) # наївний, у полях час за UTC
так_дав_би_now = datetime(2026, 6, 15, 12, 0) # наївний, у полях київський час
# обидва наївні, тому Python порівнює їх БЕЗ помилки — просто зіставляє цифри
різниця = так_дав_би_now - так_дав_би_utcnow
print("Python каже, що між ними:", різниця)
print("а насправді це одна й та сама мить")
Python каже, що між ними: 3:00:00 а насправді це одна й та сама мить
from datetime import timezone
# правильний спосіб: обидва моменти обізнані
правильно_utc = datetime(2026, 6, 15, 9, 0, tzinfo=timezone.utc)
правильно_київ = datetime(2026, 6, 15, 12, 0, tzinfo=ZoneInfo("Europe/Kyiv"))
print("UTC-варіант :", правильно_utc)
print("Київський :", правильно_київ)
print("справжня різниця:", правильно_київ - правильно_utc)
assert правильно_київ == правильно_utc, "це мала бути одна й та сама мить"
print("✅ з поясами Python бачить, що це одна мить")
UTC-варіант : 2026-06-15 09:00:00+00:00 Київський : 2026-06-15 12:00:00+03:00 справжня різниця: 0:00:00 ✅ з поясами Python бачить, що це одна мить
6 · Вік у днях і різниця дат¶
Відніми одну дату від іншої — отримаєш timedelta. Поле .days дає повні доби.
Дату «сьогодні» беремо фіксовану: інакше зошит давав би різний результат щодня.
дата_народження = date(2010, 9, 14)
умовне_сьогодні = date(2026, 3, 28) # фіксуємо замість date.today()
прожито = умовне_сьогодні - дата_народження
print("тип різниці двох дат:", type(прожито).__name__)
print("прожито діб :", прожито.days)
print("це приблизно років :", round(прожито.days / 365.2425, 2))
assert прожито.days == 5674, "перевір арифметику дат"
print("✅ вік у днях порахований")
тип різниці двох дат: timedelta прожито діб : 5674 це приблизно років : 15.53 ✅ вік у днях порахований
7 · У timedelta немає місяців¶
Наступна клітинка навмисно падає. Місяць не має сталої довжини — 28, 29, 30 або 31 день — тому «плюс місяць» не є проміжком часу.
from datetime import timedelta
timedelta(months=1)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[12], line 3 1 from datetime import timedelta ----> 3 timedelta(months=1) TypeError: 'months' is an invalid keyword argument for __new__()
Отже «плюс місяць» доводиться описати самому¶
І одразу стає видно, чому цього немає в стандартній бібліотеці: доводиться самому вирішити, що робити з 31 січня. Ми оберемо найпоширеніше правило — обрізати день до останнього наявного в цільовому місяці.
import calendar
def додати_місяць(дата):
"""Той самий день наступного місяця; якщо такого дня немає — останній наявний."""
місяць = дата.month + 1
рік = дата.year
if місяць > 12: # грудень переносить нас у наступний рік
місяць = 1
рік += 1
# monthrange повертає (день тижня 1-го числа, кількість днів у місяці)
останній_день = calendar.monthrange(рік, місяць)[1]
return дата.replace(year=рік, month=місяць, day=min(дата.day, останній_день))
for проба in [date(2026, 1, 31), date(2024, 1, 31), date(2026, 3, 28), date(2026, 12, 15)]:
print(проба, "+ місяць ->", додати_місяць(проба))
# 2024-й високосний, тому 31 січня їде на 29 лютого, а не на 28
assert додати_місяць(date(2024, 1, 31)) == date(2024, 2, 29)
assert додати_місяць(date(2026, 1, 31)) == date(2026, 2, 28)
print()
print("✅ обрізання дня працює і у високосний рік, і у звичайний")
2026-01-31 + місяць -> 2026-02-28 2024-01-31 + місяць -> 2024-02-29 2026-03-28 + місяць -> 2026-04-28 2026-12-15 + місяць -> 2027-01-15 ✅ обрізання дня працює і у високосний рік, і у звичайний
8 · Доба, у якій 23 години¶
29 березня 2026 року Київ переводить годинники: о третій ночі стрілки стрибають на четверту. Порівняємо два способи «додати добу» — і побачимо, що вони дають різні відповіді.
старт = datetime(2026, 3, 28, 14, 5, tzinfo=київ)
# спосіб 1 — настінна арифметика: додаємо до полів, циферблат лишається на 14:05
завтра_о_тій_самій_порі = старт + timedelta(days=1)
# спосіб 2 — фізичний час: рахуємо в UTC, де переходів не буває
через_добу = (старт.astimezone(timezone.utc) + timedelta(hours=24)).astimezone(київ)
print("старт :", старт)
print("спосіб 1 (+timedelta у поясі):", завтра_о_тій_самій_порі)
print("спосіб 2 (+24 години в UTC) :", через_добу)
старт : 2026-03-28 14:05:00+02:00 спосіб 1 (+timedelta у поясі): 2026-03-29 14:05:00+03:00 спосіб 2 (+24 години в UTC) : 2026-03-29 15:05:00+03:00
# скільки часу минуло НАСПРАВДІ — рахуємо в UTC, інакше відповідь буде настінною
def справжня_тривалість(від, до):
"""Фізичний час між двома обізнаними моментами, без впливу переходів."""
return до.astimezone(timezone.utc) - від.astimezone(timezone.utc)
# total_seconds() зручніший за друк timedelta: «1 day» і «23:00:00» на око не порівняти
print("спосіб 1 — минуло годин:", справжня_тривалість(старт, завтра_о_тій_самій_порі).total_seconds() / 3600)
print("спосіб 2 — минуло годин:", справжня_тривалість(старт, через_добу).total_seconds() / 3600)
print()
print("а ось наївне віднімання в одному поясі бреше:")
print(" завтра_о_тій_самій_порі - старт =", завтра_о_тій_самій_порі - старт)
assert справжня_тривалість(старт, завтра_о_тій_самій_порі) == timedelta(hours=23)
assert справжня_тривалість(старт, через_добу) == timedelta(hours=24)
assert завтра_о_тій_самій_порі - старт == timedelta(days=1) # ось вона, настінна відповідь
print()
print("✅ доба переходу справді має 23 години")
спосіб 1 — минуло годин: 23.0 спосіб 2 — минуло годин: 24.0 а ось наївне віднімання в одному поясі бреше: завтра_о_тій_самій_порі - старт = 1 day, 0:00:00 ✅ доба переходу справді має 23 години
9 · Форматування: strftime¶
Коди %B і %A дають назви місяця й дня тижня мовою локалі процесу —
за замовчуванням англійською. Українські назви надійніше зробити своїм списком:
результат тоді не залежить від налаштувань чужого сервера.
шаблони = [
"%Y-%m-%d",
"%d.%m.%Y",
"%H:%M",
"%Y-%m-%d %H:%M:%S",
"%d.%m.%Y о %H:%M",
]
for шаблон in шаблони:
print(f"{шаблон:<22} -> {момент_замовлення.strftime(шаблон)}")
print()
print("залежить від локалі:", момент_замовлення.strftime("%A, %d %B %Y"))
%Y-%m-%d -> 2026-03-28 %d.%m.%Y -> 28.03.2026 %H:%M -> 14:05 %Y-%m-%d %H:%M:%S -> 2026-03-28 14:05:09 %d.%m.%Y о %H:%M -> 28.03.2026 о 14:05 залежить від локалі: Saturday, 28 March 2026
МІСЯЦІ_УКР = ["січня", "лютого", "березня", "квітня", "травня", "червня",
"липня", "серпня", "вересня", "жовтня", "листопада", "грудня"]
ДНІ_УКР = ["понеділок", "вівторок", "середа", "четвер", "пʼятниця", "субота", "неділя"]
def українською(момент):
"""Дата словами — без залежності від локалі системи."""
назва_місяця = МІСЯЦІ_УКР[момент.month - 1]
назва_дня = ДНІ_УКР[момент.weekday()] # weekday(): понеділок = 0
return f"{момент.day} {назва_місяця} {момент.year}, {назва_дня}"
print(українською(момент_замовлення))
print(українською(datetime(2026, 12, 31, 23, 59)))
assert українською(момент_замовлення) == "28 березня 2026, субота"
print()
print("✅ українські назви не залежать від локалі")
28 березня 2026, субота 31 грудня 2026, четвер ✅ українські назви не залежать від локалі
10 · Розбір рядків журналу: strptime¶
Дзеркальна операція. Шаблон той самий, але тепер він описує, що ми очікуємо
побачити в рядку. Якщо рядок не збігається з шаблоном — буде ValueError.
def розібрати_запис(рядок):
"""Перетворює "номер;дд.мм.рррр;гг:хх:сс" на пару (номер, обізнаний момент)."""
номер, текст_дати, текст_часу = рядок.split(";")
момент = datetime.strptime(f"{текст_дати} {текст_часу}", "%d.%m.%Y %H:%M:%S")
# журнал вели за київським часом — дописуємо пояс, поки момент не поїхав далі
return номер, момент.replace(tzinfo=київ)
замовлення = [розібрати_запис(рядок) for рядок in рядки_журналу]
for номер, момент in замовлення:
print(f"№{номер} {момент}")
assert len(замовлення) == 6
assert замовлення[0][1] == datetime(2026, 3, 28, 14, 5, 9, tzinfo=київ)
print()
print("✅ усі шість рядків розібрано")
№4417 2026-03-28 14:05:09+02:00 №4418 2025-11-02 09:30:00+02:00 №4419 2026-01-15 23:45:12+02:00 №4420 2024-12-31 18:00:00+02:00 №4421 2026-03-05 07:15:40+02:00 №4422 2025-07-09 12:00:00+03:00 ✅ усі шість рядків розібрано
# а тепер рядок із чужого журналу — там дата в американському порядку
битий_рядок = "4423;03/04/2026;10:00:00"
try:
розібрати_запис(битий_рядок)
except ValueError as помилка:
print("ValueError:", помилка)
print()
print("І найгірше: неоднозначний рядок розберуть ОБИДВА шаблони, без помилки —")
print(" як %d/%m/%Y ->", datetime.strptime("03/04/2026", "%d/%m/%Y").date())
print(" як %m/%d/%Y ->", datetime.strptime("03/04/2026", "%m/%d/%Y").date())
print("Це і є класична причина мовчазного псування дат при імпорті чужих таблиць.")
ValueError: time data '03/04/2026 10:00:00' does not match format '%d.%m.%Y %H:%M:%S' І найгірше: неоднозначний рядок розберуть ОБИДВА шаблони, без помилки — як %d/%m/%Y -> 2026-04-03 як %m/%d/%Y -> 2026-03-04 Це і є класична причина мовчазного псування дат при імпорті чужих таблиць.
11 · ISO 8601: формат для зберігання¶
isoformat() і fromisoformat() працюють без жодних шаблонів і зберігають пояс.
Перевіримо, що обіг «обʼєкт → рядок → обʼєкт» повертає рівно те саме.
рядок_iso = обізнаний.isoformat()
назад = datetime.fromisoformat(рядок_iso)
print("обʼєкт :", обізнаний)
print("isoformat() :", рядок_iso)
print("назад :", назад)
assert назад == обізнаний, "обіг через ISO мав повернути той самий момент"
print()
print("✅ ISO-обіг без втрат — на відміну від %d.%m.%Y, де зникає пояс і секунди")
# з Python 3.11 читається і скорочений запис UTC літерою Z
print("літера Z теж читається:", datetime.fromisoformat("2026-03-28T12:05:09Z"))
обʼєкт : 2026-03-28 14:05:09+02:00 isoformat() : 2026-03-28T14:05:09+02:00 назад : 2026-03-28 14:05:09+02:00 ✅ ISO-обіг без втрат — на відміну від %d.%m.%Y, де зникає пояс і секунди літера Z теж читається: 2026-03-28 12:05:09+00:00
12 · Чому дати не сортують як рядки¶
Сортування рядків посимвольне: воно порівнює перший символ, при рівності — другий.
У записі 28.03.2026 першим стоїть день місяця — тому весь журнал вишикується
за днем, а роки перемішаються.
дати_текстом = [рядок.split(";")[1] for рядок in рядки_журналу]
дати_обʼєктами = [момент.date() for _, момент in замовлення]
print("як записано в журналі:", дати_текстом)
print()
print("sorted() над рядками :", sorted(дати_текстом))
print("а правильний порядок :", [d.strftime('%d.%m.%Y') for d in sorted(дати_обʼєктами)])
як записано в журналі: ['28.03.2026', '02.11.2025', '15.01.2026', '31.12.2024', '05.03.2026', '09.07.2025'] sorted() над рядками : ['02.11.2025', '05.03.2026', '09.07.2025', '15.01.2026', '28.03.2026', '31.12.2024'] а правильний порядок : ['31.12.2024', '09.07.2025', '02.11.2025', '15.01.2026', '05.03.2026', '28.03.2026']
# ISO-рядки — єдиний формат дати, який можна сортувати як текст
дати_iso = [d.isoformat() for d in дати_обʼєктами]
порядок_за_рядками_iso = sorted(дати_iso)
порядок_за_обʼєктами = [d.isoformat() for d in sorted(дати_обʼєктами)]
print("sorted() над ISO-рядками :", порядок_за_рядками_iso)
print("sorted() над обʼєктами :", порядок_за_обʼєктами)
assert порядок_за_рядками_iso == порядок_за_обʼєктами, "ISO-рядки мали збігтися з датами"
assert sorted(дати_текстом) != [d.strftime('%d.%m.%Y') for d in sorted(дати_обʼєктами)]
print()
print("✅ ISO збігається з хронологією, дд.мм.рррр — ні")
sorted() над ISO-рядками :
['2024-12-31', '2025-07-09', '2025-11-02', '2026-01-15', '2026-03-05', '2026-03-28'] sorted() над обʼєктами : ['2024-12-31', '2025-07-09', '2025-11-02', '2026-01-15', '2026-03-05', '2026-03-28'] ✅ ISO збігається з хронологією, дд.мм.рррр — ні
13 · Наша реалізація проти бібліотечної¶
Найцікавіше наостанок. Порахуємо кількість днів між двома датами самі —
через григоріанський календар, без жодного timedelta — і звіримо з тим,
що дає бібліотека. Усередині date немає магії: там рівно ця арифметика.
ДОВЖИНИ_МІСЯЦІВ = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def високосний(рік):
"""Григоріанське правило трьох поділів."""
return рік % 4 == 0 and (рік % 100 != 0 or рік % 400 == 0)
def днів_від_початку_ери(рік, місяць, день):
"""Скільки повних діб минуло від 1 січня 1 року до цієї дати."""
# 1. усі повні роки до поточного: 365 днів кожен плюс високосні дні
повних_років = рік - 1
діб = (повних_років * 365
+ повних_років // 4 # раз на чотири роки додаємо день
- повних_років // 100 # але не в роки, кратні 100
+ повних_років // 400) # хіба що вони кратні ще й 400
# 2. повні місяці цього року
for номер_місяця in range(1, місяць):
діб += ДОВЖИНИ_МІСЯЦІВ[номер_місяця - 1]
if номер_місяця == 2 and високосний(рік):
діб += 1
# 3. повні дні цього місяця
діб += день - 1
return діб
print("1 січня 1 року ->", днів_від_початку_ери(1, 1, 1))
print("1 січня 1970 року ->", днів_від_початку_ери(1970, 1, 1))
print("28 березня 2026 року ->", днів_від_початку_ери(2026, 3, 28))
1 січня 1 року -> 0 1 січня 1970 року -> 719162 28 березня 2026 року -> 739702
def днів_між(перша, друга):
"""Наш власний підрахунок різниці двох дат, без timedelta."""
return (днів_від_початку_ери(друга.year, друга.month, друга.day)
- днів_від_початку_ери(перша.year, перша.month, перша.day))
пари_для_перевірки = [
(date(2010, 9, 14), date(2026, 3, 28)), # вік із розділу 6
(date(2024, 2, 28), date(2024, 3, 1)), # через 29 лютого високосного року
(date(2026, 2, 28), date(2026, 3, 1)), # той самий проміжок, але рік звичайний
(date(1899, 12, 31), date(1900, 3, 1)), # через 1900-й: він НЕ високосний
(date(1999, 12, 31), date(2000, 3, 1)), # через 2000-й: а він високосний
]
for перша, друга in пари_для_перевірки:
наш = днів_між(перша, друга)
бібліотечний = (друга - перша).days
print(f"{перша} → {друга}: наш {наш:>5}, бібліотека {бібліотечний:>5}")
assert наш == бібліотечний, "розрахунок розійшовся!"
print()
print("✅ збігається — усередині date рівно ця арифметика, без магії")
2010-09-14 → 2026-03-28: наш 5674, бібліотека 5674 2024-02-28 → 2024-03-01: наш 2, бібліотека 2 2026-02-28 → 2026-03-01: наш 1, бібліотека 1 1899-12-31 → 1900-03-01: наш 60, бібліотека 60 1999-12-31 → 2000-03-01: наш 61, бібліотека 61 ✅ збігається — усередині date рівно ця арифметика, без магії
# і остаточна перевірка на широкому діапазоні: кожна дата з 1900 по 2100 рік
початок = date(1900, 1, 1)
розбіжностей = 0
проба = початок
while проба <= date(2100, 12, 31):
if днів_між(початок, проба) != (проба - початок).days:
розбіжностей += 1
проба += timedelta(days=1)
print("перевірено дат:", (date(2100, 12, 31) - початок).days + 1)
print("розбіжностей :", розбіжностей)
assert розбіжностей == 0, "наша формула розійшлася з бібліотечною"
print("✅ 200 років поспіль — жодної розбіжності")
перевірено дат: 73414 розбіжностей : 0 ✅ 200 років поспіль — жодної розбіжності
14 · Що ми з'ясували¶
- наївний момент — це напис без адреси; порівняти його з обізнаним не можна,
а
==при цьому мовчки повертаєFalse; astimezoneне рухає мить, а лише переписує її іншим циферблатом — це підтвердив однаковий unix-час у чотирьох поясах;- у
timedeltaнемає місяців, бо в місяця немає сталої довжини; «плюс місяць» довелося описати самим і самим вирішити, що робити з 31 січня; - доба переходу на літній час має 23 години, і два різні способи «додати добу» дають різні відповіді — обидві правильні;
- ISO-рядки сортуються як дати,
дд.мм.рррр— ні; - усередині
dateнемає магії: наш підрахунок днів збігся з бібліотечним на кожній з 73 414 дат.
Завдання трьох рівнів¶
🟢 Рівень 1 — База¶
Додай у журнал ще три записи зі своїми датами й порахуй, скільки днів минуло
між найранішим і найпізнішим замовленням. Виведи обидві дати українською
через функцію українською.
Зроблено, якщо: число днів збігається з (макс - мін).days, а обидві дати
надруковані словами.
🟡 Рівень 2 — Плюс¶
Напиши функцію робочих_днів(перша, друга), яка рахує кількість буднів
(понеділок–пʼятниця) між двома датами. Перевір її assert-ом на тижні, який
ти можеш порахувати руками, і на проміжку, що містить 29 лютого 2024 року.
Зроблено, якщо: обидва assert проходять, а функція правильно поводиться,
коли перша дата пізніша за другу.
🔴 Рівень 3 — Виклик¶
Знайди всі моменти переходу на літній час у поясі Europe/Kyiv за 2020–2030 роки,
не заглядаючи в довідники: перебирай години в UTC і лови зміну utcoffset().
Для кожного переходу надрукуй дату, місцевий час до та після, і довжину тієї доби
в годинах.
Зроблено, якщо: знайдено рівно 22 переходи (по два на рік), у весняних добах 23 години, в осінніх — 25, і жодна дата не зашита в коді руками.
Підказки¶
date.weekday()дає 0 для понеділка й 6 для неділі — будні це< 5.- Щоб пройти рік по годинах, зручно почати з
datetime(рік, 1, 1, tzinfo=timezone.utc)і додаватиtimedelta(hours=1), щоразу дивлячись на.astimezone(київ).utcoffset(). - Довжину місцевої доби рахують як різницю двох опівночей у UTC: саме тому вона й виходить не завжди 24 години.