Files
cappuccino/Foundation/CPArray/CPArray.j
T

915 lines
26 KiB
Plaintext
Executable File

/*
* CPArray.j
* Foundation
*
* Created by Francisco Tolmasky.
* Copyright 2008, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import "CPEnumerator.j"
@import "CPException.j"
@import "CPObject.j"
@import "CPRange.j"
@import "CPSortDescriptor.j"
CPEnumerationNormal = 0;
CPEnumerationConcurrent = 1 << 0;
CPEnumerationReverse = 1 << 1;
CPBinarySearchingFirstEqual = 1 << 8;
CPBinarySearchingLastEqual = 1 << 9;
CPBinarySearchingInsertionIndex = 1 << 10;
var concat = Array.prototype.concat,
join = Array.prototype.join,
push = Array.prototype.push;
#define FORWARD_TO_CONCRETE_CLASS()\
if (self === _CPSharedPlaceholderArray)\
{\
arguments[0] = [_CPJavaScriptArray alloc];\
return objj_msgSend.apply(this, arguments);\
}\
return [super init];
/*!
@class CPArray
@brief A mutable array backed by a JavaScript Array.
@ingroup foundation
A mutable array class backed by a JavaScript Array.
There is also a CPMutableArray class,
but it is just a child class of this class with an
empty implementation. All mutable functionality is
implemented directly in CPArray.
*/
@implementation CPArray : CPObject
/*!
Returns a new uninitialized CPArray.
*/
+ (id)alloc
{
if (self === CPArray || self === CPMutableArray)
return [_CPPlaceholderArray alloc];
return [super alloc];
}
/*!
Returns a new initialized CPArray.
*/
+ (id)array
{
return [[self alloc] init];
}
/*!
Creates a new array containing the objects in \c anArray.
@param anArray Objects in this array will be added to the new array
@return a new CPArray of the provided objects
*/
+ (id)arrayWithArray:(CPArray)anArray
{
return [[self alloc] initWithArray:anArray];
}
/*!
Creates a new array with \c anObject in it.
@param anObject the object to be added to the array
@return a new CPArray containing a single object
*/
+ (id)arrayWithObject:(id)anObject
{
return [[self alloc] initWithObjects:anObject];
}
/*!
Creates a new CPArray containing all the objects passed as arguments to the method.
@param anObject the objects that will be added to the new array
@return a new CPArray containing the argument objects
*/
+ (id)arrayWithObjects:(id)anObject, ...
{
arguments[0] = [self alloc];
arguments[1] = @selector(initWithObjects:);
return objj_msgSend.apply(this, arguments);
}
/*!
Creates a CPArray from a JavaScript array of objects.
@param objects the JavaScript Array
@param aCount the number of objects in the JS Array
@return a new CPArray containing the specified objects
*/
+ (id)arrayWithObjects:(id)objects count:(unsigned)aCount
{
return [[self alloc] initWithObjects:objects count:aCount];
}
/*!
Initializes the CPArray.
@return the initialized array
*/
- (id)init
{
FORWARD_TO_CONCRETE_CLASS();
}
// Creating an Array
/*!
Creates a new CPArray from \c anArray.
@param anArray objects in this array will be added to the new array
@return a new CPArray containing the objects of \c anArray
*/
- (id)initWithArray:(CPArray)anArray
{
FORWARD_TO_CONCRETE_CLASS();
}
/*!
Initializes a the array with the contents of \c anArray
and optionally performs a deep copy of the objects based on \c copyItems.
@param anArray the array to copy the data from
@param shouldCopyItems if \c YES, each object will be copied by having a \c -copy message
sent to it, and the returned object will be added to the receiver. Otherwise, no copying will be performed.
@return the initialized array of objects
*/
- (id)initWithArray:(CPArray)anArray copyItems:(BOOL)shouldCopyItems
{
FORWARD_TO_CONCRETE_CLASS();
}
/*!
initializes an array with the contents of anArray
*/
- (id)initWithObjects:(id)anObject, ...
{
FORWARD_TO_CONCRETE_CLASS();
}
/*!
Initializes the array with a JavaScript array of objects.
@param objects the array of objects to add to the receiver
@param aCount the number of objects in \c objects
@return the initialized CPArray
*/
- (id)initWithObjects:(id)objects count:(unsigned)aCount
{
FORWARD_TO_CONCRETE_CLASS();
}
// FIXME: This should be defined in CPMutableArray, not here.
- (id)initWithCapacity:(unsigned)aCapacity
{
FORWARD_TO_CONCRETE_CLASS();
}
// Querying an array
/*!
Returns \c YES if the array contains \c anObject. Otherwise, it returns \c NO.
@param anObject the method checks if this object is already in the array
*/
- (BOOL)containsObject:(id)anObject
{
return [self indexOfObject:anObject] !== CPNotFound;
}
- (BOOL)containsObjectIdenticalTo:(id)anObject
{
return [self indexOfObjectIdenticalTo:anObject] !== CPNotFound;
}
/*!
Returns the number of elements in the array
*/
- (int)count
{
_CPRaiseInvalidAbstractInvocation(self, _cmd);
}
/*!
Returns the first object in the array. If the array is empty, returns \c nil
*/
- (id)firstObject
{
var count = [self count];
if (count > 0)
return [self objectAtIndex:0];
return nil;
}
/*!
Returns the last object in the array. If the array is empty, returns \c nil
*/
- (id)lastObject
{
var count = [self count];
if (count <= 0)
return nil;
return [self objectAtIndex:count - 1];
}
/*!
Returns the object at index \c anIndex.
@throws CPRangeException if \c anIndex is out of bounds
*/
- (id)objectAtIndex:(int)anIndex
{
_CPRaiseInvalidAbstractInvocation(self, _cmd);
}
/*!
Returns the objects at \c indexes in a new CPArray.
@param indexes the set of indices
@throws CPRangeException if any of the indices is greater than or equal to the length of the array
*/
- (CPArray)objectsAtIndexes:(CPIndexSet)indexes
{
var index = CPNotFound,
objects = [];
while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound)
objects.push([self objectAtIndex:index]);
return objects;
}
/*!
Returns an enumerator describing the array sequentially
from the first to the last element. You should not modify
the array during enumeration.
*/
- (CPEnumerator)objectEnumerator
{
return [[_CPArrayEnumerator alloc] initWithArray:self];
}
/*!
Returns an enumerator describing the array sequentially
from the last to the first element. You should not modify
the array during enumeration.
*/
- (CPEnumerator)reverseObjectEnumerator
{
return [[_CPReverseArrayEnumerator alloc] initWithArray:self];
}
/*!
Returns the index of \c anObject in this array.
If the object is not in the array,
returns \c CPNotFound. It first attempts to find
a match using \c -isEqual:, then \c ===.
@param anObject the object to search for
*/
- (CPUInteger)indexOfObject:(id)anObject
{
return [self indexOfObject:anObject inRange:nil];
}
/*!
Returns the index of \c anObject in the array
within \c aRange. It first attempts to find
a match using \c -isEqual:, then \c ===.
@param anObject the object to search for
@param aRange the range to search within
@return the index of the object, or \c CPNotFound if it was not found.
*/
- (CPUInteger)indexOfObject:(id)anObject inRange:(CPRange)aRange
{
// Only use isEqual: if our object is a CPObject.
if (anObject && anObject.isa)
{
var index = aRange ? aRange.location : 0,
count = aRange ? CPMaxRange(aRange) : [self count];
for (; index < count; ++index)
if ([[self objectAtIndex:index] isEqual:anObject])
return index;
return CPNotFound;
}
return [self indexOfObjectIdenticalTo:anObject inRange:aRange];
}
/*!
Returns the index of \c anObject in the array. The test for equality is done using only \c ===.
@param anObject the object to search for
@return the index of the object in the array. \c CPNotFound if the object is not in the array.
*/
- (CPUInteger)indexOfObjectIdenticalTo:(id)anObject
{
return [self indexOfObjectIdenticalTo:anObject inRange:nil];
}
/*!
Returns the index of \c anObject in the array
within \c aRange. The test for equality is
done using only \c ==.
@param anObject the object to search for
@param aRange the range to search within
@return the index of the object, or \c CPNotFound if it was not found.
*/
- (CPUInteger)indexOfObjectIdenticalTo:(id)anObject inRange:(CPRange)aRange
{
var index = aRange ? aRange.location : 0,
count = aRange ? CPMaxRange(aRange) : [self count];
for (; index < count; ++index)
if ([self objectAtIndex:index] === anObject)
return index;
return CPNotFound;
}
/*!
Returns the index of the first object in the receiver that passes a test in a given Javascript function.
@param predicate The function to apply to elements of the array. The function receives two arguments:
object The element in the array.
index The index of the element in the array.
The predicate function should either return a Boolean value that indicates whether the object passed the test,
or nil to stop the search, which will return CPNotFound to the sender.
@return The index of the first matching object, or \c CPNotFound if there is no matching object.
*/
- (unsigned)indexOfObjectPassingTest:(Function)aPredicate
{
return [self indexOfObjectWithOptions:CPEnumerationNormal passingTest:aPredicate context:undefined];
}
/*!
Returns the index of the first object in the receiver that passes a test in a given Javascript function.
@param predicate The function to apply to elements of the array. The function receives two arguments:
object The element in the array.
index The index of the element in the array.
context The object passed to the receiver in the aContext parameter.
The predicate function should either return a Boolean value that indicates whether the object passed the test,
or nil to stop the search, which will return CPNotFound to the sender.
@param context An object that contains context information you want passed to the predicate function.
@return The index of the first matching object, or \c CPNotFound if there is no matching object.
*/
- (unsigned)indexOfObjectPassingTest:(Function)aPredicate context:(id)aContext
{
return [self indexOfObjectWithOptions:CPEnumerationNormal passingTest:aPredicate context:aContext];
}
/*!
Returns the index of the first object in the receiver that passes a test in a given Javascript function.
@param options Specifies the direction in which the array is searched. Pass CPEnumerationNormal to search forwards
or CPEnumerationReverse to search in reverse.
@param predicate The function to apply to elements of the array. The function receives two arguments:
object The element in the array.
index The index of the element in the array.
The predicate function should either return a Boolean value that indicates whether the object passed the test,
or nil to stop the search, which will return CPNotFound to the sender.
@return The index of the first matching object, or \c CPNotFound if there is no matching object.
*/
- (unsigned)indexOfObjectWithOptions:(CPEnumerationOptions)options passingTest:(Function)aPredicate
{
return [self indexOfObjectWithOptions:options passingTest:aPredicate context:undefined];
}
/*!
Returns the index of the first object in the receiver that passes a test in a given Javascript function.
@param options Specifies the direction in which the array is searched. Pass CPEnumerationNormal to search forwards
or CPEnumerationReverse to search in reverse.
@param predicate The function to apply to elements of the array. The function receives two arguments:
object The element in the array.
index The index of the element in the array.
context The object passed to the receiver in the aContext parameter.
The predicate function should either return a Boolean value that indicates whether the object passed the test,
or nil to stop the search, which will return CPNotFound to the sender.
@param context An object that contains context information you want passed to the predicate function.
@return The index of the first matching object, or \c CPNotFound if there is no matching object.
*/
- (unsigned)indexOfObjectWithOptions:(CPEnumerationOptions)options passingTest:(Function)aPredicate context:(id)aContext
{
// We don't use an enumerator because they return nil to indicate end of enumeration,
// but nil may actually be the value we are looking for, so we have to loop over the array.
if (options & CPEnumerationReverse)
{
var index = [self count] - 1,
stop = -1,
increment = -1;
}
else
{
var index = 0,
stop = [self count],
increment = 1;
}
for (; index !== stop; index += increment)
if (aPredicate([self objectAtIndex:index], index, aContext))
return index;
return CPNotFound;
}
- (CPUInteger)indexOfObject:(id)anObject
inSortedRange:(CPRange)aRange
options:(CPBinarySearchingOptions)options
usingComparator:(Function)aComparator
{
// FIXME: comparator is not a function
if (!aComparator)
_CPRaiseInvalidArgumentException(self, _cmd, "comparator is nil");
if ((options & CPBinarySearchingFirstEqual) && (options & CPBinarySearchingLastEqual))
_CPRaiseInvalidArgumentException(self, _cmd,
"both CPBinarySearchingFirstEqual and CPBinarySearchingLastEqual options cannot be specified");
var count = [self count];
if (count <= 0)
return (options & CPBinarySearchingInsertionIndex) ? 0 : CPNotFound;
var first = aRange ? aRange.location : 0,
last = (aRange ? CPMaxRange(aRange) : [self count]) - 1;
if (first < 0)
_CPRaiseRangeException(self, _cmd, first, count);
if (last >= count)
_CPRaiseRangeException(self, _cmd, last, count);
while (first <= last)
{
var middle = FLOOR((first + last) / 2),
result = aComparator(anObject, [self objectAtIndex:middle]);
if (result > 0)
first = middle + 1;
else if (result < 0)
last = middle - 1;
else
{
if (options & CPBinarySearchingFirstEqual)
while (middle > first && aComparator(anObject, [self objectAtIndex:middle - 1]) === CPOrderedSame)
--middle;
else if (options & CPBinarySearchingLastEqual)
{
while (middle < last && aComparator(anObject, [self objectAtIndex:middle + 1]) === CPOrderedSame)
++middle;
if (options & CPBinarySearchingInsertionIndex)
++middle;
}
return middle;
}
}
if (options & CPBinarySearchingInsertionIndex)
return MAX(first, 0);
return CPNotFound;
}
// Sending messages to elements
/*!
Sends each element in the array a message.
@param aSelector the selector of the message to send
@throws CPInvalidArgumentException if \c aSelector is \c nil
*/
- (void)makeObjectsPerformSelector:(SEL)aSelector
{
[self makeObjectsPerformSelector:aSelector withObjects:nil];
}
/*!
Sends each element in the array a message with an argument.
@param aSelector the selector of the message to send
@param anObject the first argument of the message
@throws CPInvalidArgumentException if \c aSelector is \c nil
*/
- (void)makeObjectsPerformSelector:(SEL)aSelector withObject:(id)anObject
{
return [self makeObjectsPerformSelector:aSelector withObjects:[anObject]];
}
- (void)makeObjectsPerformSelector:(SEL)aSelector withObjects:(CPArray)objects
{
if (!aSelector)
[CPException raise:CPInvalidArgumentException
reason:"makeObjectsPerformSelector:withObjects: 'aSelector' can't be nil"];
var index = 0,
count = [self count];
if ([objects count])
{
argumentsArray = [[nil, aSelector] arrayByAddingObjectsFromArray:objects];
for (; index < count; ++index)
{
argumentsArray[0] = [self objectAtIndex:index];
objj_msgSend.apply(this, argumentsArray);
}
}
else
for (; index < count; ++index)
objj_msgSend([self objectAtIndex:index], aSelector);
}
- (void)enumerateObjectsUsingBlock:(Function)aFunction
{
var index = 0,
count = [self count];
for (; index < count; ++index)
aFunction([self objectAtIndex:index], index);
}
// Comparing arrays
/*!
Returns the first object found in the receiver (starting at index 0) which is present in the
\c otherArray as determined by using the \c -containsObject: method.
@return the first object found, or \c nil if no common object was found.
*/
- (id)firstObjectCommonWithArray:(CPArray)anArray
{
var count = [self count];
if (![anArray count] || !count)
return nil;
var index = 0;
for (; index < count; ++index)
{
var object = [self objectAtIndex:index];
if ([anArray containsObject:object])
return object;
}
return nil;
}
/*!
Returns true if anArray contains exactly the same objects as the receiver.
*/
- (BOOL)isEqualToArray:(id)anArray
{
if (self === anArray)
return YES;
if (![anArray isKindOfClass:CPArray])
return NO;
var count = [self count],
otherCount = [anArray count];
if (anArray === nil || count !== otherCount)
return NO;
var index = 0;
for (; index < count; ++index)
{
var lhs = [self objectAtIndex:index],
rhs = [anArray objectAtIndex:index];
// If they're not equal, and either doesn't have an isa, or they're !isEqual (not isEqual)
if (lhs !== rhs && (lhs && !lhs.isa || rhs && !rhs.isa || ![lhs isEqual:rhs]))
return NO;
}
return YES;
}
- (BOOL)isEqual:(id)anObject
{
return (self === anObject) || [self isEqualToArray:anObject];
}
- (Array)_javaScriptArrayCopy
{
var index = 0,
count = [self count],
copy = [];
for (; index < count; ++index)
push.call(copy, [self objectAtIndex:index]);
return copy;
}
// Deriving new arrays
/*!
Returns a copy of this array plus \c anObject inside the copy.
@param anObject the object to be added to the array copy
@throws CPInvalidArgumentException if \c anObject is \c nil
@return a new array that should be n+1 in size compared to the receiver.
*/
- (CPArray)arrayByAddingObject:(id)anObject
{
var argumentArray = [self _javaScriptArrayCopy];
// We push instead of concat,because concat flattens arrays, so if the object
// passed in is an array, we end up with its contents added instead of itself.
push.call(argumentArray, anObject);
return objj_msgSend([self class], @selector(arrayWithArray:), argumentArray);
}
/*!
Returns a new array which is the concatenation of \c self and otherArray (in this precise order).
@param anArray the array that will be concatenated to the receiver's copy
*/
- (CPArray)arrayByAddingObjectsFromArray:(CPArray)anArray
{
if (!anArray)
return [self copy];
var anArray = anArray.isa === _CPJavaScriptArray ? anArray : [anArray _javaScriptArrayCopy],
argumentArray = concat.call([self _javaScriptArrayCopy], anArray);
return objj_msgSend([self class], @selector(arrayWithArray:), argumentArray);
}
/*
- (CPArray)filteredArrayUsingPredicate:(CPPredicate)aPredicate
{
var i= 0,
count = [self count],
array = [CPArray array];
for (; i<count; ++i)
if (aPredicate.evaluateWithObject(self[i]))
array.push(self[i]);
return array;
}
*/
/*!
Returns a subarray of the receiver containing the objects found in the specified range \c aRange.
@param aRange the range of objects to be copied into the subarray
@throws CPRangeException if the specified range exceeds the bounds of the array
*/
- (CPArray)subarrayWithRange:(CPRange)aRange
{
if (!aRange)
return [self copy];
if (aRange.location < 0 || CPMaxRange(aRange) > self.length)
[CPException raise:CPRangeException reason:"subarrayWithRange: aRange out of bounds"];
var index = aRange.location,
count = CPMaxRange(aRange),
argumentArray = [];
for (; index < count; ++index)
push.call(argumentArray, [self objectAtIndex:index]);
return objj_msgSend([self class], @selector(arrayWithArray:), argumentArray);
}
// Sorting arrays
/*
Not yet described.
*/
- (CPArray)sortedArrayUsingDescriptors:(CPArray)descriptors
{
var sorted = [self copy];
[sorted sortUsingDescriptors:descriptors];
return sorted;
}
/*!
Return a copy of the receiver sorted using the function passed into the first parameter.
*/
- (CPArray)sortedArrayUsingFunction:(Function)aFunction
{
return [self sortedArrayUsingFunction:aFunction context:nil];
}
/*!
Returns an array in which the objects are ordered according
to a sort with \c aFunction. This invokes
\c -sortUsingFunction:context.
@param aFunction a JavaScript 'Function' type that compares objects
@param aContext context information
@return a new sorted array
*/
- (CPArray)sortedArrayUsingFunction:(Function)aFunction context:(id)aContext
{
var sorted = [self copy];
[sorted sortUsingFunction:aFunction context:aContext];
return sorted;
}
/*!
Returns a new array in which the objects are ordered according to a sort with \c aSelector.
@param aSelector the selector that will perform object comparisons
*/
- (CPArray)sortedArrayUsingSelector:(SEL)aSelector
{
var sorted = [self copy];
[sorted sortUsingSelector:aSelector];
return sorted;
}
// Working with string elements
/*!
Returns a string formed by concatenating the objects in the
receiver, with the specified separator string inserted between each part.
If the element is a Objective-J object, then the \c -description
of that object will be used, otherwise the default JavaScript representation will be used.
@param aString the separator that will separate each object string
@return the string representation of the array
*/
- (CPString)componentsJoinedByString:(CPString)aString
{
return join.call([self _javaScriptArrayCopy], aString);
}
// Creating a description of the array
/*!
Returns a human readable description of this array and it's elements.
*/
- (CPString)description
{
var index = 0,
count = [self count],
description = '(';
for (; index < count; ++index)
{
if (index === 0)
description += '\n';
var object = [self objectAtIndex:index],
objectDescription = object && object.isa ? [object description] : String(object);
description += "\t" + objectDescription.split('\n').join("\n\t");
if (index !== count - 1)
description += ", ";
description += '\n';
}
return description + ')';
}
// Collecting paths
/*!
Returns a new array subset formed by selecting the elements that have
filename extensions from \c filterTypes. Only elements
that are of type CPString are candidates for inclusion in the returned array.
@param filterTypes an array of CPString objects that contain file extensions (without the '.')
@return a new array with matching paths
*/
- (CPArray)pathsMatchingExtensions:(CPArray)filterTypes
{
var index = 0,
count = [self count],
array = [];
for (; index < count; ++index)
if (self[index].isa && [self[index] isKindOfClass:[CPString class]] && [filterTypes containsObject:[self[index] pathExtension]])
array.push(self[index]);
return array;
}
// Copying arrays
/*!
Makes a copy of the receiver.
@return a new CPArray copy
*/
- (id)copy
{
return [[self class] arrayWithArray:self];
}
@end
@implementation CPArray (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
return [aCoder decodeObjectForKey:@"CP.objects"];
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder _encodeArrayOfObjects:self forKey:@"CP.objects"];
}
@end
/* @ignore */
@implementation _CPArrayEnumerator : CPEnumerator
{
CPArray _array;
int _index;
}
- (id)initWithArray:(CPArray)anArray
{
self = [super init];
if (self)
{
_array = anArray;
_index = -1;
}
return self;
}
- (id)nextObject
{
if (++_index >= [_array count])
return nil;
return [_array objectAtIndex:_index];
}
@end
/* @ignore */
@implementation _CPReverseArrayEnumerator : CPEnumerator
{
CPArray _array;
int _index;
}
- (id)initWithArray:(CPArray)anArray
{
self = [super init];
if (self)
{
_array = anArray;
_index = [_array count];
}
return self;
}
- (id)nextObject
{
if (--_index < 0)
return nil;
return [_array objectAtIndex:_index];
}
@end
var _CPSharedPlaceholderArray = nil;
@implementation _CPPlaceholderArray : CPArray
{
}
+ (id)alloc
{
if (!_CPSharedPlaceholderArray)
_CPSharedPlaceholderArray = [super alloc];
return _CPSharedPlaceholderArray;
}
@end
@import "_CPJavaScriptArray.j"