Koh
Koh

Reputation: 2897

Remove last elements of a collection while a condition in Swift

I am attempting to remove "" & " " from the back of a string array until the last item contains some text, but my implementation isn't picking up " ".

My implementation so far:

var array = ["A", "B", "", "C", "D", " ", " ", ""]

while true {
    if (array.last == " " || array.last == "") {
        array.removeLast()
    } else {
        break
    }
}

My desired output is

["A", "B", "", "C", "D"]

, but my current output is

["A", "B", "", "C", "D", " ", " "]

, where the while loop simply breaks after encountering " "

Any advice why is it not picking up the " "?

Upvotes: 5

Views: 2180

Answers (6)

Cristik
Cristik

Reputation: 32863

For completeness, here's a one liner, functional programming style, extension, that removes the last elements that satisfy the given criteria:

extension Collection {
    func dropLast(while predicate: (Element) -> Bool) -> SubSequence {
        indices.reversed().first { !predicate(self[$0]) }.map(prefix(through:)) ?? prefix(0)
    }
}

Same code, but in a more imperative style:

extension Collection {
    func dropLast(while predicate: (Element) -> Bool) -> SubSequence {
        if let lastValidIndex = indices.reversed().first(where: { !predicate(self[$0]) }) {
            return prefix(through: lastValidIndex)
        } else {
            return prefix(0)
        }
    }
}

Upvotes: 0

Leo Dabus
Leo Dabus

Reputation: 236458

I don't know why they have drop(while:) and did not implement dropLast(while:). The implementation bellow works on any collection:

extension Collection {
    func dropLast(while predicate: (Element) throws -> Bool) rethrows -> SubSequence {
        guard let index = try indices.reversed().first(where: { try !predicate(self[$0]) }) else {
            return self[startIndex..<startIndex]
        }
        return self[...index]
    }
}

"123".dropLast(while: \.isWholeNumber)    // ""
"abc123".dropLast(while: \.isWholeNumber) // "abc"
"123abc".dropLast(while: \.isWholeNumber) // "123abc"

And extending RangeReplaceableCollection we can implement remove(while:) and removeLast(while:) as well:

extension RangeReplaceableCollection {
     mutating func remove(while predicate: (Element) throws -> Bool) rethrows {
        guard let index = try indices.first(where: { try !predicate(self[$0]) }) else {
            removeAll()
            return
        }
        removeSubrange(..<index)
    }
    mutating func removeLast(while predicate: (Element) throws -> Bool) rethrows {
        guard let index = try indices.reversed().first(where: { try !predicate(self[$0]) }) else {
            removeAll()
            return
        }
        removeSubrange(self.index(after: index)...)
    }
}

var string = "abc123"
string.removeLast(while: \.isWholeNumber)
string  // "abc"

var string2 = "abc123"
string2.remove(while: \.isLetter)
string2 // "123"

var array = ["A", "B", "", "C", "D", " ", " ", ""]
array.removeLast { $0 == "" || $0 == " " }
array  // ["A", "B", "", "C", "D"]

Upvotes: 5

staticVoidMan
staticVoidMan

Reputation: 20274

Just for fun, lets extend Array with this functionality in a generic way while also externally providing the condition for more flexibility.

Similar to Arrays having a drop(while:), we can make a dropLast(while:) like so:

extension Array {
    func dropLast(while handler: (Element)->Bool) -> Array {
        var array = self
        while let last = array.last, handler(last) {
            array.removeLast()
        }
        return array
    }
}

Usage Example:

let array = ["", "A", "B", "", "C", "D", " ", " ", ""]
let modified = array.dropLast { $0.trimmingCharacters(in: .whitespaces).isEmpty }
print(modified) //["", "A", "B", "", "C", "D"]

Bonus:

It can handle other types of arrays too, and since the condition is not baked into the functionality, it's flexible and reusable.

let array = [0, 1, 2, 3, 0, 5, 6, 7, 0, -1, 0, -2]

//Drop (from tail) all numbers less than 1
let modified = array.dropLast(while: { (val) -> Bool in
    return val < 1
})
print(modified) //[0, 1, 2, 3, 0, 5, 6, 7]

Upvotes: 4

Gustavo Vollbrecht
Gustavo Vollbrecht

Reputation: 3256

Move your condition to while and make sure you're checking on the correct array after the operation.

var array = ["A", "B", "", "C", "D", " ", " ", ""]

while array.last == " " || array.last == "" {
    array.removeLast()
}

print(array) // ["A", "B", "", "C", "D"]

Upvotes: 4

DarkDust
DarkDust

Reputation: 92384

One way to solve this is to reverse the collection (which is done lazily) and drop the unwanted items until you encounter the wanted ones. Afterwards, reverse the collection again.

let array = ["A", "B", "", "C", "D", " ", " ", ""]

let filtered = array.reversed().drop(while: {
    $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}).reversed() as [String]

print(filtered) // "["A", "B", "", "C", "D"]\n"

Note that the check for " " may fail if it's not a normal space, for example a non-breaking space (Unicode checkpoint U+00A0). This may be the issue you're having in the first place. So trim the string (it removes characters from the start and end only) and check whether the result is an empty string.

Upvotes: 4

ngbaanh
ngbaanh

Reputation: 535

Basically your solution plays fine. But you can make it more generic for "", " ", "", " ", .... :

import Foundation
var array = ["A", "B", "", "C", "D", " ", " ", ""]
while true {
    let shouldRemoveLast = array.last?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? false
    if (shouldRemoveLast) { array.removeLast() } else { break }
}

Upvotes: 1

Related Questions