0

Array in Swift has several instance methods for excluding elements, such as dropFirst(), dropLast(), drop(while:), etc. What about drop(at:)?

Note: I'd use remove(at:), but the array I'm working with is a let constant.

ma11hew28
  • 121,420
  • 116
  • 450
  • 651

5 Answers5

2

You can extend RangeReplaceableCollection protocol instead of Array type, this way you can use it on Strings as well:

extension RangeReplaceableCollection {
    func drop(at offset: Int) -> SubSequence {
        let index = self.index(startIndex, offsetBy: offset, limitedBy: endIndex) ?? endIndex
        let next = self.index(index, offsetBy: 1, limitedBy: endIndex) ?? endIndex
        return self[..<index] + self[next...]
    }
}

var str = "Hello, playground"
str.drop(at: 5)  // "Hello playground"

let numbers = [1, 2, 3, 4, 5]
print(numbers.drop(at: 2))  // "[1, 2, 4, 5]\n"

If you would like to accept also String.Index in your method:

extension RangeReplaceableCollection {
    func drop(at index: Index) -> SubSequence {
        let index = self.index(startIndex, offsetBy: distance(from: startIndex, to: index), limitedBy: endIndex) ?? endIndex
        let next = self.index(index, offsetBy: 1, limitedBy: endIndex) ?? endIndex
        return self[..<index] + self[next...]
    }
}

var str = "Hello, playground"
str.drop(at: 0)               // "ello, playground"
str.drop(at: str.startIndex)  // "ello, playground"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Great idea! However, I would make two separate extensions (one for `String` and one for `Array`), each with its own `drop(at:)` instance method. Why? Because this is how `remove(at:)` is implemented in Swift. It's defined separately for each structure (`String` & `Array`). Also, [`remove(at:)` for `String`](https://developer.apple.com/documentation/swift/string/3018532-remove) takes a `String.Index`, not an `Int`. So, I would have `drop(at:)` also take a `String.Index`, not an `Int`, to keep things consistent. – ma11hew28 Sep 17 '18 at 12:39
  • 1
    Anyway, nice work! I think they should consider adding our instance methods to the [Swift standard library](https://developer.apple.com/documentation/swift/swift_standard_library). – ma11hew28 Sep 17 '18 at 12:39
  • 1
    Nice! Thank you. :-) – ma11hew28 Sep 19 '18 at 16:39
2

Note: I'd use remove(at:), but the array I'm working with is a constant.

I wouldn't let that stop you:

extension Array {
    func drop(at:Int) -> [Element] {
        var temp = self
        temp.remove(at: at)
        return temp
    }
}
let numbers = [1, 2, 3, 4, 5]
print(numbers.drop(at: 2)) // [1, 2, 4, 5]
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Interesting. Thanks! Yeah, this was actually my initial approach. Looking at your code, I just thought of something: Is the array returned by your `drop(at:)` method mutable, since `temp` is a variable? That is, could I do `numbers.drop(at: 2).remove(at: 1)`? Probably not a great example, but you get what I mean. I'm just curious. I guess I could try it out. :-) – ma11hew28 Sep 17 '18 at 12:20
  • OK. I just copy & pasted your extension into the [Swift REPL](https://developer.apple.com/swift/blog/?id=18) and tried `numbers.drop(at: 2).remove(at: 1)` and got back `error: cannot use mutating member on immutable value: function call returns immutable value`. So, I guess the array returned by your `drop(at:)` method is immutable, even though `temp` is a variable. – ma11hew28 Sep 17 '18 at 12:22
  • 2
    **1)** This doesn't return `ArraySlice` like the other drop functions in the standard library. **2)** This is inefficient since it creates a whole new array in memory – ielyamani Sep 17 '18 at 12:50
  • @Carpsen90 I see no such requirement in the question. The only thing I was responding to here was the line I quoted, "I'd use `remove(at:)`, but the array I'm working with is a constant". Well, `remove(at:)` doesn't return a slice and it does create a whole new array in memory. So my answer here does just what it does, plus it shows how to deal with the original array being a constant. – matt Sep 17 '18 at 14:10
1

How about using a Swift extension to add drop(at:) to the Array structure?

extension Array {
  func drop(at index: Int) -> ArraySlice<Element> {
    precondition(indices.contains(index), "Index out of range")
    return self[..<index] + self[(index+1)...]
  }
}

It returns a slice of the original array without the element at the specified index.

let numbers = [1, 2, 3, 4, 5]
print(numbers.drop(at: 2))
// Prints "[1, 2, 4, 5]"

