How to Format Number Converted From String Without Losing Decimal Precision - swift

I have the following JSON payload that I need to convert to numbers and subsequently format for display.
{
"kilometers_per_second": "14.4578929636",
"kilometers_per_hour": "52048.4146691173",
"miles_per_hour": "32340.8607703746"
}
Using Codable, I created the following structure:
struct RelativeVelocity: Codable, Equatable {
let kilometersPerSecond: String?
let kilometersPerHour: String?
let milesPerHour: String?
enum CodingKeys: String, CodingKey {
case kilometersPerSecond = "kilometers_per_second"
case kilometersPerHour = "kilometers_per_hour"
case milesPerHour = "miles_per_hour"
}
}
The properties are String instances because that's what the API returns, and I am learning to use view models for the first time, so I would like to use a view model to convert the String instances into numbers prior to returning formatted String instances.
My view model has the following structure:
struct RelativeVelocityViewModel {
private let relativeVelocity: RelativeVelocity
init(relativeVelocity: RelativeVelocity) {
self.relativeVelocity = relativeVelocity
}
}
extension RelativeVelocityViewModel {
var formattedKilometersPerHour: String? {
guard
let stringValue = relativeVelocity.kilometersPerHour,
let decimalValue = Decimal(string: stringValue),
let formatted = NumberFormatter.relativeVelocityFormatter.string(from: decimalValue as NSNumber)
else { return nil }
return formatted
}
var formattedKilometersPerSecond: String? {
guard
let stringValue = relativeVelocity.kilometersPerSecond,
let decimalValue = Decimal(string: stringValue),
let formatted = NumberFormatter.relativeVelocityFormatter.string(from: decimalValue as NSNumber)
else { return nil }
return formatted
}
var formattedMilesPerHour: String? {
guard
let stringValue = relativeVelocity.kilometersPerSecond,
let decimalValue = Decimal(string: stringValue),
let formatted = NumberFormatter.relativeVelocityFormatter.string(from: decimalValue as NSNumber)
else { return nil }
return formatted
}
}
As you can see, it converts the String instances into Decimal instances, and the Decimal instances are then formatted by the following NumberFormatter:
extension NumberFormatter {
static let relativeVelocityFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = .max
formatter.numberStyle = .decimal
formatter.usesGroupingSeparator = true
return formatter
}()
}
My XCTestCase subclass for testing my view models is:
class Tests_RelativeVelocityViewModel: XCTestCase {
let kilometersPerSecond = "14.4578929636"
let kilometersPerHour = "52048.4146691173"
let milesPerHour = "32340.8607703746"
var populatedViewModel: RelativeVelocityViewModel!
var emptyViewModel: RelativeVelocityViewModel!
override func setUpWithError() throws {
try super.setUpWithError()
let populatedRelativeVelocity = RelativeVelocity(
kilometersPerSecond: kilometersPerSecond,
kilometersPerHour: kilometersPerHour,
milesPerHour: milesPerHour
)
populatedViewModel = RelativeVelocityViewModel(relativeVelocity: populatedRelativeVelocity)
let emptyRelativeVelocity = RelativeVelocity(
kilometersPerSecond: nil,
kilometersPerHour: nil,
milesPerHour: nil
)
emptyViewModel = RelativeVelocityViewModel(relativeVelocity: emptyRelativeVelocity)
}
override func tearDownWithError() throws {
emptyViewModel = nil
populatedViewModel = nil
try super.tearDownWithError()
}
func test_RelativeVelocityViewModel_ReturnsNilFormattedKilometersPerHour_WhenValueIsMissing() {
XCTAssertNil(emptyViewModel.formattedKilometersPerHour)
}
func test_RelativeVelocityViewModel_ReturnsFormattedKilometersPerHour_WhenValueIsPresent() {
let expected = "52,048.4146691173"
XCTAssertEqual(populatedViewModel.formattedKilometersPerHour, expected)
}
func test_RelativeVelocityViewModel_ReturnsNilFormattedKilometersPerSecond_WhenValueIsMissing() {
XCTAssertNil(emptyViewModel.formattedKilometersPerSecond)
}
func test_RelativeVelocityViewModel_ReturnsNilFormattedMilesPerHour_WhenValueIsMissing() {
XCTAssertNil(emptyViewModel.formattedMilesPerHour)
}
}
The following test...
func test_RelativeVelocityViewModel_ReturnsFormattedKilometersPerHour_WhenValueIsPresent() {
let expected = "52,048.4146691173"
XCTAssertEqual(populatedViewModel.formattedKilometersPerHour, expected)
}
...produces the following failure:
XCTAssertEqual failed: ("Optional("52,048.414669")") is not equal to ("Optional("52,048.4146691173")")
I know that I can use XCTAssertEqual(_:_:accuracy:_:file:line:), but I want to retain all of the decimal values.
What am I doing incorrectly that is causing the formatted result to be rounded by losing the value's precision?

