How to format negative 0 with currency NumberFormatter - swift

I noticed my app is displaying "-$0.00" for a value of -0. The simple form of the math is let balance: Double = -1 * value. I want my negative 0 values to just display as "$0.00". Is there a way to have the NumberFormatter properly handle this?
let formatter = NumberFormatter()
formatter.numberStyle = .currency
let d: Double = -1 * 0
formatter.string(from: d as NSNumber)! // -$0.00

If you are working with money, currency etc it is much better to use Decimal than Double and it will also solve your problem here, change the declaration of d as below
let d: Decimal = -1 * 0
and the formatter will produce "$0.00"

My current approach is:
struct Formatter {
static func currency(_ value: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
let value = (value == 0) ? 0 : value // Convert -0 to 0
return formatter.string(from: value as NSNumber) ?? ""
}
}
Formatter.currency(-1 * 0) // $0.00

Related

Using Swift ".formatted()" for decimals with trailing zeros removed?

I am trying to remove trailing zeros from doubles, the code I've seen are similar to this.
let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
print(formatter.string(from: 1.0000)!) // 1
print(formatter.string(from: 1.2345)!) // 1.23
Which requires you to create a NumberFormatter(). I know Apple introduced a new .formatted() function recently. Does anyone know if it's possible to do the above with this new system?
The documentation is a bit convoluted for me but so far I've only seen the ability to set the precision.
let string = 1.00000.formatted(.number.precision(.fractionLength(2)))
// 1.00
.precision is what you should use and then there is a variant of .fractionLength that takes a range as argument,
10_000.123.formatted(.number.precision(.fractionLength(0…2)))
You can create your own FormatStyle
public struct DecimalPrecision<Value>: FormatStyle, Equatable, Hashable, Codable where Value : BinaryFloatingPoint{
let maximumFractionDigits: Int
public func format(_ value: Value) -> String {
let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = maximumFractionDigits
return formatter.string(from: value as! NSNumber) ?? ""
}
}
extension FormatStyle where Self == FloatingPointFormatStyle<Double> {
public static func precision (maximumFractionDigits: Int) -> DecimalPrecision<Double> {
return DecimalPrecision(maximumFractionDigits: maximumFractionDigits)
}
}
Then use it
let string = 1.00000.formatted(.precision(maximumFractionDigits: 2))

How to properly reduce scale and format a Float value in Swift?

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

Trying to combine a number and variable in Swift

I am a newbie trying hard to create 0.2 from combining a a number and a variable together. But I think I have it all backwards somehow. can anyone help?
//turns a string of '20%' into '2'.
let tipPercent = tip.prefix(1)
//turns the string into a Int of '2'
let tipPercent1: Int = Int(tipPercent) ?? 0
//So now I want to combine the 0. with the variable tipPercent1. this will create '0.2'
let twentyTipamount = (billamount * 0.tipPercent1) + billamount
It seems like you just want to parse a percent string into a number. You should use a NumberFormatter.
let numberFormatter = NumberFormatter()
numberFormatter.locale = Locale(identifier: "en-US_POSIX")
// or whatever other locale that your tip is written in
numberFormatter.numberStyle = .percent
// if the tip cannot be parsed, use 0 as the default
let tipRate = numberFormatter.number(from: tip)?.doubleValue ?? 0
// or you can do something else:
// guard let tipRate = numberFormatter.number(from: tip)?.doubleValue else { ... }
// calculate the amount after tips
let totalAmount = billamount * (1 + tipRate)

Swift lose precision in decimal formatting

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

Format Double for negative currency, or remove "-" sign from Double

I have a Double which can be positive or negative that I want to display in the following way:
For positive values:
+$1.10
For negative values:
-$0.50
And blank for 0 values
I've tried like this:
Double surcharge = ...
if(surcharge < 0){
cell.lblSurcharge.text = "-$" + String(format:"%.02f", surcharge)
}else if(surcharge > 0){
cell.lblSurcharge.text = "+$" + String(format:"%.02f", surcharge)
}else{
cell.lblSurcharge.text = nil
}
but for negative values it shows them like this:
-$-0.50
How can I format it correctly, or remove the "-" from the Double?
As much as possible, you should be making use of the available formatters the API supplies, for example, NumberFormatter
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.string(from: 50.0) // "$50.00"
formatter.string(from: -50.0) // "-$50.00"
This makes it easier to support different localisations without any additional code
So, if were to add formatter.locale = Locale.init(identifier: "en_UK"), the output would become...
"£50.00"
"-£50.00"
Obviously, in most cases I'd use Locale.current, but situations change
Sure, but I want the positive output to show the "+" sign too
It's not as pretty as I might like, but...
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.positivePrefix = formatter.plusSign + formatter.currencySymbol
formatter.string(from: 50.0)
formatter.string(from: -50.0)
Outputs
"+$50.00"
"-$50.00"
such as if the view is reloaded and I don't reset numberStyle to .currency before the String is prepared
Make it lazy property
var currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.positivePrefix = formatter.plusSign + formatter.currencySymbol
return formatter
}()
And/or a static (and possibly lazy) property of "configuration" struct/class
You know that surcharge is negative at that point, so just add - to make it positive for the formatting. Also, you can move the "-$" into the format string:
Change:
cell.lblSurcharge.text = "-$" + String(format:"%.02f", surcharge)
to:
cell.lblSurcharge.text = String(format:"-$%.02f", -surcharge)
To do it all in one line, use the trinary operator (?:) to handle zero and to add the sign to the format string:
cell.lblSurcharge.text = surcharge == 0 ? "" : String(format:"%#$%.02f", surcharge > 0 ? "+" : "-", abs(surcharge))
Easy answer might be to use the "abs" function exposed by the class type.
cell.lblSurcharge.text = "-$" + String(format:"%.02f", abs(surcharge))