Skip to content

Commit fff27d9

Browse files
committed
Merge pull request #130 from HeMet/master
Fix NSArray.indexOfObject(inSortedRange:options:usingComparator) for fringe cases
2 parents 87dacb2 + 660c1d1 commit fff27d9

File tree

3 files changed

+64
-11
lines changed

3 files changed

+64
-11
lines changed

Foundation/NSArray.swift

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -412,22 +412,44 @@ public class NSArray : NSObject, NSCopying, NSMutableCopying, NSSecureCoding, NS
412412
}
413413

414414
public func indexOfObject(obj: AnyObject, inSortedRange r: NSRange, options opts: NSBinarySearchingOptions, usingComparator cmp: NSComparator) -> Int {
415-
guard (r.location + r.length) <= count else {
416-
NSInvalidArgument("range \(r) extends beyond bounds [0 .. \(count - 1)]")
415+
let lastIndex = r.location + r.length - 1
416+
417+
// argument validation
418+
guard lastIndex < count else {
419+
let bounds = count == 0 ? "for empty array" : "[0 .. \(count - 1)]"
420+
NSInvalidArgument("range \(r) extends beyond bounds \(bounds)")
417421
}
418422

419423
if opts.contains(.FirstEqual) && opts.contains(.LastEqual) {
420424
NSInvalidArgument("both NSBinarySearching.FirstEqual and NSBinarySearching.LastEqual options cannot be specified")
421425
}
422426

427+
let searchForInsertionIndex = opts.contains(.InsertionIndex)
428+
429+
// fringe cases
430+
if r.length == 0 {
431+
return searchForInsertionIndex ? r.location : NSNotFound
432+
}
433+
434+
let leastObj = objectAtIndex(r.location)
435+
if cmp(obj, leastObj) == .OrderedAscending {
436+
return searchForInsertionIndex ? r.location : NSNotFound
437+
}
438+
439+
let greatestObj = objectAtIndex(lastIndex)
440+
if cmp(obj, greatestObj) == .OrderedDescending {
441+
return searchForInsertionIndex ? lastIndex + 1 : NSNotFound
442+
}
443+
444+
// common processing
423445
let firstEqual = opts.contains(.FirstEqual)
424446
let lastEqual = opts.contains(.LastEqual)
425447
let anyEqual = !(firstEqual || lastEqual)
426448

427449
var result = NSNotFound
428450
var indexOfLeastGreaterThanObj = NSNotFound
429451
var start = r.location
430-
var end = r.location + r.length - 1
452+
var end = lastIndex
431453

432454
loop: while start <= end {
433455
let middle = start + (end - start) / 2
@@ -459,18 +481,14 @@ public class NSArray : NSObject, NSCopying, NSMutableCopying, NSSecureCoding, NS
459481
}
460482
}
461483

462-
guard opts.contains(.InsertionIndex) && lastEqual else {
484+
guard searchForInsertionIndex && lastEqual else {
463485
return result
464486
}
465487

466488
guard result == NSNotFound else {
467489
return result + 1
468490
}
469491

470-
guard indexOfLeastGreaterThanObj != NSNotFound else {
471-
return count
472-
}
473-
474492
return indexOfLeastGreaterThanObj
475493
}
476494

Foundation/NSObjCRuntime.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public let NSNotFound: Int = Int.max
8080
fatalError("\(fn) is not yet implemented")
8181
}
8282

