scaryguy
scaryguy

Reputation: 7960

How to group a dictionary array depending on a unique property value?

This is what I have, an array of dictionary response I fetched from an API:

var response = [
["title": "First one", "code": "the_first", "category": "Entertainment category"], 
["title": "Second one", "code": "the_second", "category": "Entertainment category"], 
["title": "Third one", "code": "the_third", "category": "News category"], 
["title": "Fourth one", "code": "the_fourth", "category": "News category"], 
["title": "Fifth one", "code": "the_fifth", "category": "Children category"], 
["title": "Sixth one", "code": "the_sixth", "category": "Children category"]
]

What I need to have is:

var channels_by_category = [
["title":"Entertainment category", 
"channels":[
["title": "First one", "code": "the_first", "category": "Entertainment category"],
["title": "Second one", "code": "the_second", "category": "Entertainment category"]
]],

["title":"News category", 
"channels":[
"title": "Third one", "code": "the_third", "category": "News category"],
["title": "Fourth one", "code": "the_fourth", "category": "News category"]
]]
],

["title":"Children category", 
"channels":[
["title": "Fifth one", "code": "the_fifth", "category": "Children category"],
["title": "Sixth one", "code": "the_sixth", "category": "Children category"]
]]
]

In JavaScript world, I'd use lodash to get over this problem. However, I'm not experienced enough in Swift. I guess that it should be some kind of combination of for loop and reduce but I couldn't figure out how to do it.

How to achieve what I want to get?

Thank you

Upvotes: 1

Views: 689

Answers (3)

Luca Angeletti
Luca Angeletti

Reputation: 59536

Since a good answer to your question has already been provided I am suggesting here a different solution

You should not use dictionaries for your data model. This solutions shows you how to use a struct.

Show

first of all lets define a struct to represent your data model

struct Show {
    let title: String
    let code: String
    let category: String

    init?(dict:[String:String]) {
        guard let
            title = dict["title"],
            code = dict["code"],
            category = dict["category"]
            else { return nil }
        self.title = title
        self.code = code
        self.category = category
    }
}

Organising your data

Next let's convert your raw data into a list of Show(s)

let shows = response.flatMap(Show.init)

Grouping

And finally let's group the list by category

let categories: [String:[Show]] = shows.reduce([String:[Show]]()) { (categories, show) -> [String:[Show]] in
    var categories = categories
    categories[show.category] = (categories[show.category] ?? []) + [show]
    return categories
}

Now categories has your category name as key and a list of Show(s) as value.

Update 1: Iterating over a list of Show(s)

if let shows = categories["News category"] {
    for show in shows {
        print(show.title)
    }
}

or

categories["News category"]?.forEach { show in
    print(show.title)
}

Update 2: iterating over the categories

for elm in categories {
    let category = elm.0
    let shows = elm.1

    print("Category \(category) has \(shows.count) shows")
    shows.forEach { show in
        print(" - \(show.title)")
    }
}

Result

Category News category has 2 shows
 - Third one
 - Fourth one
Category Entertainment category has 2 shows
 - First one
 - Second one
Category Children category has 2 shows
 - Fifth one
 - Sixth one

Upvotes: 3

larva
larva

Reputation: 5148

try following code:

var response = [
    ["title": "First one", "code": "the_first", "category": "Entertainment category"],
    ["title": "Second one", "code": "the_second", "category": "Entertainment category"],
    ["title": "Third one", "code": "the_third", "category": "News category"],
    ["title": "Fourth one", "code": "the_fourth", "category": "News category"],
    ["title": "Fifth one", "code": "the_fifth", "category": "Children category"],
    ["title": "Sixth one", "code": "the_sixth", "category": "Children category"]
]

// Step1 convert array to dictionary
var categories = [String:[[String:String]]]()
for item in response {
    if let category = item["category"] {
        var channels = [[String:String]]()
        if let resultCategory = categories[category] {
            channels = resultCategory
        }
        channels.append(item)
        categories[category] = channels
    }
}

// Step2 get result
var result = [[String:Any]]()
for item in categories.keys {
    let value = categories[item]!
    result.append(["title": item, "channels":[value]])
}

result:

[["title": "News category", "channels": [[["title": "Third one", "code": "the_third", "category": "News category"], ["title": "Fourth one", "code": "the_fourth", "category": "News category"]]]], ["title": "Entertainment category", "channels": [[["title": "First one", "code": "the_first", "category": "Entertainment category"], ["title": "Second one", "code": "the_second", "category": "Entertainment category"]]]], ["title": "Children category", "channels": [[["title": "Fifth one", "code": "the_fifth", "category": "Children category"], ["title": "Sixth one", "code": "the_sixth", "category": "Children category"]]]]]

Upvotes: 0

sam-w
sam-w

Reputation: 7687

Stage 1, separating your results into categories:

You have a couple of options for how to do this: the reduce option does reduce (pun intended) mutable state in your code, but it's a much bigger performance hit, and less readable IMO. I would go for the forEach if it was me.

Option 1 - Mutable dict, forEach:

var response = [
    ["title": "First one", "code": "the_first", "category": "Entertainment category"],
    ["title": "Second one", "code": "the_second", "category": "Entertainment category"],
    ["title": "Third one", "code": "the_third", "category": "News category"],
    ["title": "Fourth one", "code": "the_fourth", "category": "News category"],
    ["title": "Fifth one", "code": "the_fifth", "category": "Children category"],
    ["title": "Sixth one", "code": "the_sixth", "category": "Children category"]
]

typealias ChannelsByCategory = [String: [[String: String]]]

var categories = ChannelsByCategory()
response.forEach { channel in
    guard let channelCategory = channel["category"] else {
        return
    }

    if var category = categories[channelCategory] {
        category.append(channel)
    } else {
        categories[channelCategory] = [channel]
    }
}

Option 2 - reduce:

let categories = response.reduce(ChannelsByCategory()) { dict, channel in
    guard let channelCategory = channel["category"] else {
        return dict
    }

    let channels = [channel] + (dict[channelCategory] ?? [])
    var modifiedDict = dict
    modifiedDict[channelCategory] = channels
    return modifiedDict
}

Stage 2 - formatting the categories into your dictionary structure:

typealias ResultDict = [[String: Any]]
let result: ResultDict = categories.map { name, channels in
    return ["title": name, "channels": channels]
}

Upvotes: 0

Related Questions