JavaRush /Курсы /Swift SELF /Преобразования и округления: rounded(), floor/ceil

Преобразования и округления: rounded(), floor/ceil

Swift SELF
6 уровень , 3 лекция
Открыта

1. Явные преобразования

Если писать программы только про «сложили два целых — получили целое», жизнь действительно проста. Но реальный ввод пользователя быстро ломает эту идиллию: цены, проценты, скидки, средние значения, деление “на всех”. В таких задачах вы почти всегда встречаете смесь Int и Double, а ещё — необходимость «привести результат к виду, который понимает человек». Человек хочет “19.99”, а не “19.990000000000002”.

Представим, что мы делаем небольшой консольный калькулятор заказа. Пользователь вводит цену, скидку и число людей, а программа считает итоговую сумму и «сколько с каждого». Для вычислений удобен Double, а для некоторых частей логики (например, “сколько людей”) нужен Int. Значит, нам понадобятся преобразования типов — и аккуратное округление.

Swift — язык, который предпочитает, чтобы вы явно сказали, что именно хотите сделать. Поэтому выражение вида “целое умножить на дробное” без подготовки часто не компилируется: компилятор не угадывает, хотите ли вы расширить Int до Double, или наоборот сузить Double до Int.

Посмотрим на мини‑пример. Допустим, у нас есть количество людей (Int) и коэффициент скидки (Double):

import Foundation

let people: Int = 3
let discountFactor: Double = 0.9

let result = Double(people) * discountFactor
print(result) // 2.7

Здесь важный момент: мы сами сделали Double(people). Swift не делает это автоматически — и это хорошо, потому что для программы “случайно не потерять данные” обычно важнее, чем “сэкономить 5 символов”.

Тот же принцип работает и для целых типов разной ширины:

import Foundation

let a: Int = 42
let b: Int64 = 42

let sum = Int64(a) + b
print(sum) // 84

Это продолжение идеи из той лекции про целые типы: если типы разные, договоритесь, в каком типе вы считаете.

2. Когда преобразование может не получиться: if let

Когда вы пишете:

let age = Int("25")

всё работает — строка "25" превращается в число 25.

Но если строка не число?

let age = Int("сорок два")

Swift не может превратить "сорок два" в число. Поэтому функция Int(...) не возвращает число напрямую. Она возвращает опциональное значение (Optional<Int>): либо число, либо nil.

Самый простой способ работать с таким результатом — конструкция if let. Она означает: “если значение получилось — положи его в переменную и выполни код внутри if.”

import Foundation

let text = "42"

if let number = Int(text) {		//number имеет тип Int. Не Int?
    print("Число:", number)
} else {
    print("Это не число")
}

Если преобразование удалось, переменная number содержит результат. Если нет — выполняется ветка else.

Внутри if переменная уже имеет обычный тип (Int), поэтому её можно использовать как обычное число.

import Foundation

let text = "10"

if let value = Int(text) {
    print(value * 2)   // 20
}

3. Double → Int: отбрасывание дроби и движение к нулю

Когда вы делаете Int(someDouble), вы можете ожидать «округления». Но Swift не читает ваши мысли (и слава богу). Преобразование Double Int делает не округление, а отбрасывание дробной части — причём не “вниз”, а именно к нулю.

Это важно, потому что на отрицательных числах многие интуитивно путаются.

import Foundation

let a: Double = 19.99
let b: Double = -19.99

print(Int(a)) // 19
print(Int(b)) // -19

Обратите внимание: -19.99 превратилось в -19, а не в -20. Это и есть «движение к нулю».

Если вы делаете финансовые вычисления или расчёты скидок/налогов, такой эффект может внезапно “съесть” единицу, и вы начнёте спорить с калькулятором (обычно калькулятор выигрывает, потому что у него нет бага).

4. rounded() и round(): округление как операция

Чтобы именно округлить, в Swift есть rounded() и round(). Это часть API протокола FloatingPoint, и она специально сделана как отдельная операция: «сначала округли, потом (если надо) переведи в целое». В стандартной библиотеке для этого есть правила округления, описанные через FloatingPointRoundingRule.

Базовый вариант — просто rounded():

import Foundation

let x: Double = 19.49
let y: Double = 19.50
let z: Double = 19.51

