1

I want to convert a value might be string or number to number finally by property wrapper.

For example:

"12" -> 12
12 -> 12

I achieved it by specific type Int, Double etc. (with below code, change T to specific type):

import Foundation

@propertyWrapper
struct NumericOrString<T: Numeric & Decodable> {

    var wrappedValue: T?

    init(wrappedValue: T?) {
        self.wrappedValue = wrappedValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let value = try? container.decode(String.self) {
            wrappedValue = T(value) // if T is Double then Double(value) works.
        } else if let value = try? container.decode(T.self) {
            wrappedValue = value
        } else {
            wrappedValue = 0
        }
    }

}

But generic type should be the right direction. So I tried the code above by generic type.

The issue is in this part:

wrappedValue = T(value) 

No exact matches in call to initializer .

So what can I do right now is:

            if T.self is Double.Type {
                wrappedValue = Double(value) as? T
            }
            if T.self is Int.Type {
                wrappedValue = Int(value) as? T
            } 

Then I have to add many ifs to check the type. Not sure if Numeric is the right one for both String and Numeric, Any better solutions?

William Hu
  • 15,423
  • 11
  • 100
  • 121

1 Answers1

1

In order for this to work, you need either concrete types to instantiate, or at least a protocol with an initializer requirement for this.

My recommendation would be to go with the latter option, of using a protocol, and push the Numeric requirement to the protocol:

protocol NumericOrStringDecodable: Numeric {
    init(numericOrStringContainer: SingleValueDecodingContainer) throws
}

@propertyWrapper struct NumericOrString<T: NumericOrStringDecodable>: Decodable {
    var wrappedValue: T
    
    init(from decoder: Decoder) throws {
        wrappedValue = try T(numericOrStringContainer: decoder.singleValueContainer())
    }
}

Using a protocol will help you a lot, as you'll be able to detect at compile time if the property wrapper was applied to an incompatible type, instead of detecting this at runtime (via a crash):

struct MyStruct {
    @NumericOrString var age: Int
    @NumericOrString var weight: Double

    // the below won't compile
    @NumericOrString var visitedCities: [String]
}

Now, coming to the protocol conformance, as others have said in the comments, you only need to support Int or Double if you're decoding from JSON.

extension Int: NumericOrStringDecodable {
    init(numericOrStringContainer container: SingleValueDecodingContainer) throws {
        if let int = try? container.decode(Int.self) {
            self = int
        } else if let string = try? container.decode(String.self), let int = Int(string) {
            self = int
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid int value"))
        }
    }
}

extension Double: NumericOrStringDecodable {
    init(numericOrStringContainer container: SingleValueDecodingContainer) throws {
        if let double = try? container.decode(Double.self) {
            self = double
        } else if let string = try? container.decode(String.self), let double = Double(string) {
            self = double
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid double value"))
        }
    }
}
Cristik
  • 30,989
  • 25
  • 91
  • 127
  • If this works for parsing JSON values that are either string or int, this will be so much more elegant than the current decoder overloads that are required with coding keys. I'll create a test for that now. Do you think a property wrapper could be used similarly for int or string arrays where the JSON values might be the opposite type? e.g. `[Int64]` is my declared type but the JSON input is `[String]`. – John Kaster Jan 29 '22 at 01:31
  • Working on the test the extension code for string conversion is more complicated than I thought. I'll resume later. Thanks for the lead, though. – John Kaster Jan 29 '22 at 01:46