Details
- Swift 5.2, Xcode 11.4 (11E146)
Solution
import Foundation
extension RandomAccessCollection where Element: Comparable {
private func binarySearchIteration(forIndexOf value: Element, in range: Range<Index>? = nil,
valueDetected: ((Index, _ in: Range<Index>) -> Index?)) -> Index? {
let range = range ?? startIndex..<endIndex
guard range.lowerBound < range.upperBound else { return nil }
let size = distance(from: range.lowerBound, to: range.upperBound)
let middle = index(range.lowerBound, offsetBy: size / 2)
switch self[middle] {
case value: return valueDetected(middle, range) ?? middle
case ..<value: return binarySearch(forIndexOf: value, in: index(after: middle)..<range.upperBound)
default: return binarySearch(forIndexOf: value, in: range.lowerBound..<middle)
}
}
func binarySearch(forIndexOf value: Element, in range: Range<Index>? = nil) -> Index? {
binarySearchIteration(forIndexOf: value, in: range) { currentIndex, _ in currentIndex }
}
func binarySearch(forFirstIndexOf value: Element, in range: Range<Index>? = nil) -> Index? {
binarySearchIteration(forIndexOf: value, in: range) { currentIndex, range in
binarySearch(forFirstIndexOf: value, in: range.lowerBound..<currentIndex)
}
}
func binarySearch(forLastIndexOf value: Element, in range: Range<Index>? = nil) -> Index? {
binarySearchIteration(forIndexOf: value, in: range) { currentIndex, range in
binarySearch(forFirstIndexOf: value, in: index(after: currentIndex)..<range.upperBound)
}
}
func binarySearch(forIndicesRangeOf value: Element, in range: Range<Index>? = nil) -> Range<Index>? {
let range = range ?? startIndex..<endIndex
guard range.lowerBound < range.upperBound else { return nil }
guard let currentIndex = binarySearchIteration(forIndexOf: value, in: range, valueDetected: { index, _ in index
}) else { return nil }
let firstIndex = binarySearch(forFirstIndexOf: value, in: range.lowerBound ..< index(after: currentIndex)) ?? currentIndex
let lastIndex = binarySearch(forFirstIndexOf: value, in: index(after: currentIndex) ..< range.upperBound) ?? currentIndex
return firstIndex..<index(after: lastIndex)
}
}
Usage
//let array = ["one", "two", "three", "three", "three", "three", "three", "four", "five", "five"]
//let value = "three"
let array = [1, 2, 3, 3, 3, 3, 3, 4, 5, 5]
let value = 3
print(array.binarySearch(forFirstIndexOf: value))
print(array.binarySearch(forLastIndexOf: value))
print(array.binarySearch(forIndicesRangeOf: value))
Tests
protocol _BinarySearchTestable: class where Collection: RandomAccessCollection, Collection.Element: Comparable {
associatedtype Collection
var array: Collection! { get set }
var elementToSearch: Collection.Element! { get set }
func testFindFirstIndexOfValueInCollection()
func testFindLastIndexOfValueInCollection()
func testFindIndicesRangeOfValueInCollection()
}
extension _BinarySearchTestable where Self: XCTest {
typealias Element = Collection.Element
typealias Index = Collection.Index
func _testFindFirstIndexOfValueInCollection() {
_testfindFirstIndex(comparableArray: array, testableArray: array)
}
func _testFindLastIndexOfValueInCollection() {
let index1 = array.lastIndex(of: elementToSearch)
let index2 = array.binarySearch(forLastIndexOf: elementToSearch)
_testElementsAreEqual(indexInComparableArray: index1, comparableArray: array,
indexInTestableArray: index2, testableArray: array)
}
func _testFindIndicesRangeOfValueInCollection() {
var range1: Range<Index>?
if let firstIndex = array.firstIndex(of: elementToSearch),
let lastIndex = array.lastIndex(of: elementToSearch) {
range1 = firstIndex ..< array.index(after: lastIndex)
}
let range2 = array.binarySearch(forIndicesRangeOf: elementToSearch)
XCTAssertEqual(range1, range2)
}
private func _testElementsAreEqual(indexInComparableArray: Index?, comparableArray: Collection,
indexInTestableArray: Index?, testableArray: Collection) {
XCTAssertEqual(indexInComparableArray, indexInTestableArray)
var valueInComparableArray: Element?
if let index = indexInComparableArray { valueInComparableArray = comparableArray[index] }
var valueInTestableArray: Element?
if let index = indexInComparableArray { valueInTestableArray = testableArray[index] }
XCTAssertEqual(valueInComparableArray, valueInTestableArray)
}
private func _testfindFirstIndex(comparableArray: Collection, testableArray: Collection) {
let index1 = comparableArray.firstIndex(of: elementToSearch)
let index2 = testableArray.binarySearch(forFirstIndexOf: elementToSearch)
_testElementsAreEqual(indexInComparableArray: index1, comparableArray: comparableArray,
indexInTestableArray: index2, testableArray: testableArray)
}
}
class TestsInEmptyArray: XCTestCase, _BinarySearchTestable {
var array: [String]!
var elementToSearch: String!
override func setUp() {
array = []
elementToSearch = "value"
}
func testFindFirstIndexOfValueInCollection() { _testFindFirstIndexOfValueInCollection() }
func testFindLastIndexOfValueInCollection() { _testFindLastIndexOfValueInCollection() }
func testFindIndicesRangeOfValueInCollection() { _testFindIndicesRangeOfValueInCollection() }
}
class TestsInArray: XCTestCase, _BinarySearchTestable {
var array: [Int]!
var elementToSearch: Int!
override func setUp() {
array = [1, 2, 3, 3, 3, 3, 3, 4, 5, 5]
elementToSearch = 3
}
func testFindFirstIndexOfValueInCollection() { _testFindFirstIndexOfValueInCollection() }
func testFindLastIndexOfValueInCollection() { _testFindLastIndexOfValueInCollection() }
func testFindIndicesRangeOfValueInCollection() { _testFindIndicesRangeOfValueInCollection() }
}
class TestsInArrayWithOneElement: XCTestCase, _BinarySearchTestable {
var array: [Date]!
var elementToSearch: Date!
override func setUp() {
let date = Date()
array = [date]
elementToSearch = date
}
func testFindFirstIndexOfValueInCollection() { _testFindFirstIndexOfValueInCollection() }
func testFindLastIndexOfValueInCollection() { _testFindLastIndexOfValueInCollection() }
func testFindIndicesRangeOfValueInCollection() { _testFindIndicesRangeOfValueInCollection() }
}