print(x.rounded()) // 19.0
print(y.rounded()) // 20.0
print(z.rounded()) // 20.0

Заметьте: результат по‑прежнему Double. Это логично: округление — это про дробное число, но тип остаётся дробным.

Если вам нужен Int, делайте это явно:

import Foundation

let value: Double = 19.99
let roundedInt = Int(value.rounded())

print(roundedInt) // 20

Такой двухшаговый стиль читается честно: «я округляю, потом превращаю в целое».

5. Правила округления: .up, .down, .towardZero

Когда вы начинаете округлять не “до ближайшего”, а по правилам “вверх” или “вниз”, важно понимать, что речь идёт о движении по числовой прямой. Swift даёт несколько правил округления через FloatingPointRoundingRule.

Давайте соберём это в небольшую таблицу — так мозгу проще:

Правило Что делает интуитивно Пример для 2.3 Пример для -2.3
.down
“вниз” (к −∞), аналог floor
2.0
-3.0
.up
“вверх” (к +∞), аналог ceil
3.0
-2.0
.towardZero
к нулю (как Int(...) по смыслу)
2.0
-2.0
.awayFromZero
от нуля
3.0
-3.0

Обратите внимание на отрицательные числа: именно там чаще всего появляются «почему оно округлилось не так?!».

Посмотрим это в коде:

import Foundation

let v: Double = -2.3

print(v.rounded(.down))         // -3.0
print(v.rounded(.up))           // -2.0
print(v.rounded(.towardZero))   // -2.0
print(v.rounded(.awayFromZero)) // -3.0

И вот теперь Int(v) перестаёт быть загадкой: это по смыслу примерно “округление к нулю”, только сразу с изменением типа.

6. floor() и ceil() и связь с rounded(.down/.up)

Иногда в задачах вам прямо хочется сказать: округлить вниз (floor) или округлить вверх (ceil). Например, “сколько целых коробок нужно, если в коробке 12 банок, а я покупаю 25 банок” — это ceil(25/12).

В Swift исторически floor() и ceil() приходят из C‑мира (через Foundation/Darwin/Glibc), но концептуально это то же самое, что rounded(.down) и rounded(.up). Даже в обсуждении дизайна стандартной библиотеки прямо отмечалось, что rounded(.up)/rounded(.down) эквивалентны ceil()/floor().

Давайте сравним:

import Foundation

let x: Double = 2.3

print(floor(x))         // 2.0
print(ceil(x))          // 3.0
print(x.rounded(.down)) // 2.0
print(x.rounded(.up))   // 3.0

Почему тогда вообще нужны floor/ceil, если есть rounded(.down/.up)? В учебном коде часто удобнее показать “классические” названия (особенно если вы переходите из других языков). А в Swift‑стиле часто приятнее держать всё в виде “методов числа”: x.rounded(...). Выбирайте вариант, который делает ваш код более читаемым именно вам и вашей команде.

7. Округление до N знаков: масштабирование

С округлением до целого всё понятно. Но пользователи обычно хотят округление до 2 знаков (деньги), до 1 знака (температура), до 3 знаков (какие‑то измерения). В базовом Swift (без форматтеров и без будущих тем) самый простой и понятный паттерн — масштабирование.

Идея такая: если вам нужно 2 знака после запятой, умножаете на 100, округляете до целого, делите обратно.

import Foundation

let raw: Double = 19.999

let scaled = raw * 100
let roundedScaled = scaled.rounded()
let result = roundedScaled / 100

print(result) // 20.0

Это выглядит чуть многословно, зато прозрачно: вы явно управляете шагами, и любой новичок может отладить это по print.

Если хочется компактнее, можно в одну строку (но не злоупотребляйте “умными” строками, они любят становиться простынёй):

import Foundation

let raw: Double = 19.994
let rounded2 = (raw * 100).rounded() / 100

print(rounded2) // 19.99

Смысл тот же: масштабировали — округлили — вернули масштаб.

8. Где появляются потери: три типичных места

Потери — это не обязательно “ошибка языка”. Чаще это честная цена за то, что мы переводим число из одной модели мира в другую. Проблема начинается, когда мы теряем данные неосознанно.

