Skip to content

Commit

Permalink
Added longestPrefx(where:) on RandomAccessCollection.
Browse files Browse the repository at this point in the history
  • Loading branch information
wadetregaskis committed Mar 12, 2024
1 parent 7cbcc2d commit 1c03b69
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ A miscellaneous collection of extensions to Apple's Foundation framework.

* `orNilString` returns a `String` describing the contents or the string literal "nil" if the `Optional` is empty. For `Optional<String>` it returns the contained `String` directly (when present), for all other types it uses `String(describing:)`.

### RandomAccessCollection

* `longestPrefix(where:)` determines the longest prefix that matches a given condition (optionally performing a transformation on that prefix, as well), using a binary search.

### StringProtocol

* `quoted` returns a quoted version of the string, with backslash-escaping for existing quotes and backslashes in the string. e.g. `#"Hello, "Alex" \ "Alexis"."#.quoted` -> `#""Hello, \"Alex\" \\ \"Alexis\".""#`
46 changes: 46 additions & 0 deletions Sources/FoundationExtensions/RandomAccessCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Created by Wade Tregaskis on 2024-03-11.

extension RandomAccessCollection {
/// A combination of finding the longest prefix for some condition and transforming that prefix.
///
/// Combining the transformation with the prefix search is an efficiency improvement for some use-cases, as it avoids duplicate work in having to repeat the transformation on the winning prefix.
///
/// The transformation step is technically optional - you can return the closure's argument (the candidate prefix) to degenerate this into simply returning the longest matching prefix.
///
/// - Parameter transform: The transform to apply, returning a value if successful or nil otherwise. This _must_ have a single transition point (where it goes from returning values to returning nil, for increasingly long prefixes), else the result is undefined.
/// - Returns: The result of a transformation closure on the longest prefix for which the transform succeeds, or nil if there is no [non-empty] prefix for which this is the case.
func longestPrefix<T>(where transform: (SubSequence) throws -> T?) rethrows -> T? {
var lowerBound = self.startIndex
var lowerBoundResult: T? = nil
var upperBound = self.endIndex

var currentGuess = self.index(lowerBound, offsetBy: self.distance(from: lowerBound, to: upperBound) / 2)

while lowerBound != upperBound {
let prefix = self[..<currentGuess]

if let result = try transform(prefix) {
let distance = self.distance(from: currentGuess, to: upperBound)

guard 0 < distance else {
return result
}

lowerBound = currentGuess
lowerBoundResult = result
self.formIndex(&currentGuess, offsetBy: (distance + 1) / 2)
} else {
let distance = self.distance(from: lowerBound, to: currentGuess)

guard 0 < distance else {
return lowerBoundResult
}

upperBound = self.index(before: currentGuess)
self.formIndex(&currentGuess, offsetBy: -((distance + 1) / 2))
}
}

return lowerBoundResult
}
}
34 changes: 34 additions & 0 deletions Tests/FoundationExtensionsTests/RandomAccessCollectionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Created by Wade Tregaskis on 2024-03-11.

import XCTest
@testable import FoundationExtensions


final class RandomAccessCollectionTests: XCTestCase {
func testLongestPrefix() throws {
XCTAssertNil([].longestPrefix(where: { _ in true }))

for array in [[1],
[1, 1],
[1, 1, 2],
[1, 1, 2, 3, 5, 8, 13]] {
XCTAssertEqual(array.longestPrefix(where: { $0 }),
array[...])
XCTAssertNil(array.longestPrefix(where: { _ in nil }))

for length in 1..<array.count {
let targetPrefix = array[0..<length]

XCTAssertEqual(array.longestPrefix(where: { targetPrefix.starts(with: $0) ? $0 : nil }),
targetPrefix)
XCTAssertEqual(array.longestPrefix(where: { targetPrefix.starts(with: $0) ? length : nil }),
length)

XCTAssertEqual(array.longestPrefix(where: { $0.count <= length ? $0 : nil }),
targetPrefix)
XCTAssertEqual(array.longestPrefix(where: { $0.count <= length ? length : nil }),
length)
}
}
}
}

0 comments on commit 1c03b69

Please sign in to comment.