niks
niks

Reputation: 554

Swift: Merging 2 or more elements custom objects in array based on unique value

Below is my custom object class.

class UserGroups: NSObject {
    let groupName: String
    let users: [CheckIn]?

    init(json:JSON) {
        self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
        self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
    }

    class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn]{
        return jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
            return CheckIn(json: jsonItem)
        })
    }
}

I've an array of above custom objects. How can I combine 2 or more custom objects into a single object by merging users of every object having same groupName.

Below is my CheckIn Model:

class CheckIn: NSObject {

let id: String
let firstName: String
let lastName: String
let latitude: String
let longitude: String
let hint: String

init(json: JSON) {
    self.id = json[Constants.Models.CheckIn.id].stringValue
    self.firstName = json[Constants.Models.CheckIn.firstName].stringValue
    self.lastName = json[Constants.Models.CheckIn.lastName].stringValue
    self.hint = json[Constants.Models.CheckIn.hint].stringValue
    self.latitude = json["location"][Constants.Models.CheckIn.latitude].stringValue
    self.longitude = json["location"][Constants.Models.CheckIn.longitude].stringValue
}

}

id field is not unique in CheckIn.

Upvotes: 0

Views: 3035

Answers (2)

hashemi
hashemi

Reputation: 2678

The Solution

You did not include the CheckIn model, but I will assume that it has some sort of an id field unique to each user. We will use this to make the object Hashable:

// Add this to your file outside of the UserGroups class
extension CheckIn: Hashable {
    var hashValue: Int { return self.id }
}

Making it Hashable allows you to convert the Array to a Set, which does not allow duplicates and will remove them in a very efficient way.

// Change getUserGroupsList as follows
class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
    return Array(Set(jsonArray.flatMap({ (jsonItem: JSON) -> CheckIn in
        return CheckIn(json: jsonItem)
    })))
}

Optional Considerations

As an aside, in case you're coming from another language, Swift gives you nice type inference and default names for closure arguments ($0 is the first argument). You can probably make the code a little less verbose, but it's a matter of taste which is preferred.

class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
    return Array(Set(jsonArray.flatMap { CheckIn(json: $0) }))
}

Also consider whether you really want the return value to be an array. If you want the list to always have unique users, it is a bit more efficient to use a Set as your return type and forgo the conversion back to an Array like this:

class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
    return Set(jsonArray.flatMap { CheckIn(json: $0) })
}

Finally, consider whether you really need the users property to be optional. With sequence types, it is often sufficient to use an empty sequence to denote absence of a value. Depending on your situation, this may simplify your code. The final version looks like this:

class UserGroups: NSObject {
    let groupName: String
    let users: Set<CheckIn>

    init(json:JSON) {
        self.groupName = json[Constants.Models.UserGroups.groupName].stringValue
        self.users = UserGroups.getUserGroupsList(jsonArray: json[Constants.Models.UserGroups.users].arrayValue)
    }

    class func getUserGroupsList(jsonArray: [JSON]) -> Set<CheckIn> {
        return Set(jsonArray.flatMap { CheckIn(json: $0) })
    }
}

Maintaining Order

The caveat is that Set does not maintain the order of the items. If the order of the groups does matter, we can use this solution instead:

class func getUserGroupsList(jsonArray: [JSON]) -> [CheckIn] {
    var encountered: Set<CheckIn> = []
    return jsonArray.flatMap { CheckIn(json: $0) }.filter { encountered.update(with: $0) == nil }
}

In this version, we still use a set, but only to maintain a set of items we've encountered already. The update method on a set returns the same value if it's already in the set or returns nil if it's being inserted for the first time. We use that to filter our array to those items being encountered for the first time while adding them to the set of encountered items to filter them out when they are subsequently encountered again.

Upvotes: 0

paulvs
paulvs

Reputation: 12053

Here's a slightly simplified example that shows how to combine groups that have the same group name.

Here is the UserGroup class. users is now a variable (var) because we will be adding elements to groups to combine them.

class UserGroups: NSObject {
    let groupName: String
    var users: [String]?

    init(groupName: String, users: [String]?) {
        self.groupName = groupName
        self.users = users
    }
}

Here are three groups, two of the share the same group name, Blues.

let group1 = UserGroups(groupName: "Blues", users: ["Tom", "Huck", "Jim"])
let group2 = UserGroups(groupName: "Reds", users: ["Jo", "Ben", "Tommy"])
let group3 = UserGroups(groupName: "Blues", users: ["Polly", "Watson", "Douglas"])

Next, we'll put all the groups in an array.

let allGroups = [group1, group2, group3]

Here, we use Swift's reduce function to allow us to reduce the array to only groups with unique group names.

let compacted = allGroups.reduce([UserGroups](), { partialResult, group in

    var dupe = partialResult.filter {$0.groupName == group.groupName }.first
    if let dupeGroup = dupe {
        dupeGroup.users?.append(contentsOf: group.users ?? [])
        return partialResult
    } else {
        var newPartialResult = partialResult
        newPartialResult.append(group)
        return newPartialResult
    }
})

The array is now reduced to unique groups, we print out all the groups and their users with the help of Swift's map function.

print(compacted.map { $0.users })

// Prints [
Optional(["Tom", "Huck", "Jim", "Polly", "Watson", "Douglas"]), 
Optional(["Jo", "Ben", "Tommy"])
]

Upvotes: 3

Related Questions