3

I have some data returned from the server that look like this:

let returnedFromServer = ["title" : ["abc",  "def",  "ghi"],

                      "time"  : ["1234", "5678", "0123"],

                      "content":["qwerty", "asdfg", "zxcvb"]]

I want to transform it into something like this:

let afterTransformation =

[["title" : "abc",
  "time"  : "1234",
 "content": "qwerty"],

["title" : "def",
 "time"  : "5678",
"content": "asdfg"],

["title" : "ghi",
 "time"  : "0123",
"content": "zxcvb"]]

My current implementation is as follows:

var outputArray = [[String : AnyObject]]()

for i in 0..<(returnedFromServer["time"] as [String]).count {

        var singleDict = [String: AnyObject]()

        for attribute in returnedFromServer {

            singleDict[attribute] = returnedFromServer[attribute]?[i]
        }
        outputArray.append(singleDict)
}

This works fine but I think it is not a very elegant solution. Given that Swift has some neat features such as reduce, filter and map, I wonder if I can do the same job without explicitly using a loop.

Thanks for any help!

3 Answers3

3

Using the ideas and the dictionary extension

extension Dictionary {
    init(_ pairs: [Element]) {
        self.init()
        for (k, v) in pairs {
            self[k] = v
        }
    }

    func map<OutKey: Hashable, OutValue>(transform: Element -> (OutKey, OutValue)) -> [OutKey: OutValue] {
        return Dictionary<OutKey, OutValue>(Swift.map(self, transform))
    }
}

from

you could achieve this with

let count = returnedFromServer["time"]!.count
let outputArray = (0 ..< count).map {
    idx -> [String: AnyObject] in
    return returnedFromServer.map {
        (key, value) in
        return (key, value[idx])
    }
}
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
2

Martin R’s answer is a good one and you should use that and accept his answer :-), but as an alternative to think about:

In an ideal world the Swift standard library would have:

  • the ability to initialize a Dictionary from an array of 2-tuples
  • a Zip3 in addition to a Zip2 (i.e. take 3 sequences and join them into a sequence of 3-tuples
  • an implementation of zipWith (i.e. similar to Zip3 but instead of just combining them into pairs, run a function on the given tuples to combine them together).

If you had all that, you could write the following:

let pairs = map(returnedFromServer) { (key,value) in map(value) { (key, $0) } }
assert(pairs.count == 3)
let inverted = zipWith(pairs[0],pairs[1],pairs[2]) { [$0] + [$1] + [$2] }
let arrayOfDicts = inverted.map { Dictionary($0) }

This would have the benefit of being robust to ragged input – it would only generate those elements up to the shortest list in the input (unlike a solution that takes a count from one specific list of the input). The downside it its hard-coded to a size of 3 but that could be fixed with a more general version of zipWith that took a sequence of sequences (though if you really wanted your keys to be strings and values to be AnyObjects not strings you’d have to get fancier.

Those functions aren’t all that hard to write yourself – though clearly way too much effort to write for this one-off case they are useful in multiple situations. If you’re interested I’ve put a full implementation in this gist.

Airspeed Velocity
  • 40,491
  • 8
  • 113
  • 118
1

I'd create 2 helpers:

ZipArray (similar to Zip2, but works with arbitrary length):

struct ZipArray<S:SequenceType>:SequenceType {
    let _sequences:[S]

    init<SS:SequenceType where SS.Generator.Element == S>(_ base:SS) {
        _sequences =  Array(base)
    }

    func generate() -> ZipArrayGenerator<S.Generator> {
        return ZipArrayGenerator(map(_sequences, { $0.generate()}))
    }
}

struct ZipArrayGenerator<G:GeneratorType>:GeneratorType {
    var generators:[G]
    init(_ base:[G]) {
        generators = base
    }
    mutating func next() -> [G.Element]? {
        var row:[G.Element] = []
        row.reserveCapacity(generators.count)
        for i in 0 ..< generators.count {
            if let e = generators[i].next() {
                row.append(e)
            }
            else {
                return nil
            }
        }
        return row
    }
}

Basically, ZipArray flip the axis of "Array of Array", like:

[
    ["abc",  "def",  "ghi"],
    ["1234", "5678", "0123"],
    ["qwerty", "asdfg", "zxcvb"]
]

to:

[
    ["abc", "1234", "qwerty"],
    ["def", "5678", "asdgf"],
    ["ghi", "0123", "zxcvb"]
]

Dictionary extension:

extension Dictionary {
    init<S:SequenceType where S.Generator.Element == Element>(_ pairs:S) {
        self.init()
        var g = pairs.generate()
        while let (k:Key, v:Value) = g.next() {
            self[k] = v
        }
    }
}

Then you can:

let returnedFromServer = [
    "title" : ["abc",  "def",  "ghi"],
    "time"  : ["1234", "5678", "0123"],
    "content":["qwerty", "asdfg", "zxcvb"]
]

let outputArray = map(ZipArray(returnedFromServer.values)) {
    Dictionary(Zip2(returnedFromServer.keys, $0))
}
rintaro
  • 51,423
  • 14
  • 131
  • 139