Потеря дробной части при Double → Int

Это самая очевидная потеря: Int(19.99) становится 19. Тут не «сломалось», вы сами попросили сделать целое число, а целое число не умеет хранить дробь.

import Foundation

let price: Double = 19.99
let whole = Int(price)

print(whole) // 19

Если вам нужно округление, не делайте вид, что Int(...) обязан быть “умным”. Сначала округлите.

Погрешности Double: «почему 0.1 не живёт нормально»

Вещественные числа приближённы, и иногда это проявляется не только в сравнении, но и в округлении “по дороге”. Например, вы можете умножить цену на 100, а там окажется не 1999.0, а 1998.9999999997. Поэтому паттерн с (x * 100).rounded() — не глупость, а практическая защита.

Большие целые и Double: предел около 2^53

Есть более тонкая потеря, о которой полезно знать хотя бы как о факте: Double не может точно представить все большие целые числа. Внутри у Double ограниченная точность мантиссы, и после определённой границы целые начинают «слипаться» (между соседними целыми числами просто нет представимых значений). В обсуждениях стандартной библиотеки эта граница часто всплывает как около 2^53.

Практическое следствие: если вы возьмёте очень большое целое, превратите в Double, а потом обратно в Int, вы можете получить не то же самое значение.

Покажем идею на примере (числа намеренно большие):

import Foundation

let big: Int64 = 9_007_199_254_740_993 // 2^53 + 1 (примерно)
let asDouble = Double(big)
let back = Int64(asDouble)

print(big)  // 9007199254740993
print(back) // может стать 9007199254740992 (эффект потери точности)

В зависимости от платформы/реализации вы можете увидеть разницу прямо в выводе. Важно не то, какое именно число “сломается”, а сам принцип: Double — не контейнер для бесконечно больших целых без потерь. Для большинства бытовых задач (цены, проценты, расстояния) вы не упираетесь в такие значения, но знать про это полезно, чтобы не делать “ID‑шники” через Double.

9. Преобразования без потерь: init?(exactly:)

Иногда вы хотите не просто “преобразовать”, а “преобразовать без потерь — иначе отказаться”. В Swift есть семейство failable‑инициализаторов init?(exactly:), которые возвращают nil, если точное представление невозможно. Это было добавлено именно как более безопасная модель числовых преобразований.

На практике это выглядит так: вы пробуете сделать преобразование, а если нельзя — получаете nil и обрабатываете как обычно (через if let, как мы уже делали с readLine()).

Пример: хотим превратить Double в Int, но только если дробной части нет:

import Foundation

let x: Double = 10.0
let y: Double = 10.5

let a = Int(exactly: x)
let b = Int(exactly: y)

print(a) // Optional(10)
print(b) // nil

И типичный “джуновский, но рабочий” стиль обработки:

import Foundation

let value: Double = 10.5

if let exactInt = Int(exactly: value) {
    print("Exact:", exactInt)
} else {
    print("Not an exact integer")
}
// Not an exact integer

Это отличный приём, когда вы не хотите “случайно потерять дробь” и потом ловить странные баги в логике.

10. Практика: считаем чек и округляем до копеек

Теперь давайте аккуратно соберём небольшой, но интересный сценарий в наш учебный CLI‑скрипт: пользователь вводит сумму заказа и процент чаевых, а мы считаем итог. Затем покажем три варианта округления: до копеек “по ближайшему”, “всегда вверх” и “всегда вниз”. Это полезно, потому что разные системы ведут себя по‑разному: где‑то округляют в пользу клиента, где‑то в пользу магазина, а где‑то “как получится”.

Сделаем ввод максимально простым: одна строка, два числа через пробел. Мы уже умеем split, а Double(...) и ?? у нас тоже есть.

import Foundation

print("Enter sum and tip percent (e.g. 19.99 10):", terminator: " ")

let parts = (readLine() ?? "").split(separator: " ")
let sum = Double(parts.first ?? "0") ?? 0
let tipPercent = Double(parts.dropFirst().first ?? "0") ?? 0

let total = sum + sum * tipPercent / 100
print("Raw total:", total)

Дальше — округление до 2 знаков тремя способами.

Округление “до ближайшего”:

import Foundation

