Skip to content

Internally add Collection.endOfPrefix(while:) and BidirectionalCollection.startOfSuffix(while:) #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions Sources/Algorithms/Chunked.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ extension LazyChunked: LazyCollectionProtocol {
internal func endOfChunk(startingAt start: Base.Index) -> Base.Index {
let subject = projection(base[start])
return base[base.index(after: start)...]
.firstIndex(where: { !belongInSameGroup(subject, projection($0)) })
?? base.endIndex
.endOfPrefix(while: { belongInSameGroup(subject, projection($0)) })
}

@inlinable
Expand Down Expand Up @@ -119,20 +118,8 @@ extension LazyChunked: BidirectionalCollection

// Get the projected value of the last element in the range ending at `end`.
let subject = projection(base[indexBeforeEnd])

// Search backward from `end` for the first element whose projection isn't
// equal to `subject`.
if let firstMismatch = base[..<indexBeforeEnd]
.lastIndex(where: { !belongInSameGroup(projection($0), subject) })
{
// If we found one, that's the last element of the _next_ previous chunk,
// and therefore one position _before_ the start of this chunk.
return base.index(after: firstMismatch)
} else {
// If we didn't find such an element, this chunk extends back to the start
// of the collection.
return base.startIndex
}
return base[..<indexBeforeEnd]
.startOfSuffix(while: { belongInSameGroup(projection($0), subject) })
}

@inlinable
Expand Down
63 changes: 56 additions & 7 deletions Sources/Algorithms/Suffix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,62 @@ extension BidirectionalCollection {
public func suffix(
while predicate: (Element) throws -> Bool
) rethrows -> SubSequence {
let start = startIndex
var result = endIndex
while result != start {
let previous = index(before: result)
guard try predicate(self[previous]) else { break }
result = previous
try self[startOfSuffix(while: predicate)...]
}
}

//===----------------------------------------------------------------------===//
// endOfPrefix(while:)
//===----------------------------------------------------------------------===//

extension Collection {
/// Returns the exclusive upper bound of the prefix of elements that satisfy
/// the predicate.
///
/// - Parameter predicate: A closure that takes an element of the collection
/// as its argument and returns `true` if the element is part of the prefix
/// or `false` if it is not. Once the predicate returns `false` it will not
/// be called again.
///
/// - Complexity: O(*n*), where *n* is the length of the collection.
@usableFromInline
internal func endOfPrefix(
while predicate: (Element) throws -> Bool
) rethrows -> Index {
var index = startIndex
while try index != endIndex && predicate(self[index]) {
formIndex(after: &index)
}
return index
}
}

//===----------------------------------------------------------------------===//
// startOfSuffix(while:)
//===----------------------------------------------------------------------===//

extension BidirectionalCollection {
/// Returns the inclusive lower bound of the suffix of elements that satisfy
/// the predicate.
///
/// - Parameter predicate: A closure that takes an element of the collection
/// as its argument and returns `true` if the element is part of the suffix
/// or `false` if it is not. Once the predicate returns `false` it will not
/// be called again.
///
/// - Complexity: O(*n*), where *n* is the length of the collection.
@usableFromInline
Copy link
Contributor

@karwa karwa Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks really nice, but I think these should be inlinable rather than just usableFromInline. We want the compiler to be able to specialise them for a particular predicate closure (and for generic specialisation for the collection, of course)

internal func startOfSuffix(
while predicate: (Element) throws -> Bool
) rethrows -> Index {
var index = endIndex
while index != startIndex {
let after = index
formIndex(before: &index)
if try !predicate(self[index]) {
return after
}
}
return self[result...]
return index
}
}
18 changes: 3 additions & 15 deletions Sources/Algorithms/Trim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,8 @@ extension BidirectionalCollection {
public func trimming(
while predicate: (Element) throws -> Bool
) rethrows -> SubSequence {
// Consume elements from the front.
let sliceStart = try firstIndex { try predicate($0) == false } ?? endIndex
// sliceEnd is the index _after_ the last index to match the predicate.
var sliceEnd = endIndex

while sliceStart != sliceEnd {
let idxBeforeSliceEnd = index(before: sliceEnd)
guard try predicate(self[idxBeforeSliceEnd]) else {
return self[sliceStart..<sliceEnd]
}
sliceEnd = idxBeforeSliceEnd
}

// Trimmed everything.
return self[Range(uncheckedBounds: (sliceStart, sliceStart))]
let start = try endOfPrefix(while: predicate)
let end = try self[start...].startOfSuffix(while: predicate)
return self[start..<end]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing ❤️

}
}
26 changes: 25 additions & 1 deletion Tests/SwiftAlgorithmsTests/SuffixTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//===----------------------------------------------------------------------===//

import XCTest
import Algorithms
@testable import Algorithms

final class SuffixTests: XCTestCase {
func testSuffix() {
Expand All @@ -23,4 +23,28 @@ final class SuffixTests: XCTestCase {
let empty: [Int] = []
XCTAssertEqualSequences(empty.suffix(while: { $0 > 10 }), [])
}

func testEndOfPrefix() {
let array = Array(0..<10)
XCTAssertEqual(array.endOfPrefix(while: { $0 < 3 }), 3)
XCTAssertEqual(array.endOfPrefix(while: { _ in false }), array.startIndex)
XCTAssertEqual(array.endOfPrefix(while: { _ in true }), array.endIndex)

let empty = [Int]()
XCTAssertEqual(empty.endOfPrefix(while: { $0 < 3 }), 0)
XCTAssertEqual(empty.endOfPrefix(while: { _ in false }), empty.startIndex)
XCTAssertEqual(empty.endOfPrefix(while: { _ in true }), empty.endIndex)
}

func testStartOfSuffix() {
let array = Array(0..<10)
XCTAssertEqual(array.startOfSuffix(while: { $0 >= 3 }), 3)
XCTAssertEqual(array.startOfSuffix(while: { _ in false }), array.endIndex)
XCTAssertEqual(array.startOfSuffix(while: { _ in true }), array.startIndex)

let empty = [Int]()
XCTAssertEqual(empty.startOfSuffix(while: { $0 < 3 }), 0)
XCTAssertEqual(empty.startOfSuffix(while: { _ in false }), empty.endIndex)
XCTAssertEqual(empty.startOfSuffix(while: { _ in true }), empty.startIndex)
}
}