# Домашнє завдання · Числа й арифметика

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb) · Тест: [quiz.html](quiz.html)

Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який.
Пиши в окремому файлі `homework_04.py` або в новому зошиті — головне, щоб код
запускався й друкував результат.

Дозволено з бібліотек: `math`, `decimal`, `fractions`, `sys`. Більше нічого не треба.

---

## 🟢 Рівень 1 — База

### 1.1 · Розкладач часу

Напиши функцію `розкласти_секунди(всього)`, яка перетворює кількість секунд
у кортеж `(дні, години, хвилини, секунди)`. Використай тільки `//` і `%` —
жодних `datetime`.

Виведи результат для `всього = 1_000_000` у форматі
`1000000 с = 11 д 13 год 46 хв 40 с`.

**Зроблено, якщо:** для будь-якого невід'ємного цілого виконується
```python
assert дні * 86400 + години * 3600 + хвилини * 60 + секунди == всього
```
і всі три молодші компоненти лежать у своїх діапазонах
(`години < 24`, `хвилини < 60`, `секунди < 60`).

### 1.2 · Чек без копійчаних втрат

Дано список цін рядками: `["149.99", "3.55", "27.05", "0.07"]`.
Порахуй суму двома способами — через `float` і через `Decimal` із рядка —
і надрукуй обидві разом із точним десятковим значенням float-суми
(`Decimal(float_сума)`).

**Зроблено, якщо:** твій код друкує рядок, у якому видно, що Decimal-сума
дорівнює рівно `180.66`, а float-сума — ні, і виконується
```python
assert decimal_сума == Decimal("180.66")
assert float_сума != 180.66
```

> Спробуй потім замінити список на `["149.99", "3.50", "27.05", "0.01"]` —
> там float-сума випадково зійдеться рівно. Це і є найгірша властивість
> помилок із рухомою крапкою: вони не завжди себе показують.

---

## 🟡 Рівень 2 — Плюс

### 2.1 · Чесний калькулятор здачі

Напиши функцію `здача(сума_копійок, номінали)`, яка розкладає суму на найменшу
кількість купюр і монет. `номінали` — список у копійках, відсортований за спаданням,
наприклад `[50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 25, 10, 5, 2, 1]`.
Функція повертає список пар `(номінал, скільки штук)`, пропускаючи нульові.

Уся арифметика — на цілих числах. Жодного `float` у розв'язку бути не повинно.

**Зроблено, якщо:** для `здача(78_63, номінали)` (тобто 78 грн 63 коп) сума
`номінал * кількість` по всіх парах точно дорівнює вихідній сумі, і це перевірено
через `assert`. Додатково: покажи, що для суми `10_00` результат складається
рівно з однієї позиції.

### 2.2 · Де ламається наївне порівняння

Досліди, наскільки далеко можна відійти від числа, щоб `==` іще казав `True`.
Візьми три опорні числа: `1.0`, `1000.0` і `1e15`. Для кожного знайди найменше
додатне `крок`, при якому `x + крок != x`.

Підказка: не перебирай — скористайся `math.ulp(x)` і перевір, що
`x + math.ulp(x) != x`, а `x + math.ulp(x) / 2 == x`.

Побудуй таблицю: опорне число, `math.ulp`, і у скільки разів крок більший,
ніж біля одиниці.

**Зроблено, якщо:** у таблиці видно, що крок росте пропорційно самому числу,
і ти пояснив словами (2-3 речення), чому біля `1e16` додавання одиниці
взагалі нічого не змінює. Перевір це твердження через
```python
assert 1e16 + 1 == 1e16
```

---

## 🔴 Рівень 3 — Виклик

### 3.1 · Свій розкладач float на біти

Напиши функцію `розкласти(x)`, яка для додатного `float` повертає кортеж
`(поле_порядку, мантиса)` — два цілі числа з внутрішнього представлення,
**не використовуючи** `struct`, `Decimal` і `.hex()`.

Дозволено: `math.frexp`, цілочисельна арифметика, зсуви.

Потім напиши зворотну функцію `зібрати(поле_порядку, мантиса)` і переконайся,
що вона повертає рівно вихідне число.

**Зроблено, якщо:** виконуються всі три перевірки
```python
assert розкласти(0.1) == (1019, 2702159776422298)
assert розкласти(0.5) == (1022, 0)
for проба in [0.1, 0.5, 2.675, 1.0, 3.75, 1e16, 1e-7]:
    assert зібрати(*розкласти(проба)) == проба
```

### 3.2 · Точне десяткове значення без Decimal

Спираючись на 3.1, напиши функцію `точний_запис(x)`, яка повертає **рядок**
із точним десятковим значенням збереженого числа — усі цифри, без округлення.
Знову без `Decimal`: тільки цілі числа Python (вони ж необмежені!).

Підказка: якщо `x = мантиса × 2^(-k)` при `k > 0`, то помножити чисельник
на `5^k` — і ти отримаєш ціле число, у якого треба лише поставити кому
за `k` знаків від кінця. Бо `2^(-k) = 5^k / 10^k`.

**Зроблено, якщо:**
```python
assert точний_запис(0.1) == "0.1000000000000000055511151231257827021181583404541015625"
assert точний_запис(0.5) == "0.5"
assert точний_запис(2.675) == "2.67499999999999982236431605997495353221893310546875"
```

---

## Підказки

- **Не бійся цілих чисел.** У Рівні 3 всі проміжні значення — це `int`,
  а він у Python необмежений. `5 ** 1074` обчислиться без жодних проблем.
- **`math.frexp(x)`** повертає пару `(m, e)`, де `0.5 <= m < 1` і `x == m * 2**e`.
  Щоб дістати 53-бітну мантису, домнож `m` на `2 ** 53` і візьми `int()`.
  Зсув поля порядку — 1023, а розмір мантиси — 52 біти: обидва числа
  доведеться акуратно узгодити.
- **Перевіряй на простому.** Перш ніж запускати на `0.1`, переконайся, що твій
  розкладач правильно працює на `1.0`, `2.0` і `0.5` — там відповідь легко
  порахувати в голові.
- **У 2.1 стеж за типами.** Якщо десь випадково з'явиться `/` замість `//`,
  усе перетвориться на `float`, і `assert` про точну суму почне падати
  на, здавалося б, правильних даних. Це саме та помилка, заради якої писалась
  ця тема.
