Avatar ·

Округление Double до заданного количества знаков после запятой в Swift

Привет! Я новичок в Swift и столкнулся с проблемой при работе с числами типа Double. Мне нужно выводить значения времени (NSTimeInterval) с определенной точностью, например, до двух знаков после запятой. Но Swift, похоже, не дает простого способа это сделать. Вот пример моей проблемы: ```swift let duration: TimeInterval = 3.14159265 print(duration) // Выводит 3.14159265, а мне нужно 3.14 let price: Double = 19.995 // Нужно округлить до 20.00 для отображения в UI ``` Я пробовал использовать `String(format:)`: ```swift let formatted = String(format: "%.2f", duration) print(formatted) // Выводит "3.14", но это строка, а мне нужно Double! ``` Проблема в том, что мне нужно получить именно число Double с округлением, а не строку. Например, для дальнейших математических операций или передачи в API. Я слышал про `round()`, `floor()` и `ceil()`, но они работают только с целыми числами. Пробовал умножать на 100, округлять и делить обратно: ```swift let rounded = round(duration * 100) / 100 print(rounded) // Выводит 3.14, но иногда бывают ошибки с плавающей точкой ``` Иногда результат получается 3.1400000000000001 или 3.1399999999999999 из-за особенностей Double. Как правильно округлить Double до заданного количества знаков в Swift? Есть ли надежный способ, который работает и для TimeInterval (NSTimeInterval)? Буду благодарен за примеры кода и объяснения. Использую Swift 5 и Xcode 14. <<>>

Основное решение: использование 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.