0

I feel like the answer here should be obvious and I should be able to sort it out myself, but after a full day of struggling I think it makes sense to ask for help. I have an array of structs that I have created by first decoding a JSON data file and then filtering part of its elements into my own model. I need to reorganize the data so that I can use it to populate 3 linked pickers, where a user will choose specific search parameters.

My model looks like this:

    struct Brand: Hashable, Comparable {
    static func == (lhs: Brand, rhs: Brand) -> Bool {
        return lhs.name == rhs.name && lhs.models == rhs.models
    }
    
    static func <(lhs: Brand, rhs: Brand) -> Bool {
        return lhs.name < rhs.name
    }
    var name: String
    var models: [Model]

    init(name:String, models:[Model]) {
        self.name = name
        self.models = models
    }
}

struct Model: Hashable, Comparable {
    static func == (lhs: Model, rhs: Model) -> Bool {
        return lhs.name == rhs.name && lhs.years == rhs.years
    }
    
    static func <(lhs: Model, rhs: Model) -> Bool {
        return lhs.name < rhs.name
    }
    var name: String
    var years: [String]

    init(name:String, years:[String]) {
        self.name = name
        self.years = years
    }
}

After passing in data from the JSON and doing some reorganizing (i.e. removing duplicates) I now have an array of 50 to 100 structs. Currently multiple structs repeat the same Brand and then have ONE distinct Model. I need to reorganize it so that each struct has only a SINGLE brand, but within it there is an array of models. The data structure is obviously set up for this. Here's what the data currently looks like (from a console dump):

▿ 5 elements
▿ PickerTesting.Brand
    - name: "Cannondale"
    ▿ models: 1 element
    ▿ PickerTesting.Model
        - name: "SystemSix Carb"
        ▿ years: 1 element
        - "2020"
▿ PickerTesting.Brand
    - name: "Cannondale"
    ▿ models: 1 element
    ▿ PickerTesting.Model
        - name: "SuperX Wmn\'s"
        ▿ years: 1 element
        - "2020"
▿ PickerTesting.Brand
    - name: "Cannondale"
    ▿ models: 1 element
    ▿ PickerTesting.Model
        - name: "Synapse Carb"
        ▿ years: 1 element
        - “2020"
PickerTesting.Brand
    - name: "Pinarello"
    ▿ models: 1 element
    ▿ PickerTesting.Model
        - name: "Razha"
        ▿ years: 1 element
        - "2021"
▿ PickerTesting.Brand
    - name: "Pinarello"
    ▿ models: 1 element
    ▿ PickerTesting.Model
        - name: "Angliru"
        ▿ years: 1 element
        - "2021"

AND I NEED TO GET IT TO LOOK LIKE THIS:

▿ 2 elements
▿ PickerTesting.Brand
    - name: "Cannondale"
    ▿ models: 3 element
    ▿ PickerTesting.Model
        - name: "SystemSix Carb”, “SuperX Wmn”, “Synapse Carb"
        ▿ years: 1 element
        - “2020"
PickerTesting.Brand
    - name: "Pinarello"
    ▿ models: 2 element
    ▿ PickerTesting.Model
        - name: “Razha”, “Angliru"
        ▿ years: 1 element
        - "2021"

I've been working to try to loop through the items with the same brand name, copy the Models(and thus years) into an empty array, and then at the end of repeated Brand names, append the Models to the single brand. That logic still seems right to me and I've been able to combine some models under one brand. But my code is an absolute mess: what I have now keeps repeating over the same Brands and produce repeat structs with multiple models. Worse still, my previously successful use of Array(Set(Data)) to remove duplicates fails. Here's my current function which I'm embarrassed to show here – but in the interests of learning. The counter is obviously wrong, but my attempt to use for index in range loops led to either infinite loops or 10s of thousands of items output. I'm still not sure why a counter value of only 10 will produce SO MANY results, but the results never move past the first two Brands (there are about 10); instead, they just keep repeating results there. Note: in the code below the uglyData taken as an argument to this function is my current array of structs, as described above.

I apologize that this isn't more succinct, and I would be enormously grateful for any guidance. To repeat, I feel like this just shouldn't be too hard, but I'm at my whit's end.

func combineSort (uglyData: [Brand]) -> [Brand] {
    var cleanData: [Brand] = []
    var newModels = [Model]()
    var counter = 0
        while counter < 10 {
            if uglyData[counter].name == uglyData[counter+1].name {
                newModels.append(contentsOf: uglyData[counter].models)
                }
            else if uglyData[counter].name != uglyData[counter+1].name {
                cleanData.append(Brand(name: uglyData[counter].name, models: newModels))
                newModels = []
                }
            counter += 1
        }
    return cleanData
}

As Leo Dabus requested, here's the function that parses the JSON data the first time through. Probably the better solution is to write one function to go from JSON to the form I need. I'm working on that now, but not necessarily close to success.

