UKDataGeek
UKDataGeek

Reputation: 6902

Parse String into an object in Swift

I have received this response from the server and I am sure there must be a more efficient way to convert it into an object.
I have the following response:

[
id=2997,rapidViewId=62,state=ACTIVE,name=Sprint7,startDate=2018-11-20T10:28:37.256Z,endDate=2018-11-30T10:28:00.000Z,completeDate=<null>,sequence=2992,goal=none
]

How do I convert it nicely into a well formed swift object in the simplest way?

Here is my attempt which gives me just the Sprint Value

if sprintJiraCustomField.count > 0 {
                         let stringOutput = sprintJiraCustomField.first?.stringValue // convert output to String
                        let name = stringOutput?.components(separatedBy: "name=") // get name section from string
                        let nameFieldRaw = name![1].components(separatedBy: ",") // split out to the comma
                        let nameValue = nameFieldRaw.first!
                        sprintDetail = nameValue// show name field
                        }

Upvotes: 0

Views: 1110

Answers (3)

anon
anon

Reputation:

We already have some suggestion to first split the string at each comma and then split each part at the equals sign. This is rather easy to code and works well, but it is not very efficient as every character has to be checked multiple times. Writing a proper parser using Scanner is just as easy, but will run faster.

Basically the scanner can check if a given string is at the current position or give you the substring up to the next occurrence of a separator.

With that the algorithm would have the following steps:

  1. Create scanner with the input string
  2. Check for the opening bracket, otherwise fail
  3. Scan up to the first =. This is the key
  4. Consume the =
  5. Scan up to the first , or ]. This is the value
  6. Store the key/value pair
  7. If there is a , consume it and continue with step 3
  8. Consume the final ].

Sadly the Scanner API is not very Swift-friendly. With a small extension it is much easier to use:

extension Scanner {
    func scanString(_ string: String) -> Bool {
        return scanString(string, into: nil)
    }

    func scanUpTo(_ delimiter: String) -> String? {
        var result: NSString? = nil
        guard scanUpTo(delimiter, into: &result) else { return nil }
        return result as String?
    }

    func scanUpTo(_ characters: CharacterSet) -> String? {
        var result: NSString? = nil
        guard scanUpToCharacters(from: characters, into: &result) else { return nil }
        return result as String?
    }
}

With this we can write the parse function like this:

func parse(_ list: String) -> [String: String]? {
    let scanner = Scanner(string: list)

    guard scanner.scanString("[") else { return nil }

    var result: [String: String] = [:]

    let endOfPair: CharacterSet = [",", "]"]
    repeat {
        guard
            let key = scanner.scanUpTo("="),
            scanner.scanString("="),
            let value = scanner.scanUpTo(endOfPair)
        else {
            return nil
        }

        result[key] = value
    } while scanner.scanString(",")

    guard scanner.scanString("]") else { return nil }

    return result
}

Upvotes: 1

AbdelAli
AbdelAli

Reputation: 816

This is a work for reduce:

let keyValueStrings = yourString.components(separatedBy: ",")

let dictionary = keyValueStrings.reduce([String: String]()) {
    (var aggregate: [String: String], element: String) -> [String: String] in

    let elements = element.componentsSeparatedByString("=")
    let key = elements[0]

    // replace nil with the value you want to use if there is no value        
    let value = (elements.count > 1) ? elements[1] : nil
    aggregate[key] = value

    return aggregate
}

This is a functional approach, but you can achieve the same using a for iteration. So then you can use Swift’s basic way of mapping. for example you will have your custom object struct. First, you will add an init method to it. Then map your object like this:

init(with dictionary: [String: Any]?) {
  guard let dictionary = dictionary else { return }
  attribute = dictionary["attrName"] as? String
}

let customObjec = CustomStruct(dictionary: dictionary)

Upvotes: 0

Joakim Danielson
Joakim Danielson

Reputation: 51971

Not sure what format you want but the below code will produce an array of tuples (key, value) but all values are strings so I guess another conversion is needed afterwards

let items = stringOutput.components(separatedBy: ",").compactMap( {pair -> (String, String) in
    let keyValue = pair.components(separatedBy: "=")
    return (keyValue[0], keyValue[1])
})

Upvotes: 2

Related Questions