docBen
docBen

Reputation: 39

Displaying float to a variable number of decimal places in Swift

Is there a simple way of displaying a float or double to a relevant number of decimal places in Swift.

For example, an iOS app using SI units, which can be altered depending on the property desired, and converted through up to 6 orders of magnitude depending on desired inputs and output. Therefore it needs to display not only 1mg to 1000 micrograms, but also the other way around - i.e 1 microgram = 0.001 mg.

I can easily format a string as follows:

textFieldFoo.text = NSString(format: "%.1f mg", bar) as String

However, if the user were to convert from 1mcg to 0.001mg, this would display as 0.0 mg

Yet, to include up to 6 decimal places to encompass all common possibilities would lead to an unwieldy, ugly looking UI.

Is there a simple way to format a string, in order to include a float/ double where it is displayed to a relevant number of decimal places/ significant figures? I'm sure, given time and enough boilerplate code, that I could pyramid if/ else it to get a result, but that's frankly inelegant.

Upvotes: 1

Views: 1691

Answers (3)

AnderCover
AnderCover

Reputation: 2661

If I understand you correctly you are:

  1. using Swift
  2. working with SI units
  3. trying to display floating points
  4. trying to avoid boilerplate and possibly magic numbers

You should definitely use Apple's Measurement which is :

A numeric quantity labeled with a unit of measure, with support for unit conversion and unit-aware calculations.

and MeasurementFormatter which is :

A formatter that provides localized representations of units and measurements.

MeasurementFormatter uses a NumberFormatter to format the quantity of a measurement.
NumberFormatters's usesSignificantDigits property is set to false by default but :

Set this property to true to format numbers according to the significant digits configuration specified by the minimumSignificantDigits and maximumSignificantDigits properties. By default, the minimum number of significant digits is 1, and the maximum number of significant digits is 6.

Here's an example of what you can do with masses

let micrograms = Measurement(value: 1, unit: UnitMass.micrograms) // 1.0 µg
let nanograms = micrograms.converted(to: .nanograms) // 1000.0000000000001 ng
let picograms = micrograms.converted(to: .picograms) // 1000000.0 pg
let milligrams = micrograms.converted(to: .milligrams) // 0.001 mg
let centigrams = micrograms.converted(to: .centigrams) // 0.0001 cg
let decigrams = micrograms.converted(to: .decigrams) // 1e-05 dg
let grams = micrograms.converted(to: .grams) // 1e-06 g
let kilograms = micrograms.converted(to: .kilograms) // 1e-09 kg
let ounces = micrograms.converted(to: .ounces) // 3.527399072294044e-08 oz
let pounds = micrograms.converted(to: .pounds) // 2.2046244201837776e-09 lb
let stones = micrograms.converted(to: .stones) // 1.574731232746851e-10 st

let formatter = MeasurementFormatter()
formatter.numberFormatter.usesSignificantDigits = true
formatter.unitOptions = .providedUnit

formatter.string(from: nanograms) // "1 000 ng"
formatter.string(from: picograms) // "1 000 000 pg"
formatter.string(from: micrograms) // "1 µg"
formatter.string(from: milligrams) // "0,001 mg"
formatter.string(from: centigrams) // "0,0001 cg"
formatter.string(from: decigrams) // "0,00001 dg"
formatter.string(from: grams) // "0,000001 g"
formatter.string(from: kilograms) // "0,000000001 kg"
formatter.string(from: ounces) // "0,000000035274 oz"
formatter.string(from: pounds) // "0,00000000220462 lb"
formatter.string(from: stones) // "0,000000000157473 st"

Upvotes: 0

joshB
joshB

Reputation: 1

Formatting to Significant Figures using Swift

What you want is the ability to format to a fixed number of significant figures, rather than a fixed number of decimal places. A good swift option to solve this is using class extensions, with a little maths to decide how many decimal places to show based on the magnitude of the number.

The example below extends the Double class to enable formatting to a fixed number of significant figures and uses either float notation or scientific notation depending on the magnitude of the number.

import Foundation

//extension to format a Double to a fixed number of significant figures
extension Double {
    func sigFigs(_ numberOfSignificantFigures: Int) -> String {

        let mag = log10(abs(self))
        let intMag = Int(mag)

        if mag >= 0 {
            if intMag < numberOfSignificantFigures {
                return String(format: "%.\(numberOfSignificantFigures - intMag - 1)f",self)
            }
            else {
                return String(format: "%.\(numberOfSignificantFigures - 1)e",self)
            }
        }
        else {
            if -intMag < numberOfSignificantFigures {
                return String(format: "%.\(numberOfSignificantFigures)f",self)
            }
            else {
                return String(format: "%.\(numberOfSignificantFigures - 1)e",self)
            }
        }
    }
}

Usage

let num1 = 1234.5678
let num2 = 12.345678
let num3 = 0.0012345678
let num4 = 1234567.8

print(num1.sigFigs(6))
print(num1.sigFigs(2))
print(num2.sigFigs(6))
print(num2.sigFigs(2))
print(num3.sigFigs(6))
print(num3.sigFigs(2))
print(num4.sigFigs(6))
print(num4.sigFigs(2))

Output

1234.57
1.2e+03
12.3457
12
0.001235
1.2e-03
1.23457e+06
1.2e+06

Upvotes: 0

Code Different
Code Different

Reputation: 93161

There's NSMAssFormatter but it doesn't go all the way down to microgram. It was designed to format human-level weight.

You can roll your own by subclassing NSNumberFormatter:

enum MassUnit: Double {
    case Microgram = 1e-6
    case Milligram = 1e-3
    case Gram = 1
    case Kilogram = 1e3

    static let allUnits: [MassUnit] = [.Microgram, .Milligram, .Gram, .Kilogram]

    var unitAbbreviation: String {
        get {
            switch self {
            case .Microgram: return "mcg"
            case .Milligram: return "mg"
            case .Gram: return "g"
            case .Kilogram: return "kg"
            }

        }
    }
}

class MyMassFormatter: NSNumberFormatter {
    func bestFitStringForWeightInGrams(weight: Double) -> String {
        var selectedString = self.stringFromNumber(weight)!
        var selectedUnit = MassUnit.Gram

        // Pick the unit that results in the shortest string
        for unit in MassUnit.allUnits {
            if let str = self.stringFromNumber(weight / unit.rawValue)
                where str.characters.count < selectedString.characters.count {
                    selectedString = str
                    selectedUnit = unit
            }
        }

        return selectedString + selectedUnit.unitAbbreviation
    }
}

Usage:

let formatter = MyMassFormatter()
formatter.format = "0.######"

print(formatter.bestFitStringForWeightInGrams(0.000001))   // 1mcg
print(formatter.bestFitStringForWeightInGrams(0.005))      // 5mg
print(formatter.bestFitStringForWeightInGrams(2500))       // 2.5kg
print(formatter.bestFitStringForWeightInGrams(1234.5))     // 1234.5g

Upvotes: 1

Related Questions