Categories
Uncategorized

Better to be roughly right than precisely wrong: Rounding numbers with Swift

ludicrous trek accuracy

Update (December 19, 2016): The code in this article was updated for Swift 3.0.

The 1960s Star Trek TV series wasn’t written by engineers, scientists, or mathematicians, which is why one of their tricks for showing you that Mr. Spock was an intellectual badass was having make calculations to a ridiculous degree of preciseness, a dramatic device that TV Tropes calls ludicrous precision.

You don’t want your apps to be as annoying as Spock could sometimes be, which is why rounding is an important operation.

Rounding is useful for things like:

  • The limits of your input precision. Suppose you’re calculating the average of the daily high temperatures over the past week, each of which is accurate to the nearest degree. Because of the way an average (or more technically, the mean) is calculated, the result may contain a decimal point. However, the result you report shouldn’t be more precise than the data it’s based on, so you’ll want to round it to the nearest degree.
  • The limits of usefulness. If you’re displaying directions for a short trip, you probably wouldn’t want to report the total distance with more than a 1/4 mile or 1/2 kilometer precision, and you’d probably want to state the trip time rounded to the nearest minute. On the other hand, for a trip covering the width of the United States or Canada, anything more precise than rounding the total distance to the nearest 10 miles and the trip time to the nearest half-hour isn’t generally helpful.
  • Practical considerations. When performing financial calculations, especially when dealing with cash, you’ll want to round your result to the nearest penny. In countries like Canada, where they’ve phased out the penny, you’ll want to round cash calculations to the nearest nickel.

Swift’s built-in rounding functions

Swift comes built-in with most of the rounding functions in C’s math library. The ones you’re most likely to use are round, floor, and ceil:

round(_:) Given a number n, this returns n rounded to the nearest whole number. For example:

  • round(2.0) returns 2.0
  • round(2.2) returns 2.0
  • round(2.5) returns 3.0
  • round(2.9) returns 3.0

With negative numbers, a fractional value of 0.5 or higher rounds the number down, and lower fractional values round the number up:

  • round(-2.0) returns –2.0
  • round(-2.2) returns –2.0
  • round(-2.5) returns –3.0
  • round(-2.9) returns –3.0
floor(_:) Given a number n, this returns n rounded down to the nearest whole number. For example:

  • floor(2.0) returns 2.0
  • floor(2.2) returns 2.0
  • floor(2.5) returns 2.0
  • floor(2.9) returns 2.0
  • floor(-2.0) returns -2.0
  • floor(-2.2) returns -3.0
  • floor(-2.5) returns -3.0
  • floor(-2.9) returns -3.0
ceil(_:) Given a number n, this returns n rounded up to the nearest whole number (you’ve probably guessed that ceil is short for ceiling). For example:

  • ceil(2.0) returns 2.0
  • ceil(2.2) returns 3.0
  • ceil(2.5) returns 3.0
  • ceil(2.9) returns 3.0
  • ceil(-2.0) returns -2.0
  • ceil(-2.2) returns -2.0
  • ceil(-2.5) returns -2.0
  • ceil(-2.9) returns -2.0

round, floor, and ceil each come in two versions:

  • A version that accepts a Double argument and returns a Double result (Doubles are 64-bit numbers and have a precision of at least 15 decimal digits), and
  • a version that accepts a Float argument and returns a Float result (Floats are 32-bit numbers and have a precision of as little as 6 decimal digits).

Rounding n to the nearest multiple of x

rounding coaster

round, floor, and ceil work well when you’re trying to round to nearest whole number, but they’re not as useful if you’re facing one of these rounding scenarios:

  • Rounding a number to the nearest 10 (as demonstrated in the “Rounding Coaster” shown above), 100, 1000, etc.
  • Rounding a number to the nearest .1, .01, .001, etc.
  • Rounding a dollar value to the nearest penny
  • In Canada, which has eliminated the penny (because it costs more than a penny to manufacture one), you calculate change by rounding to the nearest nickel
  • Rounding a number to the nearest multiple of x

For situations like the ones listed above, you’ll find these Swift functions handy:

// Given a value to round and a factor to round to,
// round the value to the nearest multiple of that factor.
func round(_ value: Double, toNearest: Double) -> Double {
  return round(value / toNearest) * toNearest
}

// Given a value to round and a factor to round to,
// round the value DOWN to the largest previous multiple
// of that factor.
func roundDown(_ value: Double, toNearest: Double) -> Double {
  return floor(value / toNearest) * toNearest
}

// Given a value to round and a factor to round to,
// round the value DOWN to the largest previous multiple
// of that factor.
func roundUp(_ value: Double, toNearest: Double) -> Double {
  return ceil(value / toNearest) * toNearest
}

Here’s the round(_:toNearest:) function in action:

round(52.376, toNearest: 0.01) // 52.38
round(52.376, toNearest: 0.1)  // 52.4
round(52.376, toNearest: 0.25) // 52.5
round(52.376, toNearest: 0.5)  // 52.5
round(52.376, toNearest: 1)    // 52
round(52.376, toNearest: 10)   // 50
round(52.376, toNearest: 100)  // 100
round(52.376, toNearest: 1000) // 0

