Implementing a Collection.removingIndices(_:)

Looking at this thread and the [Pre-pitch] removeAll(at:) thread, maybe we can generalize to:

protocol RangeReplaceableCollection {

    //...

    /// Removes all the elements whose indices satisfy the given predicate.
    ///
    /// Use this method to remove every element in a collection whose location
    /// meets a particular criteria. The order of the remaining elements is
    /// preserved. This example removes all the odd values from an
    /// array of numbers:
    ///
    ///     var numbers = [5, 6, 7, 8, 9, 10, 11]
    ///     numbers.removeAllAt(locations: { $0.isMultiple(of: 2) })
    ///     // numbers == [6, 8, 10]
    ///
    /// - Parameter shouldBeRemoved: A closure that takes an index of the
    ///   collection as its argument and returns a Boolean value indicating
    ///   whether the corresponding element should be removed from the
    ///   collection.
    ///
    /// - Complexity: O(*n*), where *n* is the length of the collection.
    mutating func removeAllAt(locations shouldBeRemoved: (Index) throws -> Bool) rethrows

}

Instead of using a Set, a hopefully sorted Sequence, or an arbitrary Sequence, we express the exclusion set as a predicate. Note that this is a customization point. There are two default implementations:

extension RangeReplaceableCollection {

    /// Removes all the elements whose indices satisfy the given predicate.
    ///
    /// Use this method to remove every element in a collection whose location
    /// meets a particular criteria. The order of the remaining elements is
    /// preserved. This example removes all the vowels from a string:
    ///
    ///     var phrase = "The rain in Spain stays mainly in the plain."
    ///
    ///     let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
    ///     let vowelIndices = Set(phrase.indices.lazy.filter({ vowels.contains(phrase[$0]) }))
    ///     phrase.removeAllAt(locations: { vowelIndices.contains($0) })
    ///     // phrase == "Th rn n Spn stys mnly n th pln."
    ///
    /// - Parameter shouldBeRemoved: A closure that takes an index of the
    ///   collection as its argument and returns a Boolean value indicating
    ///   whether the corresponding element should be removed from the
    ///   collection.
    ///
    /// - Complexity: O(*n*), where *n* is the length of the collection.
    @inlinable public mutating func removeAllAt(locations shouldBeRemoved: (Self.Index) throws -> Bool) rethrows {
        var result = Self()
        result.reserveCapacity(count)
        for i in try indices.lazy.filter({ try !shouldBeRemoved($0) }) {
            result.append(self[i])
        }
        self = result
    }

}

extension RangeReplaceableCollection where Self: MutableCollection {

    /// Removes all the elements whose indices satisfy the given predicate.
    ///
    /// Use this method to remove every element in a collection whose location
    /// meets a particular criteria. The order of the remaining elements is
    /// preserved. This example removes all the odd values from an
    /// array of numbers:
    ///
    ///     var numbers = [5, 6, 7, 8, 9, 10, 11]
    ///     numbers.removeAllAt(locations: { $0.isMultiple(of: 2) })
    ///     // numbers == [6, 8, 10]
    ///
    /// - Parameter shouldBeRemoved: A closure that takes an index of the
    ///   collection as its argument and returns a Boolean value indicating
    ///   whether the corresponding element should be removed from the
    ///   collection.
    ///
    /// - Complexity: O(*n*), where *n* is the length of the collection.
    @inlinable public mutating func removeAllAt(locations shouldBeRemoved: (Self.Index) throws -> Bool) rethrows {
        var latest = startIndex
        for i in try indices.lazy.filter({ try !shouldBeRemoved($0) }) {
            self[latest] = self[i]
            formIndex(after: &latest)
        }
        removeSubrange(latest...)
    }

}

We need a customization point so collections that are not MutableCollection but (at least internally) don't necessarily invalidate indices upon (the first) removal, or can keep track of any shifts for at least one call, can do so in optimized implementations. This is helpful for collections that store elements as collections of collections, where removing elements in one segment doesn't disturb the internal indices of elements in other segments.

1 Like