From 5b00f7f2e8b58d3a4719ccdb06039d7dbf6f3e87 Mon Sep 17 00:00:00 2001 From: Klaas Pieter Annema Date: Thu, 15 Sep 2011 22:32:23 +0200 Subject: [PATCH] improve _CPKVCArray _CPKVCArray now consistently calls both single and multiple indexes accessors consistently Tests that verify the behavior are included --- Foundation/CPArray+KVO.j | 97 +++++++-- Tests/Foundation/CPKVCArrayTest.j | 314 ++++++++++++++++++++++++++++++ 2 files changed, 397 insertions(+), 14 deletions(-) create mode 100644 Tests/Foundation/CPKVCArrayTest.j diff --git a/Foundation/CPArray+KVO.j b/Foundation/CPArray+KVO.j index cba5700cd..8000d2076 100644 --- a/Foundation/CPArray+KVO.j +++ b/Foundation/CPArray+KVO.j @@ -131,7 +131,7 @@ _replaceManySEL = sel_getName(@"replace" + capitalizedKey + "AtIndexes:with" + capitalizedKey + ":"); if ([_proxyObject respondsToSelector:_replaceManySEL]) - _replace = [_proxyObject methodForSelector:_replaceManySEL]; + _replaceMany = [_proxyObject methodForSelector:_replaceManySEL]; _objectAtIndexSEL = sel_getName(@"objectIn" + capitalizedKey + "AtIndex:"); if ([_proxyObject respondsToSelector:_objectAtIndexSEL]) @@ -233,10 +233,25 @@ - (id)objectAtIndex:(unsigned)anIndex { - if (_objectAtIndex) - return _objectAtIndex(_proxyObject, _objectAtIndexSEL, anIndex); + return [[self objectsAtIndexes:[CPIndexSet indexSetWithIndex:anIndex]] firstObject]; +} - return [[self _representedObject] objectAtIndex:anIndex]; +- (CPArray)objectsAtIndexes:(CPIndexSet)theIndexes +{ + if (_objectsAtIndexes) + return _objectsAtIndexes(_proxyObject, _objectsAtIndexesSEL, theIndexes); + if (_objectAtIndex) + { + var index = CPNotFound, + objects = []; + + while ((index = [theIndexes indexGreaterThanIndex:index]) !== CPNotFound) + objects.push(_objectAtIndex(_proxyObject, _objectAtIndexSEL, index)); + + return objects; + } + + return [[self _representedObject] objectsAtIndexes:theIndexes]; } - (void)addObject:(id)anObject @@ -288,6 +303,27 @@ [self removeObject:anObject inRange:CPMakeRange(0, [self count])]; } +- (void)removeObject:(id)theObject inRange:(CPRange)theRange +{ + if (_remove) + _remove(_proxyObject, _removeSEL, [self indexOfObject:theObject inRange:theRange]); + else if (_removeMany) + { + var index = [self indexOfObject:theObject inRange:theRange]; + _removeMany(_proxyObject, _removeManySEL, [CPIndexSet indexSetWithIndex:index]); + } + else + { + var index; + + while ((index = [self indexOfObject:theObject inRange:theRange]) !== CPNotFound) + { + [self removeObjectAtIndex:index]; + theRange = CPIntersectionRange(CPMakeRange(index, length - index), theRange); + } + } +} + - (void)removeLastObject { if (_remove) @@ -301,24 +337,57 @@ - (void)removeObjectAtIndex:(unsigned)anIndex { - if (_remove) - return _remove(_proxyObject, _removeSEL, anIndex); + [self removeObjectsAtIndexes:[CPIndexSet indexSetWithIndex:anIndex]]; +} - var target = [[self _representedObject] copy]; +- (void)removeObjectsAtIndexes:(CPIndexSet)theIndexes +{ + if (_removeMany) + _removeMany(_proxyObject, _removeManySEL, theIndexes); + else if (_remove) + { + var index = [theIndexes lastIndex]; - [target removeObjectAtIndex:anIndex]; - [self _setRepresentedObject:target]; + while (index !== CPNotFound) + { + _remove(_proxyObject, _removeSEL, index) + index = [theIndexes indexLessThanIndex:index]; + } + } + else + { + var target = [[self _representedObject] copy]; + [target removeObjectsAtIndexes:theIndexes]; + [self _setRepresentedObject:target]; + } } - (void)replaceObjectAtIndex:(unsigned)anIndex withObject:(id)anObject { - if (_replace) - return _replace(_proxyObject, _replaceSEL, anIndex, anObject); + [self replaceObjectsAtIndexes:[CPIndexSet indexSetWithIndex:anIndex] withObjects:[anObject]] +} - var target = [[self _representedObject] copy]; +- (void)replaceObjectsAtIndexes:(CPIndexSet)theIndexes withObjects:(CPArray)theObjects +{ + if (_replaceMany) + return _replaceMany(_proxyObject, _replaceManySEL, theIndexes, theObjects); + else if (_replace) + { + var i = 0, + index = [theIndexes firstIndex]; - [target replaceObjectAtIndex:anIndex withObject:anObject]; - [self _setRepresentedObject:target]; + while (index !== CPNotFound) + { + _replace(_proxyObject, _replaceSEL, index, [theObjects objectAtIndex:i++]); + index = [theIndexes indexGreaterThanIndex:index]; + } + } + else + { + var target = [[self _representedObject] copy]; + [target replaceObjectsAtIndexes:theIndexes withObjects:theObjects]; + [self _setRepresentedObject:target]; + } } @end diff --git a/Tests/Foundation/CPKVCArrayTest.j b/Tests/Foundation/CPKVCArrayTest.j new file mode 100644 index 000000000..f29bd7d91 --- /dev/null +++ b/Tests/Foundation/CPKVCArrayTest.j @@ -0,0 +1,314 @@ +var COUNTER; + +@implementation CPKVCArrayTest : OJTestCase +{ + TestObject _object @accessors(property=object); + ImplementedTestObject _implementedObject @accessors(property=implementedObject); +} + +- (void)setUp +{ + _object = [[TestObject alloc] init]; + _implementedObject = [[ImplementedTestObject alloc] init]; + + COUNTER = 0; +} + +- (void)_patchSelector:(SEL)theSelector +{ + var method = class_getInstanceMethod([[self implementedObject] class], theSelector), + implementation = method_getImplementation(method); + + class_addMethod([[self object] class], theSelector, implementation); +} + +- (void)testUsesCountOfKey +{ + [self _patchSelector:@selector(countOfValues)]; + + var count = [[[self object] mutableArrayValueForKey:@"values"] count]; + + [self assert:10 equals:count message:@"countOfValues should return 10"]; + [self assert:1 equals:COUNTER message:@"countOfValues should have been called once"] +} + +- (void)testObjectAtIndexUsesObjectInKeyAtIndex +{ + [self _patchSelector:@selector(objectInValuesAtIndex:)]; + + var values = [[self object] mutableArrayValueForKey:@"values"], + object = [values objectAtIndex:0]; + + [self assert:0 equals:object] + [self assert:1 equals:COUNTER message:@"objectInValuesAtIndex: should have been called once"]; +} + +- (void)testObjectsAtIndexesUsesObjectInKeyAtIndex +{ + [self _patchSelector:@selector(objectInValuesAtIndex:)]; + + var values = [[self object] mutableArrayValueForKey:@"values"], + objects = [values objectsAtIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, 2)]]; + + [self assert:[0, 1] equals:objects] + [self assert:2 equals:COUNTER message:@"objectInValuesAtIndex: should have been called once"]; +} + +- (void)testObjectAtIndexUsesKeyAtIndexes +{ + [self _patchSelector:@selector(valuesAtIndexes:)]; + + var values = [[self object] mutableArrayValueForKey:@"values"], + object = [values objectAtIndex:0]; + + [self assert:0 equals:object] + [self assert:1 equals:COUNTER message:@"valuesAtIndexes: should have been called once"]; +} + +- (void)testObjectsAtIndexesUsesKeyAtIndexes +{ + [self _patchSelector:@selector(valuesAtIndexes:)]; + + var values = [[self object] mutableArrayValueForKey:@"values"], + objects = [values objectsAtIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, 2)]]; + + [self assert:[0, 1] equals:objects] + [self assert:1 equals:COUNTER message:@"valuesAtIndexes: should have been called once"]; +} + +- (void)testInsertObjectAtIndexUsesInsertKeyAtIndex +{ + [self _patchSelector:@selector(insertObject:inValuesAtIndex:)]; + + [[[self object] mutableArrayValueForKey:@"values"] insertObject:11 atIndex:10]; + + [self assert:11 equals:[[[self object] values] objectAtIndex:10]]; + [self assert:1 equals:COUNTER message:@"insertObject:inValuesAtIndex: should have been called once"]; +} + +- (void)testInsertObjectsAtIndexesUsesInsertKeyAtIndex +{ + [self _patchSelector:@selector(insertObject:inValuesAtIndex:)]; + + var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(10, 2)]; + [[[self object] mutableArrayValueForKey:@"values"] insertObjects:[11, 12] atIndexes:indexes]; + + [self assert:[11, 12] equals:[[[self object] values] objectsAtIndexes:indexes]]; + [self assert:2 + equals:COUNTER + message:@"insertValues:atIndexes: should have been called once for each object"]; +} + +- (void)testInsertObjectAtIndexUsesInsertKeyAtIndexes +{ + [self _patchSelector:@selector(insertValues:atIndexes:)]; + + [[[self object] mutableArrayValueForKey:@"values"] insertObject:11 atIndex:10]; + + [self assert:11 equals:[[[self object] values] objectAtIndex:10]]; + [self assert:1 equals:COUNTER message:@"insertValues:atIndexes: should have been called once"]; +} + +- (void)testInsertObjectsAtIndexesUsesInsertKeyAtIndexes +{ + [self _patchSelector:@selector(insertValues:atIndexes:)]; + + var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(10, 2)]; + [[[self object] mutableArrayValueForKey:@"values"] insertObjects:[11, 12] atIndexes:indexes]; + + [self assert:[11, 12] equals:[[[self object] values] objectsAtIndexes:indexes]]; + [self assert:1 + equals:COUNTER + message:@"insertValues:atIndexes: should have been called once for each object"]; +} + +- (void)testRemoveObjectAtIndexUsesRemoveKeyAtIndex +{ + [self _patchSelector:@selector(removeObjectFromValuesAtIndex:)]; + + [[[self object] mutableArrayValueForKey:@"values"] removeObjectAtIndex:0]; + + [self assert:[1, 2, 3, 4, 5, 6, 7, 8, 9] equals:[[self object] values]]; + [self assert:1 equals:COUNTER message:@"removeObjectFromValuesAtIndex: should have been called once"]; +} + +- (void)testRemoveObjectAtIndexesUsesRemoveKeyAtIndex +{ + [self _patchSelector:@selector(removeObjectFromValuesAtIndex:)]; + + var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, 2)]; + [[[self object] mutableArrayValueForKey:@"values"] removeObjectsAtIndexes:indexes]; + + [self assert:[2, 3, 4, 5, 6, 7, 8, 9] equals:[[self object] values]]; + [self assert:2 equals:COUNTER message:@"removeValuesAtIndex: should have been called once for each object"]; +} + +- (void)testRemoveObjectsAtIndexesUsesRemoveKeyAtIndex +{ + [self _patchSelector:@selector(removeValuesAtIndexes:)]; + + [[[self object] mutableArrayValueForKey:@"values"] removeObjectAtIndex:0]; + + [self assert:[1, 2, 3, 4, 5, 6, 7, 8, 9] equals:[[self object] values]]; + [self assert:1 + equals:COUNTER + message:@"removeValuesAtIndexes: should have been once for each object"]; +} + +- (void)testRemoveObjectsAtIndexesUsesRemoveKeyAtIndexes +{ + [self _patchSelector:@selector(removeValuesAtIndexes:)]; + + var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(3, 2)]; + [[[self object] mutableArrayValueForKey:@"values"] removeObjectsAtIndexes:indexes]; + + [self assert:[0, 1, 2, 5, 6, 7, 8, 9] equals:[[self object] values]]; + [self assert:1 equals:COUNTER message:@"removeValuesAtIndexes: should have been called once"]; +} + +- (void)testReplaceObjectAtIndexWithObjectUsesReplaceObjectInKeyAtIndexWithObject +{ + [self _patchSelector:@selector(replaceObjectInValuesAtIndex:withObject:)]; + + [[[self object] mutableArrayValueForKey:@"values"] replaceObjectAtIndex:0 withObject:1]; + + [self assert:[1, 1, 2, 3, 4, 5, 6, 7, 8, 9] equals:[[self object] values]]; + [self assert:1 + equals:COUNTER + message:@"replaceObjectInValuesAtIndex:withObject: should have been called once"]; +} + +- (void)testReplaceObjectsAtIndexesWithObjectsUsesReplaceObjectInKeyAtIndexWithObject +{ + [self _patchSelector:@selector(replaceObjectInValuesAtIndex:withObject:)]; + + var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(5, 3)]; + [[[self object] mutableArrayValueForKey:@"values"] replaceObjectsAtIndexes:indexes withObjects:[1, 2, 3]]; + + [self assert:[0, 1, 2, 3, 4, 1, 2, 3, 8, 9] equals:[[self object] values]]; + [self assert:3 + equals:COUNTER + message:@"replaceObjectInValuesAtIndex:withObject: should have been called once"]; +} + +- (void)testReplaceObjectAtIndexWithObjectUsesReplaceKeyAtIndexesWithKeys +{ + [self _patchSelector:@selector(replaceValuesAtIndexes:withValues:)]; + + [[[self object] mutableArrayValueForKey:@"values"] replaceObjectAtIndex:5 withObject:6]; + + [self assert:[0, 1, 2, 3, 4, 6, 6, 7, 8, 9] equals:[[self object] values]]; + [self assert:1 + equals:COUNTER + message:@"replaceObjectInValuesAtIndex:withObject: should have been called once"]; +} + +- (void)testReplaceObjectsAtIndexesWithObjectsUsesReplaceKeyAtIndexesWithKeys +{ + [self _patchSelector:@selector(replaceValuesAtIndexes:withValues:)]; + + var indexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(6, 2)]; + [[[self object] mutableArrayValueForKey:@"values"] replaceObjectsAtIndexes:indexes withObjects:[7, 8]]; + + [self assert:[0, 1, 2, 3, 4, 5, 7, 8, 8, 9] equals:[[self object] values]]; + [self assert:1 + equals:COUNTER + message:@"replaceObjectInValuesAtIndexes:withValues: should have been called once"]; +} + +@end + +@implementation TestObject : CPObject +{ + CPArray _values @accessors(property=values); +} + +- (id)init +{ + if (self = [super init]) + { + _values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + return self; +} + +@end + +@implementation ImplementedTestObject : TestObject +{ +} + +- (int)countOfValues +{ + // CPLog.warn(@"countOfValues"); + + COUNTER += 1; + return [[self values] count]; +} + +- (id)objectInValuesAtIndex:(int)theIndex +{ + // CPLog.warn(@"objectInValuesAtIndex: %@", theIndex); + + COUNTER += 1; + return [[self values] objectAtIndex:theIndex]; +} + +- (CPArray)valuesAtIndexes:(CPIndexSet)theIndexes +{ + // CPLog.warn(@"valuesAtIndexes: %@", theIndexes); + + COUNTER += 1; + return [[self values] objectsAtIndexes:theIndexes]; +} + +- (void)insertObject:(id)theObject inValuesAtIndex:(int)theIndex +{ + // CPLog.warn(@"insertObject: %@ inValuesAtIndex: %@", theObject, theIndex); + + COUNTER += 1; + [[self values] insertObject:theObject atIndex:theIndex]; +} + +- (void)insertValues:(CPArray)theObjects atIndexes:(CPIndexSet)theIndexes +{ + // CPLog.warn(@"insertValues: %@ atIndexes: %@", theObjects, theIndexes); + + COUNTER += 1; + [[self values] insertObjects:theObjects atIndexes:theIndexes]; +} + +- (void)removeObjectFromValuesAtIndex:(int)theIndex +{ + // CPLog.warn(@"removeObjectFromValuesAtIndex: %@", theIndex); + + COUNTER += 1; + [[self values] removeObjectAtIndex:theIndex]; +} + +- (void)removeValuesAtIndexes:(CPIndexSet)theIndexes +{ + // CPLog.warn(@"removeValuesAtIndexes: %@", theIndexes); + + COUNTER += 1; + [[self values] removeObjectsAtIndexes:theIndexes]; +} + +- (void)replaceObjectInValuesAtIndex:(int)theIndex withObject:(id)theObject +{ + // CPLog.warn(@"replaceObjectInValuesAtIndex: %@ withObject: %@", theIndex, theObject); + + COUNTER += 1; + [[self values] replaceObjectAtIndex:theIndex withObject:theObject]; +} + +- (void)replaceValuesAtIndexes:(CPIndexSet)theIndexes withValues:(id)theObjects +{ + // CPLog.warn(@"replaceValuesAtIndexes: %@ withValues: %@", theIndexes, theObjects); + + COUNTER += 1; + [[self values] replaceObjectsAtIndexes:theIndexes withObjects:theObjects]; +} + +@end \ No newline at end of file