Try this:
class MyProjectTests: XCTestCase {
func testExample() throws {
let stringValue = "52048.12345678911111"
let decimalValue = Decimal(string: stringValue)!
let formatted = NumberFormatter.relativeVelocityFormatter(maxFractionDigits: decimalValue.significantFractionalDecimalDigits).string(from: decimalValue as NSNumber)
XCTAssert(formatted == stringValue)
}
}
extension NumberFormatter {
static func relativeVelocityFormatter(maxFractionDigits: Int) -> NumberFormatter {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = maxFractionDigits
formatter.numberStyle = .none
formatter.usesGroupingSeparator = true
return formatter
}
}
extension Decimal {
var significantFractionalDecimalDigits: Int {
return max(-exponent, 0)
}
}
Anyway, there is always a limit:
33 decimal digits.

Related

Converting text field data to decimal value and back to string

I converted text field string into a double to do calculations and then back to a string to output it on a label. I am now working with currency inputs so I need to convert it to a decimal rather than a double. Can someone help?
func calcTotal() {
let totalConv: Double? = Double(totalTextField.text!)
let tipConv: Double? = Double(tipTextField.text!)
guard totalConv != nil && tipConv != nil else {
return
}
let result = totalConv! * ((tipConv! / 100) + 1)
let output = String(format: "$ %.2f", result)
totalAmount.text = String(output)
}
You will just need to use Decimal(string:) initializer and NumberFormatter (currency style) to format your decimal value.
func calcTotal() {
guard
let totalConv = Decimal(string: totalTextField.text!),
let tipConv = Decimal(string: tipTextField.text!)
else { return }
let result = totalConv * ((tipConv / 100) + 1)
totalAmount.text = Formatter.currency.string(for: result)
}
extension Formatter {
static let currency: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
return numberFormatter
}()
}

What's wrong with my code? It says unresolved Identifier

It says "use of unresolved identifier" for both NSNumber and NSNumber. I can't find a solution for this.
I should be able to create a program that will convert each number from a given integer to its equivalent word value. The accepted value should be UInt16 only
public extension Int {
public var asWord: String {
let numberValue = NSNumber(value: self)
let formatter = NSNumberFormatter()
formatter.numberStyle = .spellOut
return formatter.string(from: numberValue)!
}
}
var value = 2500
if value > UInt16.max || value < UInt16.min {
print("The accepted value should be UInt16 only")
}
else {
print("\(value.asWord)")
}
You forgot import Foundation.
Try this (Swift 4):
import Foundation
public extension Int {
public var asWord: String {
let numberValue = NSNumber(value: self)
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter.string(from: numberValue)!
}
}
var value = 2500
if value > UInt16.max || value < UInt16.min{
print("The accepted value should be UInt16 only")
} else {
print("\(value.asWord)")
}
Do you use the if block and value definition in a class?!!
like this:
public extension Int {
public var asWord: String {
let numberValue = NSNumber(value: self)
let formatter = NSNumberFormatter()
formatter.numberStyle = .spellOut
return formatter.string(from: numberValue)!
}
}
class yourClassName {
var value = 2500
if value > UInt16.max || value < UInt16.min {
print("The accepted value should be UInt16 only")
}
else {
print("\(value.asWord)")
}
}

Check the language of numbers if it is English or Arabic/Persian

