Почему округление Double в Swift — это боль (и как с ней жить в 2025)
Каждый iOS-разработчик рано или поздно сталкивается с ситуацией: нужно показать пользователю цену товара 99.99, а в коде — 99.99000000000001. Знакомо? Добро пожаловать в мир округления Double в Swift — темы, которая собрала тысячи просмотров на Stack Overflow и продолжает будоражить умы даже в 2025 году.
Проблема не в Swift — проблема в самой природе чисел с плавающей точкой (IEEE 754). Double не может точно представить 0.1, как и 1/3 в десятичной системе. Но для пользователя это не оправдание. Ему нужны красивые цифры на экране.
В этой статье я покажу 5 проверенных способов округления Double в Swift, разберу их плюсы и минусы, и дам чёткий алгоритм выбора под вашу задачу. Поехали!
Метод 1: Foundation — round() + pow(10, n) — классика жанра
Самый очевидный способ, который приходит в голову: умножить число на 10^n, округлить, разделить обратно.
import Foundation
func roundDouble(_ value: Double, toPlaces places: Int) -> Double {
let divisor = pow(10.0, Double(places))
return round(value * divisor) / divisor
}
let price = 99.995
print(roundDouble(price, toPlaces: 2)) // 100.0 (банковское округление)
Плюсы:
- Простота и читаемость
- Работает «из коробки» — не нужны дополнительные импорты
Минусы:
- Ошибки точности — из-за Double может получиться 99.99000000000001 вместо 99.99
- Проблема с банковским округлением — round() использует round half up, что не всегда подходит для финансов
- pow(10, n) — лишние вычисления
Этот метод подходит для грубого округления в UI, но не для финансовых расчётов.
Метод 2: String(format:) — округление через строку (самый быстрый для UI)
Если ваша конечная цель — показать число на экране, зачем вообще работать с Double? Превратите его в строку сразу.
let value = 123.456789
let roundedString = String(format: "%.2f", value)
print(roundedString) // "123.46"
// Обратно в Double (если нужно)
let roundedDouble = Double(roundedString) ?? 0.0
Плюсы:
- Молниеносная скорость — нет лишних математических операций
- Гарантированно правильное количество знаков на выходе
- Поддержка локализации (десятичный разделитель)
Минусы:
- Результат — строка, для дальнейших расчётов нужно конвертировать обратно (потеря точности)
- Не подходит для банковского округления (round half up по умолчанию)
Идеально для отображения цен, процентов, рейтингов — везде, где точность ±0.01 не критична.
Метод 3: NSDecimalNumber — король финансовой точности
Когда на кону деньги, Double — зло. Используйте NSDecimalNumber — он хранит числа в десятичном формате без ошибок округления.
import Foundation
func roundDecimal(_ value: Double, toPlaces places: Int, roundingMode: NSDecimalNumber.RoundingMode = .plain) -> Double {
let decimal = NSDecimalNumber(value: value)
let handler = NSDecimalNumberHandler(
roundingMode: roundingMode,
scale: Int16(places),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
return decimal.rounding(accordingToBehavior: handler).doubleValue
}
let amount = 99.995
print(roundDecimal(amount, toPlaces: 2, roundingMode: .bankers)) // 99.99 (банковское округление!)
Плюсы:
- Полный контроль над округлением — .plain, .down, .up, .bankers
- Минимум ошибок точности
- Стандарт для финансовых приложений
Минусы:
- Медленнее простого round() — в 10-20 раз
- Громоздкий код
Используйте для расчёта налогов, комиссий, кредитов — там, где каждый цент на счету.
Метод 4: Decimal (Swift-native) — современная альтернатива
Начиная с iOS 10 / Swift 3, у нас есть нативный Decimal — обёртка над NSDecimalNumber, но с более чистым синтаксисом.
import Foundation
func roundDecimalSwift(_ value: Double, toPlaces places: Int) -> Double {
var decimal = Decimal(value)
var rounded = Decimal()
NSDecimalRound(&rounded, &decimal, places, .plain)
return (rounded as NSDecimalNumber).doubleValue
}
let testValue = 123.456789
print(roundDecimalSwift(testValue, toPlaces: 2)) // 123.46
Плюсы:
- Swift-native, нет лишних Objective-C мостов
- Поддерживает все режимы округления
- Лучше для производительности, чем NSDecimalNumber в цикле
Минусы:
- Всё ещё медленнее, чем round() + pow()
- Синтаксис с передачей указателя может напугать новичков
Хороший выбор для middle-ground — когда точность важна, но не на уровне банковских транзакций.
Метод 5: Кастомное расширение Double — удобство и переиспользование
Никто не хочет писать одну и ту же функцию в каждом проекте. Создайте расширение Double — и пользуйтесь.
import Foundation
extension Double {
func rounded(toPlaces places: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded(rule) / divisor
}
}
let value = 99.9999
print(value.rounded(toPlaces: 2)) // 100.0
print(value.rounded(toPlaces: 2, rule: .down)) // 99.99
Для iOS 15+ можно использовать FloatingPointRoundingRule — это даёт гибкость без NSDecimalNumber.
Плюсы:
- Чистый синтаксис — value.rounded(toPlaces: 2)
- Легко добавлять в любой проект через SPM или копипасту
Минусы: