Avatar ·

Точное форматирование чисел с плавающей точкой в Swift: спецификаторы строкового формата

Всем привет! Я только начал изучать Swift и столкнулся с проблемой при форматировании чисел с плавающей точкой. Мне нужно вывести значение типа Double с определённым количеством знаков после запятой, например, 2 знака. В других языках, таких как C или Java, я использовал спецификаторы формата вроде %.2f, но в Swift это, похоже, не работает напрямую.

Вот что я пробовал:

let pi = 3.1415926535
let formatted = String(format: "%.2f", pi)
print(formatted) // "3.14" — работает!

Но когда я попробовал использовать String(format:) с другими спецификаторами, например, %e для научной записи или %g для автоматического выбора формата, я получил странные результаты. Также мне нужно выровнять числа по ширине поля, добавив пробелы или нули слева. Например, чтобы число 42.1 стало "042.10".

Я читал про NumberFormatter, но он кажется слишком громоздким для простых задач. Есть ли в Swift лаконичный способ точного форматирования чисел, как в C-стиле? Или нужно обязательно использовать NumberFormatter? Буду благодарен за примеры с пояснениями.

<<>>

Использование String(format:) и NumberFormatter

Вы на правильном пути! String(format:) в Swift действительно поддерживает C-стиль форматирования, но с некоторыми особенностями. Давайте разберём основные подходы.

1. Базовое форматирование через String(format:)

Этот метод работает, но важно помнить, что он использует NSString под капотом. Вот примеры:

let value = 123.456789

// Два знака после запятой
String(format: "%.2f", value)   // "123.46"

// Шесть знаков после запятой
String(format: "%.6f", value)   // "123.456789"

// Научная нотация с двумя знаками
String(format: "%.2e", value)   // "1.23e+02"

// Автоматический выбор (короткая запись)
String(format: "%.3g", value)   // "123"

// Выравнивание по ширине 10 символов
String(format: "%10.2f", value) // "    123.46"

// Дополнение нулями слева (только для целых чисел)
String(format: "%010.2f", value) // "0000123.46" — неожиданно? Да, это особенность.

Важно: %010.2f добавляет нули слева от целой части, а не от запятой. Для полного контроля используйте NumberFormatter.

2. Профессиональный подход: NumberFormatter

Несмотря на кажущуюся громоздкость, NumberFormatter даёт максимальную гибкость и локализацию:

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
formatter.minimumIntegerDigits = 3 // минимум 3 цифры в целой части

let number = 42.1
let formatted = formatter.string(from: NSNumber(value: number))!
// "042.10" — именно то, что вам нужно!

Дополнительные опции:

  • formatter.paddingPosition = .beforePrefix — добавить символы слева
  • formatter.paddingCharacter = "0" — символ для заполнения
  • formatter.formatWidth = 10 — общая ширина поля

3. Современный Swift: протокол FormatStyle (iOS 15+ / macOS 12+)

Начиная с iOS 15, появился новый декларативный синтаксис:

let value = 123.456789

// Два знака после запятой
value.formatted(.number.precision(.fractionLength(2))) // "123.46"

// С выравниванием
value.formatted(.number.precision(.fractionLength(2))
    .grouping(.never)
    .sign(strategy: .never)) // "123.46"

// Для научной нотации
value.formatted(.number.notation(.scientific)
    .precision(.fractionLength(2))) // "1.23E2"

Этот подход наиболее лаконичен и типобезопасен, но требует современной версии ОС.

4. Ручное форматирование (для экстремальных случаев)

Если вам нужно что-то очень специфичное, можно использовать математику:

func formatDouble(_ value: Double, decimals: Int) -> String {
    let multiplier = pow(10.0, Double(decimals))
    let rounded = (value * multiplier).rounded() / multiplier
    return String(format: "%.\(decimals)f", rounded)
}

Итог: Для простых задач используйте String(format:), для локализации и сложного форматирования — NumberFormatter, для современного кода — FormatStyle. Все три подхода корректны, выбирайте по контексту.

<<>>

Альтернативный подход: расширение для Double

Если вам не нравится многословность NumberFormatter, а String(format:) кажется недостаточно гибким, можно создать удобное расширение:

extension Double {
    func formatted(decimals: Int = 2, 
                   minIntegers: Int = 1, 
                   padding: String = " ") -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.minimumFractionDigits = decimals
        formatter.maximumFractionDigits = decimals
        formatter.minimumIntegerDigits = minIntegers
        formatter.paddingPosition = .beforePrefix
        formatter.paddingCharacter = padding
        return formatter.string(from: NSNumber(value: self)) ?? "\(self)"
    }
}

// Использование:
let pi = 3.14159
print(pi.formatted())                    // "3.14"
print(pi.formatted(decimals: 4))         // "3.1416"
print(pi.formatted(minIntegers: 3))      // "003.14"
print(pi.formatted(padding: "0"))        // "03.14" (с одним нулём)

Этот подход объединяет лаконичность вызова с мощью NumberFormatter. Вы можете легко добавить параметры для разделителя тысяч, знака числа и других опций.

Совет: Если вы работаете с валютой или процентами, используйте formatter.numberStyle = .currency или .percent — это автоматически учтёт локаль пользователя.