Avatar ·

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

Всем привет! Я только начал изучать Swift и столкнулся с проблемой, которая кажется простой, но я не могу найти элегантное решение. У меня есть переменная типа `Double`, которая хранит значение времени в секундах, например, `let elapsedTime: Double = 3.14159265`. Мне нужно вывести это значение пользователю в формате с двумя знаками после запятой, чтобы было видно «3.14 секунды». Но при этом само число должно оставаться числом, а не строкой, потому что я буду использовать его в дальнейших вычислениях. Я пробовал использовать `String(format:)` для отображения, но это даёт строку, а не `Double`. Например: ```swift let value = 3.14159265 let formatted = String(format: "%.2f", value) // "3.14" — это String, не Double ``` Когда я пытаюсь снова преобразовать эту строку в `Double`, то получаю `Optional(3.14)`, но это не совсем то же самое, что исходное число. Кроме того, у меня возникают проблемы с точностью для больших чисел или при использовании `NSTimeInterval`. Есть ли встроенный способ в Swift округлить `Double` до определённого количества знаков после запятой, сохранив его тип как `Double`? Или нужно писать свою функцию? Буду благодарен за примеры кода и объяснение, как это работает под капотом. Желательно без использования `NSDecimalNumber`, если есть более простой путь. Заранее спасибо! <<>>

Использование Foundation: round() с множителем

Самый простой и надёжный способ округления Double до заданного количества знаков после запятой — это умножить число на 10^n, применить стандартную функцию round() из Foundation, а затем разделить обратно. Такой подход сохраняет тип Double и не требует дополнительных библиотек.

import Foundation

func roundDouble(_ value: Double, toDecimalPlaces places: Int) -> Double {
    let multiplier = pow(10.0, Double(places))
    return round(value * multiplier) / multiplier
}

// Примеры использования:
let elapsedTime: Double = 3.14159265
let rounded = roundDouble(elapsedTime, toDecimalPlaces: 2)
print(rounded) // 3.14
print(type(of: rounded)) // Double

let largeNumber: Double = 12345.6789
let roundedLarge = roundDouble(largeNumber, toDecimalPlaces: 1)
print(roundedLarge) // 12345.7

Как это работает?

Функция pow(10.0, Double(places)) создаёт множитель: для 2 знаков это 100.0, для 3 — 1000.0 и т.д. Умножение сдвигает десятичную точку вправо, round() округляет до ближайшего целого, а деление возвращает точку на место. Результат — Double с нужной точностью.

Важные нюансы

  • Плавающая точка: Из-за двоичного представления чисел с плавающей точкой результат может иметь микроскопические погрешности (например, 3.14 может храниться как 3.1400000000000001). Для отображения это не критично, но для строгих сравнений используйте Double.ulpOfOne или Decimal.
  • Отрицательные числа: Функция работает корректно с отрицательными значениями, так как round() округляет по стандартным правилам (0.5 вверх).
  • Производительность: pow() — это O(1) операция, поэтому функция подходит для массовых вычислений.

Расширение (Extension) для удобства

Вы можете добавить это поведение прямо к типу Double для более лаконичного синтаксиса:

extension Double {
    func rounded(toDecimalPlaces places: Int) -> Double {
        let multiplier = pow(10.0, Double(places))
        return (self * multiplier).rounded() / multiplier
    }
}

// Использование:
let price = 19.995
print(price.rounded(toDecimalPlaces: 2)) // 20.0 (округление вверх)

Альтернатива: NSDecimalNumber для финансовых расчётов

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

let decimalValue = NSDecimalNumber(value: 3.14159265)
let behavior = NSDecimalNumberHandler(
    roundingMode: .plain,
    scale: 2,
    raiseOnExactness: false,
    raiseOnOverflow: false,
    raiseOnUnderflow: false,
    raiseOnDivideByZero: false
)
let roundedDecimal = decimalValue.rounding(accordingToBehavior: behavior)
print(roundedDecimal.doubleValue) // 3.14

Этот метод гарантирует десятичную точность, но требует дополнительного кода и работает медленнее.

В итоге для большинства задач (отображение данных, вычисления с невысокой точностью) метод с round() и множителем — лучший выбор. Он прост, встроен в Foundation и не создаёт лишних преобразований.

<<>>

Использование NumberFormatter для локализованного вывода

Если ваша цель — именно отображение числа пользователю с учётом локали (десятичный разделитель, формат тысяч), то лучше использовать NumberFormatter. Он не изменяет само число, но даёт строку, готовую для UI.

import Foundation

let value = 1234567.89123
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2

if let formattedString = formatter.string(from: NSNumber(value: value)) {
    print(formattedString) // "1,234,567.89" (для en_US) или "1 234 567,89" (для ru_RU)
}

Преимущества:

  • Автоматически учитывает региональные настройки устройства.
  • Поддерживает группировку разрядов (тысячи, миллионы).
  • Не изменяет исходное число.

Недостатки: Возвращает строку, а не Double. Если вам нужно число для дальнейших вычислений, используйте первый ответ (с round()).

Для простого округления без локализации можно использовать String(format:), но помните, что это тоже строка:

let roundedString = String(format: "%.2f", value) // "1234567.89"

Выбирайте подход в зависимости от задачи: для математики — первый способ, для интерфейса — NumberFormatter.