0

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
                        }
2
  • 3
    Horrible format. Blame the owner of the service and ask him to send something standardized like JSON. First separate the string by , then in a loop separate each item by = and insert the result as key/value into a [String:String] dictionary. Commented Nov 21, 2018 at 18:14
  • does the server provide any other output formats? This isn't one of the popular standards (json, yaml, csv, xml, etc.) Commented Nov 21, 2018 at 18:14

3 Answers 3

2

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])
})
Sign up to request clarification or add additional context in comments.

1 Comment

Once you have this, the rest is easy. Good answer.
1

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
}

Comments

0

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.