Note: You may also want to add drop(at:) to ArraySlice.

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • 1
    This looks perfect to me. Very minor improvement : `precondition(index < endIndex && index >= startIndex, "Index out of range")` to use [short-circuit evaluation](https://stackoverflow.com/a/52254381/2907715), since it is usually the first condition that fails – ielyamani Sep 17 '18 at 13:58
  • `return prefix(upTo: index) + suffix(from: index+1)` looks cleaner IMO. – matt Sep 17 '18 at 16:51
  • @Carpsen90, thank you. Great point. I initially wrote `index >= startIndex && index < endIndex`, but I'm not sure if I'd remember why I reversed the order of the comparisons, so maybe I'd want to also add a comment. I think writing `startIndex <= index && index < endIndex` is a bit more clear & semantic. Albeit, maybe it's not as performant, but the performance hit would only happen if it's about to crash. Haha. Ultimately, I think I'll use `indices.contains(index)` as you did in your answer. I was unfamiliar with it at first, but it's simple & readable, and seems fast enough. – ma11hew28 Sep 18 '18 at 18:06
  • @matt I hear you, especially inside of an instance function where the subscript notation requires the use of `self`. Whichever you prefer. I think I'll stick with the subscript notation to follow the suggestion in the documentation and for consistency, because that's probably what I'll use elsewhere. And, changing the ranges (that I had before) to partial ranges makes it look a little better, so thank you for that. :-) – ma11hew28 Sep 18 '18 at 18:17
  • Thank you, @Carpsen90 & @matt, for your great feedback & suggestions. I appreciate both of you for your attention to detail, clarity, and performance. I've learned a lot, and now, I have a better implementation of `drop(at:)`. :-) – ma11hew28 Sep 18 '18 at 18:19
  • I just submitted an issue at [bugs.swift.org](https://bugs.swift.org) suggesting that this method be added to the Swift standard library: [SR-8792: Add drop(at:)](https://bugs.swift.org/browse/SR-8792). – ma11hew28 Sep 18 '18 at 19:30
1

The only thing I would add to your implementation is a guard statement with a useful error message:

extension Array {
    func drop(at index: Int) -> ArraySlice<Element> {
        guard indices.contains(index) else {
            fatalError("Index out of range")
        }
        return self[0..<index] + self[(index+1)..<endIndex]
    }

    func drop(range: CountableRange<Int>) -> ArraySlice<Element> {
        guard (indices.lowerBound, range.upperBound) <= (range.lowerBound, indices.upperBound) else {
            fatalError("Range is out of the indices bounds")
        }
        return self[0..<range.lowerBound] + self[range.upperBound..<endIndex]
    }
}

let a = [1,2,3]
a.drop(at: 3)          //Fatal error: Index out of range
let b = [0,1,2,3,4,5]
b.drop(range: 1..<5)   //[0, 5]
ielyamani
  • 17,807
  • 10
  • 55
  • 90
  • 1
    Nice call. I just found the [source code for `remove(at:) for Array](https://github.com/apple/swift/blob/master/stdlib/public/core/Array.swift#L1199-L1226), which also does what you suggest. – ma11hew28 Sep 17 '18 at 13:02
  • @ma11hew28 Is [this](https://developer.apple.com/documentation/swift/rangereplaceablecollection/3018293-removesubrange) what you're looking for? Or do you want to conform to the way excluding functions have `ArraySlice` as a return value? – ielyamani Sep 17 '18 at 13:18
  • Thank you. I didn't know about [`removeSubrange(_:)`](https://developer.apple.com/documentation/swift/rangereplaceablecollection/3018293-removesubrange). Pretty cool! Well, I just want to remove one element, not a subrange of elements. And, `removeSubrange(_:)` doesn't work on constants. For example, if I do `numbers.removeSubrange(2...2)`, I get `error: cannot use mutating member on immutable value: 'numbers' is a 'let' constant`. And yes, I do want `ArraySlice` as the return value. Your `drop(at:)` function is exactly what I want. – ma11hew28 Sep 17 '18 at 13:42
  • Thank you for suggesting to add an index-out-of-range error message. I just added it to my answer as well. – ma11hew28 Sep 17 '18 at 13:43
  • @ma11hew28 I've just found out a weird behavior of drop: `var numbers: [Int] = []; numbers.drop(at: -4)` returns `ArraySlice([])` without any error message, maybe it's because it's not a mutating func like `remove(at:)` – ielyamani Sep 17 '18 at 13:51
  • Weird. Thank you, @Carpsen90. I'm not sure which implementation of `drop(at:)` you're referring to in your last comment, but the implementation in my answer (which I've since updated) seems to work correctly for all cases. – ma11hew28 Sep 18 '18 at 12:49
  • @ma11hew28 Nevermind, apparently I was using the code from @LeoDabus's answer, or something. Here is what needs to be fixed: `let numbers: [Int] = []; numbers.dropFirst(); numbers.dropFirst(3); numbers.dropLast(); numbers.dropLast(2)` all return `ArraySlice([])`. I'll ask Ben – ielyamani Sep 18 '18 at 14:02
  • Oh, OK. I think that's how those methods are supposed to work. See the discussion section of the documentation for each method: [`dropFirst()`](https://developer.apple.com/documentation/swift/array/1688121-dropfirst), [`dropFirst(_:)`](https://developer.apple.com/documentation/swift/array/1688675-dropfirst), [`dropLast()`](https://developer.apple.com/documentation/swift/array/1689669-droplast), and [`dropLast(_:)`](https://developer.apple.com/documentation/swift/array/1689751-droplast). – ma11hew28 Sep 18 '18 at 19:11
1

return self[0..<index] + self[index+1..<endIndex]

Ugly. Why not use the tools you're given?

extension Array {
    func drop(at:Int) -> ArraySlice<Element> {
        return prefix(upTo: at) + suffix(from: at+1)
    }
}
let arr = [1,2,3,4,5]
let slice = arr.drop(at:2) // [1,2,4,5]

EDIT It seems Apple would prefer you to say (using partial ranges)

return self[..<at] + self[(at+1)...]

but personally I think that's uglier, and after all the methods I suggested are not deprecated or anything.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Beautiful. Thank you! :-) – ma11hew28 Sep 18 '18 at 01:41
  • I just looked up the documentation for [`prefix(upTo:)`](https://developer.apple.com/documentation/swift/array/1688336-prefix) & [`suffix(from:)`](https://developer.apple.com/documentation/swift/array/1689680-suffix). Each suggests using subscript notation instead, with a [`PartialRangeUpTo`](https://developer.apple.com/documentation/swift/partialrangeupto) instance and a [`PartialRangeFrom`](https://developer.apple.com/documentation/swift/partialrangefrom) instance, respectively, instead of a [`Range`](https://developer.apple.com/documentation/swift/range) instance, like I used. – ma11hew28 Sep 18 '18 at 01:43
  • 1
    Yes, now that there are partial ranges you can do that. I'll add that. – matt Sep 18 '18 at 01:44