In my project I am planning to change the language of any type number to Persian and here is what I have done:
public extension String {
func perstianString (string: String)->String {
let digitSet = CharacterSet.decimalDigits
var finalString = String()
for ch in string.unicodeScalars {
if digitSet.contains(ch) {
let sh = convertoPersianNum(num: "\(ch)")
finalString += sh
}
} else {
finalString += "\(ch)"
}
}
return finalString
}
func convertoPersianNum(num: String)->String{
var retVlue = String()
var num1 = num
let number = NSNumber(value: Int(num1)!)
let numb = (num as NSString).intValue
let format = NumberFormatter()
format.locale = Locale(identifier: "fa_IR")
let faNumber = format.string(from: number)
return faNumber!
}
}
But when the source value has Persian numbers, the app crashes. Simply said, I want to check if it is a Persian number, don't do anything, else do the the conversion above:
let string = "ییسس ۱۲۳۴"
with this type do not do anything else do something.
public extension String {
private var isPersian: Bool {
let predicate = NSPredicate(format: "SELF MATCHES %#", "(?s).*\\p{Arabic}.*")
return predicate.evaluate(with: self)
}
private var toPersianNum: String {
let number = NSDecimalNumber(string: self)
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "fa_IR")
return formatter.string(from: number) ?? ""
}
var persian:String {
var retVal = String()
self.enumerateSubstrings(in: startIndex..<endIndex, options: .byComposedCharacterSequences) { (string, _, _, _) in
guard let string = string else {return}
if string.isPersian {
retVal += string
}else {
retVal += string.toPersianNum
}
}
return retVal
}
}
let string = "9001ییسس2345777 ۱۲۳۴2345"
string.persian // "۹۰۰۱ییسس۲۳۴۵۷۷۷ناعدد۱۲۳۴۲۳۴۵" (result)

Swift 2.0 Guard Statement Fails Struct Initializer

There was a similarly named topic but the example was an error due to user mistake. I believe this example is an actual XCode issue.
I was following a treehouse tutorial and in the spirit of swift 2.0 I used guard statements instead of if lets in the initializer. My code was identical to the instruction except for the use of guard statements. It had one error that said "return from initializer without initializing all stored properties". Once I changed it to if let statements, it worked. Perhaps I made a mistake somewhere but I stared at it for atleast an hour, no properties were left un-initialized.
I made the properties equal to nil in the else clauses just in case but that didnt affect anything.
struct DailyWeather {
let maxTemp: Int?
let minTemp: Int?
let humidity: Int?
let precipChance: Int?
var summary: String?
var icon: UIImage? = UIImage(named: "default.png")
var largeIcon: UIImage? = UIImage(named: "default_large.png")
var sunriseTime: String?
var sunsetTime: String?
var day: String?
let dateFormatter = NSDateFormatter()
init(dailyWeatherDictionary: [String:AnyObject]) {
minTemp = dailyWeatherDictionary["temperatureMin"] as? Int
maxTemp = dailyWeatherDictionary["temperatureMax"] as? Int
guard let humidityFloat = dailyWeatherDictionary["humidity"] as? Double else { humidity = nil ; return }
humidity = Int(humidityFloat * 100)
guard let precipFloat = dailyWeatherDictionary["precipProbability"] as? Double else { precipChance = nil ; return }
precipChance = Int(precipFloat * 100)
summary = dailyWeatherDictionary["summary"] as? String
guard let
iconString = dailyWeatherDictionary["icon"] as? String,
iconEnum = Icon(rawValue: iconString) else { icon = nil ; largeIcon = nil ; return }
(icon, largeIcon) = iconEnum.toImage()
guard let sunriseDate = dailyWeatherDictionary["sunriseTime"] as? Double else { sunriseTime = nil ; return }
sunriseTime = timeStringFromUnixTime(sunriseDate)
guard let sunsetDate = dailyWeatherDictionary["sunsetTime"] as? Double else { sunsetTime = nil ; return }
sunsetTime = timeStringFromUnixTime(sunsetDate)
guard let time = dailyWeatherDictionary["time"] as? Double else { day = nil ; return }
day = dayStringFromUnixTime(time)
}
func timeStringFromUnixTime(unixTime: Double) -> String {
let date = NSDate(timeIntervalSince1970: unixTime)
dateFormatter.dateFormat = "hh:mm a"
return dateFormatter.stringFromDate(date)
}
func dayStringFromUnixTime(unixTime: Double) -> String {
let date = NSDate(timeIntervalSince1970: unixTime)
dateFormatter.locale = NSLocale(localeIdentifier: NSLocale.currentLocale().localeIdentifier)
dateFormatter.dateFormat = "EEEE"
return dateFormatter.stringFromDate(date)
}
}
let's have
struct S {
var i: Int?
init(b: Bool){
guard b == false else { return }
//if b == true { return }
i = 0 // if b == true, this statement doesn't execute
}
}
let s1 = S(b: true)
let s2 = S(b: false)
print(s1, s2) // S(i: nil) S(i: Optional(0))
because var i: Int? has a default value nil, even though i = 0 is not reachable if parameter of init is true, the compiler doesn't complain.
struct S {
let i: Int?
init(b: Bool){
guard b == false else { return }
//if b == true { return }
i = 0 // if b == true, this statement doesn't execute
}
}
will NOT compile, with error: return from initializer without initializing all stored properties and note: 'self.i' not initialized, because constant let i: Int? doesn't have any default value
Your trouble is, that you return from init. Normally, avoid return from an initializer if your initializer is not fail-able / init? /. In case of fail-able init? the only accepted return value is nil.

