Combine Duplicate Items in an Array

I have an object called Item with two attributes, name (String) and value (Double). Given an array of Items I need combine the values of all items with the same name and keep the items with no duplicates. For example, say there were 4 items in the array and two of them named "Test" and the others "Object" and "Item". "Object" and "Item" would remain in the list, but the values of the two "Test"s would be combined into one item with the same name "Test".

I've included the following code for a visual representation.

Delete the comments as you read them to clean up. They're just there to clear up any confusion. Leave a comment if you have any questions. Thanks for the help!

Content View:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjContext
    @ObservedObject var persistence = PersistenceController.shared
    @State private var items = PersistenceController.shared.getItems()
    
    @State var isAddViewShowing = false
    var body: some View {
        NavigationView{
            List{
                Section{
                    ForEach(items) { item in //Displays the list of items
                        HStack{
                            Text(String(item.name!))
                            Spacer()
                            Text(String(Int(item.value)))
                        }
                    }
                    .onDelete(perform: { indexSet in
                        deleteItem(indexSet: indexSet)
                    })
                }
            }
            .navigationBarTitle("Items")
            .navigationBarItems(leading: combineItemsButton, trailing: addButton)
            .sheet(isPresented: $isAddViewShowing){ //displays the view to add an item
                AddView()
                    .onDisappear(perform: {
                        items = persistence.getItems() //"refreshes" the list of items
                    })
            }
        }
    }
    
    var combineItemsButton: some View{
        Button(action:{
            //combine duplicates here
            persistence.contextSave()
            items = persistence.getItems()
        }){
            Text("Combine Duplicates")
                .bold()
        }
    }
    
    var addButton: some View{
        Button(action:{
            isAddViewShowing.toggle()
        }){
            Text("Add Item")
                .bold()
        }
    }
    
    func deleteItem(indexSet: IndexSet){
        withAnimation{
            indexSet.map {
                items[$0]
            }
            .forEach(managedObjContext.delete)
            
            persistence.contextSave()
            items = persistence.getItems()
        }
    }
}

Add View:

struct AddView: View{
    @Environment(\.dismiss) var dismiss
    @ObservedObject var persistence = PersistenceController.shared
    
    @State var name: String = ""
    @State var value = ""
    @State private var alertMessage = ""
    @State private var showAlert = false
    var body: some View{
        NavigationView{
            Form{
                TextField("Item Name", text: $name)
                TextField("Item Value", text: $value)
                    .keyboardType(.decimalPad)
            }
            .navigationBarTitle("Add Item")
            .navigationBarItems(leading: dismissButton, trailing: submitButton)
        }
    }
    var submitButton: some View{
        Button(action: {
            if (name == ""){ //ensures the item has a name
                alertMessage="Your recipe needs a name"
                showAlert.toggle()
            } else {
                persistence.addItem(name: name, value: Double(value) ?? 2)
                dismiss()
            }
        }){
            Text("Submit")
                .bold()
        }
        .alert(alertMessage, isPresented: $showAlert){
            Button("OK",role: .cancel){}
        }
    }
    
    var dismissButton: some View{
        Button(action: {
            dismiss()
        }){
            Text("Cancel")
                .bold()
        }
    }
}

Persistence File:

import CoreData

class PersistenceController : ObservableObject{
    static let shared = PersistenceController()
    let container: NSPersistentContainer
    
    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Test")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
    
    func getItems() -> [Item] { //fetches items
        let context = container.viewContext
        var request = NSFetchRequest<Item>()
        request = Item.fetchRequest()
        request.entity = NSEntityDescription.entity(forEntityName: "Item", in: context)
        do {
            let items = try context.fetch(request)
            if items.count == 0 { return []}
            return items.sorted(by: {$0.name! > $1.name!})
        } catch {
            print("**** ERROR: items fetch failed \(error)")
            return []
        }
    }
    
    func addItem(name: String, value: Double){
        let context = container.viewContext
        let item = Item(context: context)
        item.id = UUID()
        item.name = name
        item.value = value
        
        contextSave()
    }
    
    func contextSave() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
                self.objectWillChange.send()
            } catch {
                print("**** ERROR: Unable to save context \(error)")
            }
        }
    }
}

Data Model:

Answered by Claude31 in 718894022

You could do this on your array directly.

Something like this.

struct Item {
  var name: String
  var value: Double
}

var myArray: [Item]  // You load it at some point
myArray = [Item(name: "One", value: 1.0), Item(name: "Two", value: 2.0), Item(name: "One", value: 3.0)]

func itemNameInArray(theArray: [Item], theItem: Item) -> Int? {
    for (i, item) in theArray.enumerated() {
        if item.name == theItem.name {  // May be you should test on lowercased
            return i
        }
    }
    return nil
}

var myFilteredArray = [Item] ()
for item in myArray {
    if let index = itemNameInArray(theArray: myFilteredArray, theItem: item) {    // Already exist
        var newItem = myFilteredArray[index]
        newItem.value += item.value
        myFilteredArray[index] = newItem
    } else {    // new one
        myFilteredArray.append(item)
    }
}

print(myFilteredArray)

This can be optimized.

Create a function in your PersistenceController class to detect and remove duplicates:

func deDup(_ items: [Item]) {
    let context = container.viewContext
    var prevItem : Item?
    for item in items {
         if prevItem == nil {
             prevItem = item
             continue
         }
         if item.name! == prevItem!.name! {
             // this is a duplicate
             prevItem!.value += item.value
             context.delete(item)
         } else {
              prevItem = item
        }
    }
    contextSave()
}

Change you combineItemsButton action to be

Button(action:{
            //combine duplicates here
            persistence.deDup(items)
            items = persistence.getItems()

I have not tested this within any app (i.e. just written the code here) and have done it in a hurry, but I think that the logic is correct.

I hope it works!!!! Regards, Michaela

Accepted Answer

You could do this on your array directly.

Something like this.

struct Item {
  var name: String
  var value: Double
}

var myArray: [Item]  // You load it at some point
myArray = [Item(name: "One", value: 1.0), Item(name: "Two", value: 2.0), Item(name: "One", value: 3.0)]

func itemNameInArray(theArray: [Item], theItem: Item) -> Int? {
    for (i, item) in theArray.enumerated() {
        if item.name == theItem.name {  // May be you should test on lowercased
            return i
        }
    }
    return nil
}

var myFilteredArray = [Item] ()
for item in myArray {
    if let index = itemNameInArray(theArray: myFilteredArray, theItem: item) {    // Already exist
        var newItem = myFilteredArray[index]
        newItem.value += item.value
        myFilteredArray[index] = newItem
    } else {    // new one
        myFilteredArray.append(item)
    }
}

print(myFilteredArray)

This can be optimized.

Combine Duplicate Items in an Array
 
 
Q