func getBrandsAll() -> [Brand] {
var brands: [Brand] = []
let allFrames = frameData
    for i in 0..<allFrames.count {
        brands.append(Brand(name: allFrames[i].brand, models: 
        [Model(name:  allFrames[i].model, years: [allFrames[i].year.description])]))
    }
    let unique = Array(Set(brands))
    let sorted = unique.sorted()
    return sorted
}
1
  • 1
    Post your original json response Commented Aug 5, 2020 at 2:09

2 Answers 2

1

I would, do that directly in the parsing of the JSON function. By maybe using a dictionary with the brand name as key and an array of models as value. hence when you parse your json, you can check if the key exists if it doesn't exist you add it to the dictionary with an empty array, or an array with the first model.

In the end you can convert each element of your dictionary into a Brand

var brands: [String:[Model]] = []
let allFrames = frameData
var currentBrand:String
for i in 0..<allFrames.count {
    currentBrand = allFrames[i].brand
    if(brands[currentBrand] == nil)
    {
        brands[currentBrand] = [Model(name:  allFrames[i].model, years: [allFrames[i].year.description])]
    }
    else
    {
    brands[currentBrand]?.append(Brand(name: allFrames[i].brand, models: 
    [Model(name:  allFrames[i].model, years: [allFrames[i].year.description])]))
    }
}    

Then you can go over this dictionary and create you Brand object

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you! Super helpful, and what you suggest resonates completely with suggestions and help I got late last night from a friend with 25 years of coding experience.
I’m glad I could help! If it solves your problem you can accept the answer. :)
Yes, of course. I didn't click accept yet, because I want to try to implement it first (about to work on that now) and as someone very new to programming, even simple things often take me a while.
– your answer definitely pointed in the right direction, but at least with my limited skills, I had some trouble implementing it. It started with Model as the value in the dictionary, but that needs to be array. And the original data has tons of duplicate listing for models, so your else close create a lot of duplicates. After a lot of struggle and lots of help from my much smarter and more knowledgeable friend, I have a working answer. I'll post it now. Thanks again for your help.
Well the dictionary is just an intermediary storage to simplify the code of adding model to brands. If you don't want duplicates instead of using an array inside of the dictionary you could use a set (where every element is unique).
|
0

Answering my own question, after a lot of help from a good friend with way more knowledge than I, 25 years of coding experience, and a lot of patience. Thanks also to La pieuvre for help. The result takes a similar start to his suggestion, but it uses a second dictionary to store Brand/Model and an index location. I also changed the data structure to make the years a Set rather than array. This way neither Brand nor Model are duplicated, and while years are, the duplicates don't get added to the set.

Here's the code:

    func getBrandsAll() -> [Brand] {
    var brands: [String : Brand] = [:]
    var modelHelper: [String : [String : Int]] = [:]
    let allFrames = frameData
    
    for i in 0..<allFrames.count {
        let brand = allFrames[i].brand
        if brands[brand] == nil {
            brands[brand] = Brand(name: brand,
                models: [Model(name: allFrames[i].model, years: [allFrames[i].year])])

            modelHelper[brand] = [allFrames[i].model : 0]
        }
        else if modelHelper[brand]?[allFrames[i].model] == nil {
            brands[brand]?.models.append(Model(name: allFrames[i].model,
                years: [allFrames[i].year]))

            modelHelper[brand]?[allFrames[i].model] = (brands[brand]?.models.count)! - 1
        }
        else {
            let modelIdx = modelHelper[brand]?[allFrames[i].model] ?? 0
            brands[brand]?.models[modelIdx].years.insert(allFrames[i].year)
        }
    }
    var brandslist: [Brand] = []
        for (_,value) in brands {
            brandslist.append(value)
            }
    return brandslist.sorted()
}

The results are data that look like this:

▿ PickerTesting.Brand
- name: "Lynskey"
▿ models: 5 elements
    ▿ PickerTesting.Model
    - name: "Roadback Touring"
    ▿ years: 1 member
        - 2020
    ▿ PickerTesting.Model
    - name: "R300 Road"
    ▿ years: 2 members
        - 2019
        - 2020
    ▿ PickerTesting.Model
    - name: "R500 Disc"
    ▿ years: 1 member
        - 2020
    ▿ PickerTesting.Model
    - name: "GR Race Gravel"
    ▿ years: 1 member
        - 2020
    ▿ PickerTesting.Model
    - name: "GR300 Gravel"
    ▿ years: 1 member
        - 2020

3 Comments

If I may do a last remark, I would suggest you to store at the beginning of your loop properties for brand, model and years to simplify the readability of your code like this one let brand = allFrames[i].brand'. Hence you will be able to remove all the allFrames[i] that pollute your code making it smaller and easier to read. and understand.
Also you might want to separate the logic of parsing the json and returning the array. Because every time you will want to get the array you will re-parse the json.
GREAT suggestion about making the code more readable. I'll do that. As for the JSON file. I'm not parsing it in this function. It gets parsed once when the program loads, and that's then loaded into frameData, which is read by other functions.

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.