let total: Double = 21.989
let nearest2 = (total * 100).rounded() / 100

print(nearest2) // 21.99

Округление “вверх до копейки” (полезно, если вы хотите гарантировать, что не “недоберёте”):

import Foundation

let total: Double = 21.981
let up2 = (total * 100).rounded(.up) / 100

print(up2) // 21.99

Округление “вниз до копейки” (иногда используют для скидок, чтобы не завысить):

import Foundation

let total: Double = 21.989
let down2 = (total * 100).rounded(.down) / 100

print(down2) // 21.98

Здесь особенно хорошо видно, что .down — это «пол вниз», а не «к нулю»: для положительных чисел это одно и то же, а вот для отрицательных — уже нет.

Деньги в центах: Int вместо Double

Мы не уходим сегодня в специальные денежные типы (это отдельная тема, и у неё есть свои тонкости), но базовую идею полезно увидеть: иногда удобнее хранить деньги как целое число минимальных единиц. Например, $19.99 хранить как 1999 центов (Int).

Тогда округление происходит ровно один раз — в момент перевода из Double в “центы”, и дальше вы работаете с целыми числами.

import Foundation

let total: Double = 21.989
let cents = Int((total * 100).rounded())

print(cents) // 2199

А показать обратно человеку можно так:

import Foundation

let cents: Int = 2199
let dollars = Double(cents) / 100

print(dollars) // 21.99

Это очень приятная модель для задач “суммировать/делить/сравнивать деньги”, потому что в основной логике вы избегаете вещественных ошибок. Но помните: перевод туда‑сюда — это как переезд. Лучше делать его реже, а не каждые две строки.

11. Типичные ошибки

Ошибка №1: ожидать, что Int(3.9) округлит до 4.
Такое ожидание появляется из бытового смысла слова «привести к целому». Но Int(...) не округляет, а отбрасывает дробную часть (к нулю). Если вам нужно округление, сначала делайте rounded(), и только потом — Int(...). Код станет длиннее на несколько символов, зато короче на несколько часов отладки.

Ошибка №2: путать .down с “к нулю”, особенно на отрицательных числах.
На положительных значениях rounded(.down) и “отбросить дробь” дают одинаковый результат, и мозг привыкает. А потом появляется -2.3, и внезапно .down превращает его в -3.0, потому что “вниз” — это к минус бесконечности. Если вы хотите “просто убрать дробь”, мыслите правилом .towardZero или преобразованием Int(...).

Ошибка №3: округлять до двух знаков через floor(x * 100) / 100 и удивляться странностям.
Формула кажется разумной, но из‑за приближённости Double выражение x * 100 может получиться чуть меньше ожидаемого (например, 1998.9999999997). Тогда floor даст на 1 цент меньше. Поэтому практический паттерн — использовать rounded() для “до копейки”, а floor/ceil применять только когда вы действительно хотите “всегда вниз” или “всегда вверх”.

Ошибка №4: надеяться, что Double безопасно хранит любые большие целые числа.
В большинстве учебных примеров всё нормально, и это расслабляет. Но у Double есть ограничение точности, и после больших значений целые числа перестают представляться точно (примерно после области порядка 2^53). Если вы работаете с большими идентификаторами, счётчиками или временем в наносекундах, не превращайте это в Double “просто потому что так удобнее делить”.

Ошибка №5: делать преобразование “как получится”, когда по смыслу нужно “только без потерь”.
Иногда логика требует: «это число должно быть целым». Если вы просто сделаете Int(x), вы молча потеряете дробь и программа продолжит работу в неправильном состоянии. В таких ситуациях лучше использовать Int(exactly: x) и обработать nil. Эта возможность как раз существует для того, чтобы конверсия могла честно провалиться, если точность сохранить нельзя.

1
Задача
Swift SELF, 6 уровень, 3 лекция
Недоступна
Два округления
Два округления
1
Задача
Swift SELF, 6 уровень, 3 лекция
Недоступна
Округление масштаба
Округление масштаба
1
Задача
Swift SELF, 6 уровень, 3 лекция
Недоступна
Коробки и пустоты
Коробки и пустоты
1
Задача
Swift SELF, 6 уровень, 3 лекция
Недоступна
Точное целое
Точное целое
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