Very new to Swift concepts and I'm having trouble conceptualizing how to convert a long decimal value type var distance: String? into a shorter one. This code is crashing due to a:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
let distance = Int(item.distance!) // long decimal value
let x = Float((distance)!)
let y = Double(round(1000*x)/1000)
print(y)
A couple of observations:
You should not only excise the Int from your code snippet, but the Float, too. If your numbers can be large, Float can impose undesirable limitations in the precision of your calculation. So, you would likely want to remove both Int and Float like so:
guard let string = item.distance, let value = Double(string) else {
return
}
let result: Double = (value * 1000).rounded() / 1000
If you’re doing this rounding just so you can show it to 3 decimal places in your UI, you probably wouldn’t round the value at all, but rather just round the output using a NumberFormatter, e.g.:
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 3
formatter.maximumFractionDigits = 3
guard let string = item.distance, let value = Double(string) else {
return
}
let result: String = formatter.string(for: value)
We do this when showing fractional values in our UI because:
The resulting string will be “localized”. This is important because not all locales use . for the decimal point (e.g., in many countries, the value ½ is displayed as 0,5, not 0.5). We always want to show numbers in our UI in the manner preferred by the user. The NumberFormatter does all of this localization of string representations of numbers for us.
If the value had trailing zeros, the above formatter will generate all of those decimal places (e.g. 0.5 will be shown as 0.500), which is often preferable when dealing with decimal places in your UI. (And even if if you don’t want those trailing zeros, you’d still use the NumberFormatter and just set minimumFractionDigits to whatever is appropriate for your app.)
If you really need to round the number to three decimal places (which is unlikely in this case, but we encounter this in financial apps), you shouldn’t use Double at all, but rather Decimal. Again, I’m guessing this is unlikely in your scenario, but I mention it for the sake of completeness.
First of all, This will help you find the issue:
if let distance = item.distance {
if let distanceInt = Int(distance) {
let x = Float(distanceInt)
let y = Double(round(1000*x)/1000)
print(y)
} else {
print("Distance (\(distance)) is not convertible to Int. It has a value, but this value is not representing an integer number.")
}
} else {
print("distance is nil. It should be some number but it is not set yet")
}
Here you can see this string: "0.45991480288961" can not be converted to an Int. So you need to convert it directly to a Double:
if let distance = item.distance {
if let distanceDouble = Double(distance) {
let x = Float(distanceDouble)
let y = Double(round(1000*x)/1000)
print(y)
} else {
print("Distance (\(distance)) is not convertible to Double. It has a value, but this value is not representing a double number.")
}
} else {
print("distance is nil. It should be some number but it is not set yet")
}
Related
I'm trying to properly reduce scale, formatting a float value and returning it as a String in Swift.
For example:
let value: Float = 4.8962965
// formattedFalue should be 4.90 or 4,90 based on localization
let formattedValue = value.formatNumber()
Here is what I did:
extension Float {
func reduceScale(to places: Int) -> Float {
let multiplier = pow(10, Float(places))
let newDecimal = multiplier * self // move the decimal right
let truncated = Float(Int(newDecimal)) // drop the fraction
let originalDecimal = truncated / multiplier // move the decimal back return originalDecimal
}
func formatNumber() -> String {
let num = abs(self)
let numberFormatter = NumberFormatter()
numberFormatter.usesGroupingSeparator = true
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 2
numberFormatter.roundingMode = .up
numberFormatter.numberStyle = .decimal
numberFormatter.locale = // we take it from app settings
let formatted = num.reduceScale(to: 2)
let returningString = numberFormatter.string(from: NSNumber(value: formatted))!
return "\(returningString)"
}
}
But when I use this code I get 4.89 (or 4,89 depending on the localization) instead of 4.90 (or 4,90) as I expect.
Thanks in advance.
You get 4.89 because reduceScale(to:) turns the number into 4.89 (actually, probably 4.89000something because 4.89 cannot be expressed exactly as a binary floating point). When the number formatter truncates this to two decimal places, it naturally rounds it down.
In fact, you don't need reduceScale(to:) at all because the rounding function of the number formatter will do it for you.
Also the final string interpolation is unnecessary because the result of NumberFormatter.string(from:) is automatically bridged to a String?
Also (see comments below by Dávid Pásztor and Sulthan) you can use string(for:) to obviate the NSNumber conversion.
This is what you need
import Foundation
extension Float {
func formatNumber() -> String {
let num = abs(self)
let numberFormatter = NumberFormatter()
numberFormatter.usesGroupingSeparator = true
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 2
numberFormatter.roundingMode = .up
numberFormatter.numberStyle = .decimal
numberFormatter.locale = whatever
return numberFormatter.string(for: num)!
}
}
let value: Float = 4.8962965
// formattedFalue should be 4.90 or 4,90 based on localization
let formattedValue = value.formatNumber() // "4.9"
Solved by following Sulthan's comments:
remove that reduceScale method which is not necessary and it will probably work as expected. You are truncating the decimal to 4.89 which cannot be rounded any more (it is already rounded). – Sulthan 6 hours ago
That's because you have specified minimumFractionDigits = 0. If you always want to display two decimal digits, you will have to set minimumFractionDigits = 2. – Sulthan 5 hours ago
Foundation has a better API now.
In-place, you can use .number:
value.formatted(.number
.precision(.fractionLength(2))
.locale(locale)
)
But it's only available on specific types. For an extension for more than one floating-point type, you'll need to use the equivalent initializer instead:
extension BinaryFloatingPoint {
var formattedTo2Places: String {
formatted(FloatingPointFormatStyle()
.precision(.fractionLength(2))
.locale(locale)
)
}
}
let locale = Locale(identifier: "it")
(4.8962965 as Float).formattedTo2Places // 4,90
I have an precision issue when dealing with currency input using Decimal type. The issue is with the formatter. This is the minimum reproducible code in playground:
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.isLenient = true
formatter.maximumFractionDigits = 2
formatter.generatesDecimalNumbers = true
let text = "89806.9"
let decimal = formatter.number(from: text)?.decimalValue ?? .zero
let string = "\(decimal)"
print(string)
It prints out 89806.89999999999 instead of 89806.9. However, most other numbers are fine (e.g. 8980.9). So I don't think this is a Double vs Decimal problem.
Edit:
The reason I need to use the formatter is that sometimes I need to deal with currency format input:
let text = "$89,806.9"
let decimal = formatter.number(from: text)?.decimalValue ?? .zero
print("\(decimal)") // prints 89806.89999999999
let text2 = "$89,806.9"
let decimal2 = Decimal(string: text2)
print("\(decimal2)") // prints nil
Using the new FormatStyle seems to generate the correct result
let format = Decimal.FormatStyle
.number
.precision(.fractionLength(0...2))
let text = "89806.9"
let value = try! format.parseStrategy.parse(text)
Below is an example parsing a currency using the currency code from the locale
let currencyFormat = Decimal.FormatStyle.Currency
.currency(code: Locale.current.currencyCode!)
.precision(.fractionLength(0...2))
let amount = try! currencyFormat.parseStrategy.parse(text)
Swedish example:
let text = "89806,9 kr"
print(amount)
89806.9
Another option is to use the new init for Decimal that takes a String and a FormatStyle.Currency (or a Number or Percent)
let amount = try Decimal(text, format: currencyFormat)
and to format this value we can use formatted(_:) on Decimal
print(amount.formatted(currencyFormat))
Output (still Swedish):
89 806,9 kr
I agree that this is a surprising bug, and I would open an Apple Feedback about it, but I would also highly recommend switching to Decimal(string:locale:) rather than a formatter, which will achieve your goal (except perhaps the isLenient part).
let x = Decimal(string: text)!
print("\(x)") // 89806.9
If you want to fix fraction digits, you can apply rounding pretty easily with * 100 / 100 conversions through Int. (I'll explain if it's not obvious how to do this; it works for Decimal, though not Double.)
Following Joakim Danielson Answer see this amazing documentation on the format style
Decimal(10.01).formatted(.number.precision(.fractionLength(1))) // 10.0 Decimal(10.01).formatted(.number.precision(.fractionLength(2))) // 10.01 Decimal(10.01).formatted(.number.precision(.fractionLength(3))) // 10.010
Amazingly detailed documentation
If this is strictly a rendering issue and you're just looking to translate a currency value from raw string to formatted string then just do that.
let formatter = NumberFormatter()
formatter.numberStyle = .currency
let raw = "89806.9"
if let double = Double(raw),
let currency = formatter.string(from: NSNumber(value: double)) {
print(currency) // $89,806.90
}
If there is math involved then before you get to the use of string formatters, I would point you to
Why not use Double or Float to represent currency? and
How to round a double to an int using Banker's Rounding in C as great starting points.
I get my response with double value and remove formatter.generatesDecimalNumbers line to get work.
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.isLenient = true
formatter.maximumFractionDigits = 2
//formatter.generatesDecimalNumbers = true // I removed this line
let text = "$89806.9"
let double = formatter.number(from: text)?.doubleValue ?? .zero // converting as double or float
let string = "\(double)"
print(string) // 89806.9
let anotherText = "$0.1"
let anotherDouble = formatter.number(from: anotherText)?.doubleValue ?? .zero // converting as double or float
let anotherString = "\(anotherDouble)"
print(anotherString) // 0.1
I have been at this for hours and can't work it out, please help. I am doing some coding practice to help sharpen up my skills in swift and this one seems so easy but I can't work it out.
I need to create a simple function that returns (the challenge i'm doing asks for this I haven't made it up) the sum of numbers as a string, but if the string contains characters, not numbers, it should return -1. It says : Receive two values of type string. Add them together. If an input is a character, return -1
This is where I am up to but i can't get it pass the tests for returning -1. It passes 3 / 5 tests where it's fine with numbers, but not with the characters. My thinking is that the character set line should check for if myNewString contains any of those characters it should return -1
func addStrNums(_ num1: String, _ num2: String) -> String {
// write your code here
var op1 = num1
var op2 = num2
var total: Int = 0
var myNewInt = Int(op1) ?? 0
var myNewInt2 = Int(op2) ?? 0
total = myNewInt + myNewInt2
var myNewString = String (total)
let characterset = CharacterSet(charactersIn:
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#£$%^&*()_-=+;/?><"
)
if myNewString.rangeOfCharacter(from: characterset) != nil {
return "-1"
}
return myNewString
}
Results of above is :
Test Passed: 10 == 10
FAILED: Expected: -1, instead got: 5
Test Passed: 1 == 1
Test Passed: 3 == 3
import foundation
func addStringNumber(_ num1: String, num2: String) -> String {
if !CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: num1)) {
return "-1"
}
if !CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: num2)) {
return ""
}
let sum = String.init(format: "%d", Int((num1 as NSString).intValue + (num2 as NSString).intValue))
return sum
}
Note:- for this import foundation is important.
A couple of thoughts:
You are using nil coalescing operator (??), which effectively says that you want to ignore errors parsing the integers. I would suggest that you want to detect those errors. E.g.
func addStrNums(_ num1: String, _ num2: String) -> String {
guard
let value1 = Int(num1),
let value2 = Int(num2)
else {
return "-1"
}
let total = value1 + value2
return "\(total)"
}
The initializer for Int will automatically fail if there are non-numeric characters.
These programming tests generally don’t worry about localization concerns, but in a real app, we would almost never use this Int string-to-integer conversion (because we want to honor the localization settings of the user’s device). For this reason, we would generally prefer NumberFormatter in real apps for users with different locales:
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
func addStrNums(_ num1: String, _ num2: String) -> String {
guard
let value1 = formatter.number(from: num1)?.intValue, // use `doubleValue` if you want to handle floating point numbers, too
let value2 = formatter.number(from: num2)?.intValue
else {
return "-1"
}
let total = value1 + value2
return formatter.string(for: total) ?? "-1"
}
This accepts input that includes thousands separators and formats the output accordingly. (Obviously, feel free to use whatever numberStyle you want.)
Also, be aware that NumberFormatter is more tolerant of whitespace before or after the digits. It also allows for a wider array of numeric characters, e.g. it recognizes “5”, a digit entered with Japanese keyboard, which is different than the standard ASCII “5”. This greater flexibility is important in a real app, but I don’t know what your programming exercise is requiring.
While the above demonstrates that you don’t have to check for non-numeric digits manually, you can if you need to. But you need to check the two input strings, not the string representation of the total (especially if you used nil coalescing operator to disregard errors when converting the strings to integers, as I discussed in point 1 above).
If you do this, though, I would not advise trying list all of the non-numeric characters yourself. Instead, use the inverted set:
let characterSet = CharacterSet(charactersIn: "0123456789").inverted // check for 0-9 if using `Int` To convert, use `CharacterSet.decimalDigits` if converting with `NumberFormatter`
guard
num1.rangeOfCharacter(from: characterSet) == nil,
num2.rangeOfCharacter(from: characterSet) == nil
else {
return "-1"
}
Or, one could use regex to confirm that there are only one or more digits (\d+) between the start of the string (^) and the end of the string ($):
guard
num1.range(of: #"^\d+$"#, options: .regularExpression) != nil, // or `#"^-?\d+$"#` if you want to accept negative values, too
num2.range(of: #"^\d+$"#, options: .regularExpression) != nil
else {
return "-1"
}
FWIW, if you’re not familiar with it, the #"..."# syntax employs “extended string delimiters” (saving us from having to to escape the \ characters within the string).
As an aside, you mentioned a command line app that should return -1. Generally when we talk about command line apps returning values, we’re exiting with a numeric value not a string. I would be inclined to make this function return an Int?, i.e. either the numeric sum or nil on failure. But without seeing the particulars on your coding test, it is hard to be more specific.
I am puzzled. I need to compare product date codes. they look like 12-34-56. I wrote some code to break the parts up and compare them. this code works fin in the play ground. But when i make it a function in a view controller values come up NIL and i get a lot of "Optional("12-34-56")" values when printed to the log or viewed in a break. I tried unwrapping in many locations but nothing takes.? don't be confused by the variables date and month because they are not product codes can have 90 days and 90 months depending on the production machine used.
func compaireSerial(oldNumIn: NSString, newNumIn: String) -> Bool {
// take the parts of the number and compare the pics on at a time.
// Set up the old Num in chunks
let oldNum = NSString(string: oldNumIn)
let oldMonth = Int(oldNum.substringToIndex(2))
let oldDay = Int(oldNum.substringWithRange(NSRange(location: 3, length: 2)))
let oldYear = Int(oldNum.substringFromIndex(6))
print(oldMonth,oldDay, oldYear)
// Set up the new Num in chunks
let newNum = NSString(string: newNumIn)
let newMonth = Int(newNum.substringToIndex(2))
let newDay = Int(newNum.substringWithRange(NSRange(location: 3, length: 2)))
let newYear = Int(newNum.substringFromIndex(6))
print(newMonth, newDay, newYear)
// LETS Do the IF comparison steps.
if oldYear < newYear {
return true
} else if oldMonth < newMonth {
return true
} else if oldDay < newDay {
return true
} else {
return false
}
}
May thanks to any one. Im totally stumped
All Int() initializers with String parameters return always an optional Int.
The realtime result column in a Playground doesn't indicate the optional but printing it does.
let twentyTwo = Int("22") | 22
print(twentyTwo) | "Optional(22)\n"
I don't see how i can delete my question so ill post this to let others know it is fixed. Turns out the auction works okay but the NSUserDefaults value coming in was optional. So i was feeding the optional in. After unwrapping the NSUser value all works.
I want to be able to take a longitude/latitude value of 19.3563443 and turn it into 19.35634 (five decimal places). Is there any easy way to do this?
You can use NumberFormatter:
extension Formatter {
static let number = NumberFormatter()
}
extension FloatingPoint {
func fractionDigitsRounded(to digits: Int, roundingMode: NumberFormatter.RoundingMode = .halfEven) -> String {
Formatter.number.roundingMode = roundingMode
Formatter.number.minimumFractionDigits = digits
Formatter.number.maximumFractionDigits = digits
return Formatter.number.string(for: self) ?? ""
}
}
let value = 19.3563443
let roundedToFive = value.fractionDigitsRounded(to: 5) // "19.35634"
Round, get double value back.
NSString(format: "%.5f", myfloat).doubleValue
This won't truncate, but may be accurate enough for long/lat to 5 decimal places and is quick.
This is a math problem, not a coding problem. Converting between String and Double is not very attractive. In mathematics, truncating digits to the right of the decimal is called reducing scale. To truncate a non-integer, all you need to do is move the decimal to the right however many places you want it truncated (multiply it by 10 to move it one, multiply it by 100 to move it two, etc.), convert it into an integer (which drops everything after the new decimal), and then divide it back by the original multiplier to return the decimal to its original position.
To reduce pi's scale to 3:
move the decimal right: 3.1415927 * 10^3 = 3141.5927
zero the fraction = 3141.0
move the decimal back: 3141 / 10^3 = 3.141
Add it as an extension to Double for convenience:
extension Double {
/// Reduces a double's scale (truncates digits right of the decimal).
func reduceScale(to places: Int) -> Double {
guard !self.isNaN,
!self.isInfinite else {
return 0 // if the double is not a number or infinite, this func will throw an exception error
}
let multiplier = pow(10, Double(places))
let newDecimal = multiplier * self // move the decimal right
let truncated = Double(Int(newDecimal)) // drop the fraction
let originalDecimal = truncated / multiplier // move the decimal back
return originalDecimal
}
}
let dirty = 3.1415927
let clean = dirty.reduceScale(to: 2) // 3.14
I suggest using NSDecimalNumber. I don't know if this is actually better or worse than #Leonardo's answer. But when dealing with numbers I prefer to stick to numbers, and not convert to/from strings. The code below generates a function for a requested number of digits that can be reused as needed.
This truncates by rounding down. To get normal arithmetic rounding, change it to use .RoundPlain instead of .RoundDown.
func makeRounder(#digits: Int16) -> (Double) -> Double {
class RoundHandler : NSObject, NSDecimalNumberBehaviors {
var roundToDigits : Int16
init(roundToDigits: Int16) {
self.roundToDigits = roundToDigits
}
#objc func scale() -> Int16 {
println("Digits: \(digits)")
return roundToDigits
}
#objc func roundingMode() -> NSRoundingMode {
return NSRoundingMode.RoundDown
}
#objc func exceptionDuringOperation(operation: Selector, error: NSCalculationError, leftOperand: NSDecimalNumber, rightOperand: NSDecimalNumber) -> NSDecimalNumber? {
return nil
}
}
let roundHandler = RoundHandler(roundToDigits:digits)
func roundDigits(original:Double) -> Double {
let decimal = NSDecimalNumber(double: original)
let rounded = decimal.decimalNumberByRoundingAccordingToBehavior(roundHandler)
return rounded.doubleValue
}
println("Digits: \(digits)")
return roundDigits
}
let roundTo5 = makeRounder(digits:Int16(5))
roundTo5(19.3563443)
roundTo5(1.23456789)
I found a different answer on a different site and wanted to share it:
let x = 129.88888888777
let y = Double(round(1000*x)/1000)
y returns 129.888
thanks to swift's "round" function.
let x = (long*100000).rounded(.towardZero)/100000