83-
@noreturn func NSInvalidArgument(message: String, method: String = __FUNCTION__) {
83+
@noreturn internal func NSInvalidArgument(message: String, method: String = __FUNCTION__) {
8484
fatalError("\(method): \(message)")
8585
}
8686

TestFoundation/TestNSArray.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class TestNSArray : XCTestCase {
2929
("test_getObjects", test_getObjects),
3030
("test_objectAtIndex", test_objectAtIndex),
3131
("test_binarySearch", test_binarySearch),
32+
("test_binarySearchFringeCases", test_binarySearchFringeCases),
3233
("test_replaceObjectsInRange_withObjectsFromArray", test_replaceObjectsInRange_withObjectsFromArray),
3334
("test_replaceObjectsInRange_withObjectsFromArray_range", test_replaceObjectsInRange_withObjectsFromArray_range),
3435
]
@@ -140,8 +141,42 @@ class TestNSArray : XCTestCase {
140141
let indexOfLeastGreaterObjectThanFive = objectIndexInArray(array, value: 5, startingFrom: 0, length: 10, options: [.InsertionIndex, .LastEqual])
141142
XCTAssertTrue(indexOfLeastGreaterObjectThanFive == 7, "If both .InsertionIndex and .LastEqual are specified NSArray returns the index of the least greater object...")
142143

143-
let endOfArray = objectIndexInArray(array, value: 10, startingFrom: 0, length: 13, options: [.InsertionIndex, .LastEqual])
144-
XCTAssertTrue(endOfArray == array.count, "...or the index at the end of the array if the object is larger than all other elements.")
144+
let rangeStart = 0
145+
let rangeLength = 13
146+
let endOfArray = objectIndexInArray(array, value: 10, startingFrom: rangeStart, length: rangeLength, options: [.InsertionIndex, .LastEqual])
147+
XCTAssertTrue(endOfArray == (rangeStart + rangeLength), "...or the index at the end of the array if the object is larger than all other elements.")
148+
}
149+
150+
func test_binarySearchFringeCases() {
151+
let array = NSArray(array: [
152+
NSNumber(int: 0), NSNumber(int: 1), NSNumber(int: 2), NSNumber(int: 2), NSNumber(int: 3),
153+
NSNumber(int: 4), NSNumber(int: 4), NSNumber(int: 6), NSNumber(int: 7), NSNumber(int: 7),
154+
NSNumber(int: 7), NSNumber(int: 8), NSNumber(int: 9), NSNumber(int: 9)])
155+
156+
let emptyArray = NSArray()
157+
// Same as for non empty NSArray but error message ends with 'bounds for empty array'.
158+
// let _ = objectIndexInArray(emptyArray, value: 0, startingFrom: 0, length: 1)
159+
160+
let notFoundInEmptyArray = objectIndexInArray(emptyArray, value: 9, startingFrom: 0, length: 0)
161+
XCTAssertEqual(notFoundInEmptyArray, NSNotFound, "Empty NSArray return NSNotFound for any valid arguments.")
162+
163+
let startIndex = objectIndexInArray(emptyArray, value: 7, startingFrom: 0, length: 0, options: [.InsertionIndex])
164+
XCTAssertTrue(startIndex == 0, "For Empty NSArray any objects should be inserted at start.")
165+
166+
let rangeStart = 0
167+
let rangeLength = 13
168+
169+
let leastSearch = objectIndexInArray(array, value: -1, startingFrom: rangeStart, length: rangeLength)
170+
XCTAssertTrue(leastSearch == NSNotFound, "If object is less than least object in the range then there is no change it could be found.")
171+
172+
let greatestSearch = objectIndexInArray(array, value: 15, startingFrom: rangeStart, length: rangeLength)
173+
XCTAssertTrue(greatestSearch == NSNotFound, "If object is greater than greatest object in the range then there is no change it could be found.")
174+
175+
let leastInsert = objectIndexInArray(array, value: -1, startingFrom: rangeStart, length: rangeLength, options: .InsertionIndex)
176+
XCTAssertTrue(leastInsert == rangeStart, "If object is less than least object in the range it should be inserted at range' location.")
177+
178+
let greatestInsert = objectIndexInArray(array, value: 15, startingFrom: rangeStart, length: rangeLength, options: .InsertionIndex)
179+
XCTAssertTrue(greatestInsert == (rangeStart + rangeLength), "If object is greater than greatest object in the range it should be inserted at range' end.")
145180
}
146181

147182
func objectIndexInArray(array: NSArray, value: Int, startingFrom: Int, length: Int, options: NSBinarySearchingOptions = []) -> Int {

0 commit comments

Comments
 (0)