Freek Sanders
Freek Sanders

Reputation: 1257

Simple way to replace an item in an array if it exists, append it if it doesn't

Swift 4.2

I have multiple functions that replace an object or struct in an array if it exists, and if it does not exist, it adds it.

func updateFruit(_ fruit: Fruit)
{
    if let idx = fruitArray.firstIndex(where: { $0.id == fruit.id })
    {
        fruitArray[idx] = fruit
    }
    else
    {
        fruitArray.append(fruit)
    }
}

Obviously I could make this into extension on Array:

extension Array
{
    mutating func replaceOrAppend(_ item: Element, whereFirstIndex predicate: (Element) -> Bool)
    {
        if let idx = self.firstIndex(where: predicate)
        {
            self[idx] = item
        }
        else
        {
            append(item)
        }
    }
}

However, is there a simpler, easier way of expressing this? Preferably using a closure or build-in function.

NOTE: current implementation does not allow using a set.

Upvotes: 3

Views: 6813

Answers (3)

Rob Napier
Rob Napier

Reputation: 299355

Given your use case, in which you're always checking $0.<prop> == newthing.<prop>, you can lift this a little more by adding:

mutating func replaceOrAppend<Value>(_ item: Element, 
                                     firstMatchingKeyPath keyPath: KeyPath<Element, Value>)
    where Value: Equatable
{
    let itemValue = item[keyPath: keyPath]
    replaceOrAppend(item, whereFirstIndex: { $0[keyPath: keyPath] == itemValue })
}

You can then use it like:

struct Student {
    let id: Int
    let name: String
}

let alice0 = Student(id: 0, name: "alice")
let alice1 = Student(id: 1, name: "alice")
let bob = Student(id: 0, name: "bob")

var array = [alice0]

array.replaceOrAppend(alice1, firstMatchingKeyPath: \.name) // [alice1]
array.replaceOrAppend(bob, firstMatchingKeyPath: \.name)    // [alice1, bob]

And of course if you do this a lot, you can keep lifting and lifting.

protocol Identifiable {
    var id: Int { get }
}

extension Student: Identifiable {}

extension Array where Element: Identifiable {
    mutating func replaceOrAppendFirstMatchingID(_ item: Element)
    {
        replaceOrAppend(item, firstMatchingKeyPath: \.id)
    }
}

array.replaceOrAppendFirstMatchingID(alice0) // [alice1, alice0]

Upvotes: 4

GetSwifty
GetSwifty

Reputation: 7746

Assuming your Types are Equatable, this is a generic extension:

extension RangeReplaceableCollection where Element: Equatable {

    mutating func addOrReplace(_ element: Element) {
        if let index = self.firstIndex(of: element) {
            self.replaceSubrange(index...index, with: [element])
        }
        else {
            self.append(element)
        }
    }
}

Though, keep in mind my (and your) function will only replace one of matching items.

Full Working playground test:

Playgrounds Test

Upvotes: 1

Taras Chernyshenko
Taras Chernyshenko

Reputation: 2829

I can suggest to create protocol Replacable with replaceValue that will represent identifier which we can use to enumerate thru objects.

protocol Replacable {
    var replaceValue: Int { get }
}

now we can create extension to Array, but now we can drop predicate from example code like this

extension Array where Element: Replacable {
    mutating func replaceOrAppend(_ item: Element) {
        if let idx = self.firstIndex(where: { $0.replaceValue == item.replaceValue }) {
            self[idx] = item
        }
        else {
            append(item)
        }
    }
}

Since Set is not ordered collection, we can simply remove object if set contains it and insert new value

extension Set where Element: Replacable {
    mutating func replaceOrAppend(_ item: Element) {
        if let existItem = self.first(where: { $0.replaceValue == item.replaceValue }) {
            self.remove(existItem)
        }
        self.insert(item)
    }
}

Upvotes: 3

Related Questions