Swift playground - How to convert a string with comma to a string with decimal

I'm new in the Swift world.
How can I converting a String with a comma to a String with a decimal?
The code work's fine with a dot (.)
The problem is when I'm using a comma (,) ... with: var price
The origin of the problem is the Decimal french keyboard use a comma (,) instead of a dot (.)
Don't know exactly how to use NSNumberFormatter or generatesDecimalNumbers if it's the key. There's probebly more than one options.
//The answer change if "2,25" or "2.25" is used.
var price : String = "2,25"
var priceFloat = (price as NSString).floatValue
//I need to have 2.25 as answer.
var costString = String(format:"%.2f", priceFloat)
Thank's for your time and your help!
update: Xcode 8.2.1 • Swift 3.0.2
You can use NumberFormatter() to convert your string to number. You just need to specify the decimalSeparator as follow:
extension String {
static let numberFormatter = NumberFormatter()
var doubleValue: Double {
String.numberFormatter.decimalSeparator = "."
if let result = String.numberFormatter.number(from: self) {
return result.doubleValue
} else {
String.numberFormatter.decimalSeparator = ","
if let result = String.numberFormatter.number(from: self) {
return result.doubleValue
}
}
return 0
}
}
"2.25".doubleValue // 2.25
"2,25".doubleValue // 2.25
let price = "2,25"
let costString = String(format:"%.2f", price.doubleValue) // "2.25"
You should do the currency formatting also with NumberFormat, so create a read-only computed property currency extending FloatingPoint protocol to return a formatted string from the String doubleValue property.
extension NumberFormatter {
convenience init(style: Style) {
self.init()
self.numberStyle = style
}
}
extension Formatter {
static let currency = NumberFormatter(style: .currency)
}
extension FloatingPoint {
var currency: String {
return Formatter.currency.string(for: self) ?? ""
}
}
let costString = "2,25".doubleValue.currency // "$2.25"
Formatter.currency.locale = Locale(identifier: "en_US")
"2222.25".doubleValue.currency // "$2,222.25"
"2222,25".doubleValue.currency // "$2,222.25"
Formatter.currency.locale = Locale(identifier: "pt_BR")
"2222.25".doubleValue.currency // "R$2.222,25"
"2222,25".doubleValue.currency // "R$2.222,25"
You can use for this Swift 3:
let currentAmount = "2,50"
currentAmount = currentAmount.replacingOccurrences(of: ",", with: ".")
print(currentAmount) // "2.50\n"
var price = "2,25"
price = price.replacingOccurrences(of: ",", with: ".")
var priceFloat = (price as NSString).floatValue
Nullable extension version:
extension String
{
static let customNumberFormatter = NumberFormatter()
var doubleValue: Double? {
String.customNumberFormatter.decimalSeparator = "."
if let result = String.customNumberFormatter.number(from: self) {
return result.doubleValue
} else {
String.customNumberFormatter.decimalSeparator = ","
if let result = String.customNumberFormatter.number(from: self) {
return result.doubleValue
}
}
return nil
}
}
EDIT: Updated to work with the current version of Swift:
let amount = "8,35"
var counter: Int = 0
var noCommaNumber: String!
for var carattere in (amount) {
if carattere == "," { carattere = "." }
if counter != 0 { noCommaNumber = "\(noCommaNumber ?? "\(carattere)")" + "\(carattere)" } else { noCommaNumber = "\(carattere)" } // otherwise first record will always be nil
counter += 1
}
let importo = Float(noCommaNumber)