0

I have some JSON that I would like to reformat before use, preferably in an initializer (or extension ??)

[
{
 "name": "Diesel",
 "id": "1",
 "maj": "2022-07-06 18:28:29",
 "value": "2.81"
  },
{
 "name": "SP95",
 "id": "5",
 "maj": "2022-07-06 18:28:29",
 "value": "2.048"
  }
] 

I would like to ensure that the "value" data is always 3 decimal places. So in the above 2.810 instead of 2.81.

I have looked at CustomStringConvertible and in theory it looks possible, but I haven't managed to build a working version.

Mainly working from here https://www.swiftbysundell.com/articles/formatting-numbers-in-swift/

my Model looks like this :

struct Price: Codable {
    let name: String
    let id, maj: String?
    var value: String?
    var isCheapest: Bool?
        
}

I understand that I need to do some basic number formatting but I don't see how to integrate it with CustomStringConvertible or if this is the correct way to go about it. Any help appreciated.

jat
  • 183
  • 3
  • 14
  • You want JSON or Price struct? Ie, encoding or decoding the JSON? I guess it's encoding. So you should use a custom `encode(to encoder: Encoder)` and in it ensure that the `value` output is 3 decimal. You can use a `NumberFormatter` for that. – Larme Jul 07 '22 at 09:09
  • Do you want to make sure that the `value` should have 3 decimal alway when you access it? – Jayachandra A Jul 07 '22 at 09:16
  • Yes, always 3 decimal places when accessed. (decoding from JSON) – jat Jul 07 '22 at 09:25
  • Consider that the value of `value` is a string, not a number. You can just append as many `”0”` as you need. – vadian Jul 07 '22 at 10:40

3 Answers3

1

Use extension

extension Metric: CustomStringConvertible {
    var description: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 3

        let number = NSNumber(value: value)
        let formattedValue = formatter.string(from: number)!
        return "\(name): \(formattedValue)"
    }
}
Jayesh Patel
  • 938
  • 5
  • 16
  • As is, this give me an error on line 7 , No exact matches in call to initializer , I can build by changing it to: let number = NSNumber(value: Double(valeur ?? "0.000") ?? 0.000) .. but doesn't seem to work. – jat Jul 07 '22 at 09:59
0

I think you can use it like below

Extension to the string

extension String {
    var doubleValue: Double? {
        get{
            Double(self)
        }
    }

    var formatedValue: String? {
        get{
            // YOU HAVE TO MAKE SURE THAT THE STRING IS CONVERTABLE TO `DOUBLE`
            String(format: "%.3f", self.doubleValue ?? 0.000)
        }
    }
}

Usage:

let price = Price(name: "Diesel", id: "1", maj: "2022-07-06 18:28:29", value: "2.81", isCheapest: false)
if let value = price.value?.formatedValue{
    print(value)
}
//Console Output - 2.810
Jayachandra A
  • 1,335
  • 1
  • 10
  • 21
  • This would work too thanks, but I think the extension on the model using CustomStringConvertible by Jayesh is a nicer solution. Numbers come back formatted in all cases. – jat Jul 07 '22 at 09:35
0

I would replace the String value with a custom type, i.e Wrapped in the example below and implement Codable for it, which should work transparently for the underlying string value.

Also note rounding mode on number formatter, if you want to simply clip the fraction, then .floor should do it.

import Foundation

private let formatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.decimalSeparator = "."
    formatter.maximumFractionDigits = 3
    formatter.roundingMode = .floor
    return formatter
}()

struct Value: Codable {
    var wrapped: Wrapped
}

struct Wrapped: Codable {
    let source: NSNumber

    var doubleValue: Double {
        return source.doubleValue
    }

    init(_ doubleValue: Double) {
        source = NSNumber(value: doubleValue)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let stringValue = try container.decode(String.self)

        guard let numberObject = formatter.number(from: stringValue) else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: container.codingPath,
                    debugDescription: "Cannot parse NSNumber from String")
            )
        }

        source = numberObject
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        guard let stringValue = formatter.string(from: source) else {
            throw EncodingError.invalidValue(
                source,
                EncodingError.Context(
                    codingPath: container.codingPath,
                    debugDescription: "Cannot convert NSNumber to String"
                )
            )
        }

        try container.encode(stringValue)
    }

}

let value = Value(wrapped: Wrapped(11.234567899))
let data = try! JSONEncoder().encode(value)
let stringData = String(data: data, encoding: .utf8)!

// Prints: {"wrapped": "11.234"}
print("\(stringData)")


// Prints 11.234
let reverse = try JSONDecoder().decode(Value.self, from: data)
print("\(reverse.wrapped.source)")

pronebird
  • 12,068
  • 5
  • 54
  • 82