Here’s the roundDown(_:toNearest:) function in action:

roundDown(52.376, toNearest: 0.01) // 52.37
roundDown(52.376, toNearest: 0.1)  // 52.3
roundDown(52.376, toNearest: 0.25) // 52.25
roundDown(52.376, toNearest: 0.5)  // 52
roundDown(52.376, toNearest: 1)    // 52
roundDown(52.376, toNearest: 10)   // 50
roundDown(52.376, toNearest: 100)  // 0
roundDown(52.376, toNearest: 1000) // 0

Here’s the roundUp(_:toNearest:) function in action:

roundUp(52.376, toNearest: 0.01) // 52.38
roundUp(52.376, toNearest: 0.1)  // 52.4
roundUp(52.376, toNearest: 0.25) // 52.5
roundUp(52.376, toNearest: 0.5)  // 52.5
roundUp(52.376, toNearest: 1)    // 53
roundUp(52.376, toNearest: 5)    // 55
roundUp(52.376, toNearest: 10)   // 60
roundUp(52.376, toNearest: 100)  // 100
roundUp(52.376, toNearest: 1000) // 1000

Rounding to a specified number of decimal places

price break

Photo from Consumerist. Click to see the source.

If you want to round a value to a specific number of decimal places, try this function:

// Round the given value to a specified number
// of decimal places
func round(_ value: Double, toDecimalPlaces places: Int) -> Double {
  let divisor = pow(10.0, Double(places))
  return round(value * divisor) / divisor
}

Here’s roundToPlaces(_:decimalPlaces:) in action:

round(52.3761, toDecimalPlaces: 3) // 52.376
round(52.3761, toDecimalPlaces: 2) // 52.38
round(52.3761, toDecimalPlaces: 1) // 52.4
round(52.3761, toDecimalPlaces: 0) // 52.0

// If we put a negative number n into decimalPlaces,
// roundToPlaces rounds to the nearest multiple of 
// 10^abs(n):
round(52.3761, toDecimalPlaces: -1) // 50.0 (rounding to nearest multiple of 10)
round(52.3761, toDecimalPlaces: -2) // 100.0 (rounding to nearest multiple of 100)
round(52.3761, toDecimalPlaces: -3) // 0 (rounding to nearest multiple of 1000))

Removing the fractional part of a number

seamus levine from family guy

“Seamus” from Family Guy is rather truncated.

Sometimes, you just want to lop off anything after the decimal point. That’s what trunc (short for truncate) is for:

trunc(52.376)  // 52.0
trunc(52.576)  // 52.0
trunc(-52.376) // -52.0
trunc(-52.576) // -52.0

Displaying rounded values

There are times when you want to store a precise value but display a rounded one. Use the String class’ init(format:arguments:) method and C’s format strings for printf to display numbers as strings representing rounded values. Here it is in action:

// Remember, these are *string* values!
String(format: "%.3f", 52.3761) // "52.376"
String(format: "%.2f", 52.3761) // "52.38"
String(format: "%.1f", 52.3761) // "52.4"
String(format: "%.0f", 52.3761) // "52"
String(format: "%.0f", 52.5761) // "53"

Happy rounding!

Thanks to John Maynard Keynes for the title of this article!

9 replies on “Better to be roughly right than precisely wrong: Rounding numbers with Swift”

On a related point, how can you create a new type that stores dollars and cents as a floating point value with exactly 2 decimal places? If you do that, all this rounding becomes unnecessary.

I’m writing something to store financial values and it should not be possible to store fractions of a cent, so every value should be x.xx with no trailing figures after the .xx

I want this because for a bank account withdrawal, sometimes you need to check if balance is >= amount being withdrawn. Normal float/double values don’t usually work when checking equality.

@Dave: Trying to use floating-point numbers for financials is the wrong tool for the job.  The reason is right in the name— the decimal point is “floating”.  What you need is a “fixed-point decimal number”.

Fortunately, Foundation provides a class for that— Decimal (or in Obj-C, NSDecimalNumber).  To create a Decimal number, you can use either of these:

let price1 = Decimal(sign: .plus, exponent: -2, significand: 199994) // 1999.94
let price2 = Decimal(string: “1999.94”)! // 1999.94

Compare to:

let price3 = Double(1999.94) // 1999.9400000000001

@Dave: Also, the range of Decimal isn’t infinite, but it should certainly be big enough for any financial numbers you might need to do math on in the forseeable future.  The significand is stored as 128-bits, so given an exponent of -2 (for cents), that gives you a range of -$3.4 Undecillion to +$3.4 Undecillion (-$3,402,823,669,209,384,634,633,746,074,317,682,114.55 to 3,402,823,669,209,384,634,633,746,074,317,682,114.55).

Calculated using:

let min = Decimal(_exponent: -2, _length: 8, _isNegative: 1, _isCompact: 0, _reserved: 0, _mantissa: ( UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max ))
let max = Decimal(_exponent: -2, _length: 8, _isNegative: 0, _isCompact: 0, _reserved: 0, _mantissa: ( UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max, UInt16.max ))

Comments are closed.