Округление Double до заданного количества знаков после запятой в Swift
Основное решение: использование Decimal для точного округления
Проблема, с которой вы столкнулись, связана с фундаментальной особенностью чисел с плавающей точкой (Double, Float). Они не могут точно представить многие десятичные дроби (например, 0.1 в двоичной системе — это бесконечная дробь). Поэтому умножение на 100, округление и деление обратно может давать неожиданные результаты.
Надёжный способ — использовать тип Decimal из Foundation. Он работает с десятичной арифметикой и позволяет избежать ошибок двоичного представления.
Решение 1: Расширение для Double с использованием Decimal
import Foundation
extension Double {
/// Округляет Double до заданного количества знаков после запятой
/// - Parameter places: количество знаков после запятой (0-15)
/// - Returns: округлённое значение Double
func rounded(toPlaces places: Int) -> Double {
guard places >= 0 else { return self }
let divisor = pow(10.0, Double(places))
let decimalValue = Decimal(self)
let decimalDivisor = Decimal(divisor)
// Округляем с помощью Decimal
var roundedDecimal = decimalValue.multiplying(by: decimalDivisor)
var rounded = Decimal()
NSDecimalRound(&rounded, &roundedDecimal, 0, .plain) // .plain = обычное округление
// Делим обратно
let result = rounded / decimalDivisor
// Конвертируем обратно в Double
return (result as NSDecimalNumber).doubleValue
}
}
// Использование:
let duration: TimeInterval = 3.14159265
let roundedDuration = duration.rounded(toPlaces: 2)
print(roundedDuration) // 3.14
let price: Double = 19.995
let roundedPrice = price.rounded(toPlaces: 2)
print(roundedPrice) // 20.0 (корректно округляет!)
Решение 2: Для отображения в UI — используйте NumberFormatter
Если вам нужно показать число пользователю (в UILabel, TextField и т.д.), лучше использовать NumberFormatter. Он учитывает локаль пользователя (разделители, символы валют) и гарантирует правильное отображение:
import Foundation
func formatDouble(_ value: Double, toPlaces places: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = places
formatter.maximumFractionDigits = places
formatter.roundingMode = .halfUp // или .down, .up и т.д.
return formatter.string(from: NSNumber(value: value)) ?? ""
}
// Использование:
let displayString = formatDouble(19.995, toPlaces: 2)
print(displayString) // "20.00" (всегда два знака после запятой)
Решение 3: Если нужно именно Double и вы знаете про ошибки
Если вы готовы мириться с микро-ошибками Double (они обычно не влияют на расчёты), можно использовать встроенные функции:
extension Double {
func roundedTo(places: Int) -> Double {
let multiplier = pow(10.0, Double(places))
return (self * multiplier).rounded() / multiplier
}
}
// Но будьте осторожны:
print(1.005.roundedTo(places: 2)) // Может выдать 1.0 вместо 1.01 из-за ошибки Double!
Важные замечания:
- Для TimeInterval/NSTimeInterval — это просто синонимы Double, поэтому все решения работают одинаково.
- Округление 0.5: по умолчанию Swift использует "banker's rounding" (round half to even). Если вам нужно "round half up" (как в школе), используйте
NSDecimalRoundс.plain. - Производительность: Decimal работает медленнее Double, но для UI и большинства задач это незаметно.
- Для валютных расчётов всегда используйте Decimal, никогда не используйте Double.
Рекомендую использовать Решение 1 (с Decimal) для внутренних расчётов, а Решение 2 (NumberFormatter) — для отображения. Это покрывает 99% случаев.
<<Альтернативный подход: округление с помощью Darwin и проверка точности
Если вам не нужна абсолютная точность Decimal (например, для графики или анимаций), можно использовать более простое решение с проверкой на "близость к целому":
import Foundation
extension Double {
func rounded(to places: Int) -> Double {
let multiplier = pow(10.0, Double(places))
let value = self * multiplier
// Используем Darwin round для избежания banker's rounding
let roundedValue = round(value)
// Если разница между round и значением меньше epsilon, используем floor/ceil
let epsilon = 1e-10
let result = roundedValue / multiplier
// Проверка на ошибки плавающей точки
if abs(result - self) < epsilon {
return self
}
return result
}
}
// Пример:
let value: Double = 2.675
print(value.rounded(to: 2)) // 2.68 (правильно, в отличие от простого умножения)
Плюсы: быстро, не требует Foundation (кроме Darwin).
Минусы: всё ещё может давать ошибки на граничных значениях (например, 1.005).
Этот метод подходит для случаев, где точность до 10⁻¹⁰ приемлема (например, координаты на карте, научные расчёты с погрешностью). Для финансов используйте только Decimal.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги