diff --git a/Foundation/CPPredicate/CPPredicate.j b/Foundation/CPPredicate/CPPredicate.j
index 9d778a28b..44c630821 100644
--- a/Foundation/CPPredicate/CPPredicate.j
+++ b/Foundation/CPPredicate/CPPredicate.j
@@ -36,21 +36,2918 @@
@class CPPredicate
@brief The CPPredicate class is used to define logical conditions used to constrain a search either for a fetch or for in-memory filtering.
- You use predicates to represent logical conditions, used for describing objects in persistent stores and in-memory filtering of objects. Although it is common to create predicates directly from instances of CPComparisonPredicate, CPCompoundPredicate, and CPExpression, you often create predicates from a format string which is parsed by the class methods on CPPredicate. Examples of predicate format strings include:
+ You use predicates to represent logical conditions, used for describing objects in persistent stores
+ and in-memory filtering of objects. Although it is common to create predicates directly from instances
+ of CPComparisonPredicate, CPCompoundPredicate, and CPExpression, you often create predicates from a
+ format string which is parsed by the class methods on CPPredicate. Examples of predicate format strings include:
- Simple comparisons, such as grade == "7" or firstName like "Shaffiq"\n
- Case/diacritic insensitive lookups, such as name contains[cd] "itroen"\n
- Logical operations, such as (firstName like "Mark") OR (lastName like "Adderley")\n
- “Between” predicates such as date between {$YESTERDAY, $TOMORROW}.\n
+ - Simple comparisons, such as grade == "7" or firstName like "Shaffiq"
+ - Case/diacritic insensitive lookups, such as name contains[cd] "itroen"
+ - Logical operations, such as (firstName like "Mark") OR (lastName like "Adderley")
+ - “Between” predicates such as date between {$YESTERDAY, $TOMORROW}.
You can create predicates for relationships, such as:
- group.name like "work*"\n
- ALL children.age > 12\n
- ANY children.age > 12\n
- You can create predicates for operations, such as @sum.items.price < 1000.
+ - group.name like "work*"
+ - ALL children.age > 12
+ - ANY children.age > 12
- You can also create predicates that include variables, so that the predicate can be pre-defined before substituting concrete values at runtime with evaluateWithObject:substitutionVariables: method.
+ You can create predicates for operations, such as @sum.items.price < 1000.
+
+ You can also create predicates that include variables, so that the predicate can be pre-defined before
+ substituting concrete values at runtime with the evaluateWithObject:substitutionVariables: method.
+*/
+
+@implementation CPPredicate : CPObject
+{
+}
+
+/*!
+ Creates and returns a new predicate formed by creating a new string with a given format and parsing the result.
+ @param format The format string for the new predicate.
+ @param … A comma-separated list of arguments to substitute into format.
+ @return A new predicate formed by creating a new string with format and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format, ...
+{
+ if (!format)
+ [CPException raise:CPInvalidArgumentException reason:_cmd + " the format can't be 'nil'"];
+
+ var args = Array.prototype.slice.call(arguments, 3);
+ return [self predicateWithFormat:arguments[2] argumentArray:args];
+}
+
+/*!
+ Creates and returns a new predicate by substituting the values in a given array into a format string and parsing the result.
+ @param format The format string for the new predicate.
+ @param arguments The arguments to substitute into predicateFormat. Values are substituted into predicateFormat in the order they appear in the array.
+ @return A new predicate by substituting the values in arguments into predicateFormat, and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format argumentArray:(CPArray)args
+{
+ if (!format)
+ [CPException raise:CPInvalidArgumentException reason:_cmd + " the format can't be 'nil'"];
+
+ var s = [[CPPredicateScanner alloc] initWithString:format args:args],
+ p = [s parse];
+
+ return p;
+}
+
+/*!
+ Creates and returns a new predicate by substituting the values in an argument list into a format string and parsing the result.
+ @param format The format string for the new predicate.
+ @param argList The arguments to substitute into predicateFormat. Values are substituted into predicateFormat in the order they appear in the argument list.
+ @return A new predicate by substituting the values in argList into predicateFormat and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format arguments:(va_list)argList
+{
+ // UNIMPLEMENTED
+ return nil;
+}
+
+/*!
+ Returns a copy of the receiver with the receiver’s variables substituted by values specified in a given substitution variables dictionary.
+ @param variables The substitution variables dictionary. The dictionary must contain key-value pairs for all variables in the receiver.
+ @return A copy of the receiver with the receiver’s variables substituted by values specified in variables.
+*/
+- (CPPredicate)predicateWithSubstitutionVariables:(CPDictionary)variables
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+/*!
+ Creates and returns a predicate that always evaluates to a given value.
+ @param value The value to which the new predicate should evaluate.
+ @return A predicate that always evaluates to value.
+*/
++ (CPPredicate)predicateWithValue:(BOOL)value
+{
+ return [[CPPredicate_BOOL alloc] initWithBool:value];
+}
+
+// Evaluating a Predicate
+/*!
+ Returns a Boolean value that indicates whether a given object matches the conditions specified by the receiver.
+ @param object The object against which to evaluate the receiver.
+ @return YES if object matches the conditions specified by the receiver, otherwise NO.
+*/
+- (BOOL)evaluateWithObject:(id)object
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+/*!
+ Returns a Boolean value that indicates whether a given object matches the conditions specified by the receiver after substituting in the values in a given variables dictionary.
+ @param object The object against which to evaluate the receiver.
+ @param variables The substitution variables dictionary. The dictionary must contain key-value pairs for all variables in the receiver.
+ @return YES if object matches the conditions specified by the receiver after substituting in the values in variables for any replacement tokens, otherwise NO.
+*/
+- (BOOL)evaluateWithObject:(id)object substitutionVariables:(CPDictionary)variables
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+// Getting Format Information
+/*!
+ Returns the receiver’s format string.
+ @return The receiver’s format string.
+*/
+- (CPString)predicateFormat
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+- (CPString)description
+{
+ return [self predicateFormat];
+}
+
+@end
+
+@implementation CPPredicate_BOOL : CPPredicate
+{
+ BOOL _value;
+}
+
+- (id)initWithBool:(BOOL)value
+{
+ _value = value;
+ return self;
+}
+
+- (BOOL)evaluateObject:(id)object
+{
+ return _value;
+}
+
+- (CPString)predicateFormat
+{
+ return (_value) ? @"TRUEPREDICATE" : @"FALSEPREDICATE";
+}
+
+@end
+
+
+@implementation CPArray (CPPredicate)
+
+- (CPArray)filteredArrayUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count],
+ result = [CPArray array],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var object = self[i];
+ if ([predicate evaluateWithObject:object])
+ result.push(object);
+ }
+
+ return result;
+}
+
+- (void)filterUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count];
+
+ while (count--)
+ {
+ if (![predicate evaluateWithObject:self[count]])
+ splice(count, 1);
+ }
+}
+
+@end
+
+@implementation CPSet (CPPredicate)
+
+- (CPSet)filteredSetUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count],
+ result = [CPSet set],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var object = [self objectAtIndex:i];
+
+ if ([predicate evaluateWithObject:object])
+ [result addObject:object];
+ }
+
+ return result;
+}
+
+- (void)filterUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count];
+
+ while (--count >= 0)
+ {
+ var object = [self objectAtIndex:count];
+
+ if (![predicate evaluateWithObject:object])
+ [self removeObjectAtIndex:count];
+ }
+}
+
+@end
+
+#define REFERENCE(variable) \
+function(newValue)\
+{\
+ var oldValue = variable;\
+ if (typeof newValue != 'undefined')\
+ variable = newValue;\
+ return oldValue;\
+}
+
+@implementation CPPredicateScanner : CPScanner
+{
+ CPEnumerator _args;
+ unsigned _retrieved;
+}
+
+- (id)initWithString:(CPString)format args:(CPArray)args
+{
+ self = [super initWithString:format]
+
+ if (self)
+ {
+ _args = [args objectEnumerator];
+ }
+ return self;
+}
+
+- (id)nextArg
+{
+ return [_args nextObject];
+}
+
+- (BOOL)scanPredicateKeyword:(CPString)key
+{
+ var loc = [self scanLocation];
+
+ [self setCaseSensitive:NO];
+ if (![self scanString:key intoString:NULL])
+ return NO;
+
+ if ([self isAtEnd])
+ return YES;
+
+ var c = [[self string] characterAtIndex:[self scanLocation]];
+ if (![[CPCharacterSet alphanumericCharacterSet] characterIsMember:c])
+ return YES;
+
+ [self setScanLocation:loc];
+
+ return NO;
+}
+
+- (CPPredicate)parse
+{
+ var r = nil;
+
+ try
+ {
+ [self setCharactersToBeSkipped:[CPCharacterSet whitespaceCharacterSet]];
+ r = [self parsePredicate];
+ }
+ catch(error)
+ {
+ CPLogConsole(@"Unable to parse predicate '" + [self string] + "' with " + error);
+ }
+ finally
+ {
+ if (![self isAtEnd])
+ {
+ var pstr = [self string],
+ loc = [self scanLocation];
+ CPLogConsole(@"Format string contains extra characters: '" + [pstr substringToIndex:loc] + "**" + [pstr substringFromIndex:loc] + "**'");
+ }
+ }
+
+ return r;
+}
+
+- (CPPredicate)parsePredicate
+{
+ return [self parseAnd];
+}
+
+- (CPPredicate)parseAnd
+{
+ var l = [self parseOr];
+
+ while ([self scanPredicateKeyword:@"AND"] || [self scanPredicateKeyword:@"&&"])
+ {
+ var r = [self parseOr];
+
+ if ([r isKindOfClass:[CPCompoundPredicate class]] && [r compoundPredicateType] == CPAndPredicateType)
+ {
+ if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPAndPredicateType)
+ {
+ [[l subpredicates] addObjectsFromArray:[r subpredicates]];
+ }
+ else
+ {
+ [[r subpredicates] insertObject:l atIndex:0];
+ l = r;
+ }
+ }
+ else if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPAndPredicateType)
+ {
+ [[l subpredicates] addObject:r];
+ }
+ else
+ {
+ l = [CPCompoundPredicate andPredicateWithSubpredicates:[CPArray arrayWithObjects:l, r]];
+ }
+ }
+ return l;
+}
+
+- (CPPredicate)parseNot
+{
+ if ([self scanString:@"(" intoString:NULL])
+ {
+ var r = [self parsePredicate];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"predicate");
+
+ return r;
+ }
+
+ if ([self scanPredicateKeyword:@"NOT"] || [self scanPredicateKeyword:@"!"])
+ {
+ return [CPCompoundPredicate notPredicateWithSubpredicate:[self parseNot]];
+ }
+ if ([self scanPredicateKeyword:@"TRUEPREDICATE"])
+ {
+ return [CPPredicate predicateWithValue:YES];
+ }
+ if ([self scanPredicateKeyword:@"FALSEPREDICATE"])
+ {
+ return [CPPredicate predicateWithValue:NO];
+ }
+
+ return [self parseComparison];
+}
+
+- (CPPredicate)parseOr
+{
+ var l = [self parseNot];
+ while ([self scanPredicateKeyword:@"OR"] || [self scanPredicateKeyword:@"||"])
+ {
+ var r = [self parseNot];
+
+ if ([r isKindOfClass:[CPCompoundPredicate class]] && [r compoundPredicateType] == CPOrPredicateType)
+ {
+ if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPOrPredicateType)
+ {
+ [[l subpredicates] addObjectsFromArray:[r subpredicates]];
+ }
+ else
+ {
+ [[r subpredicates] insertObject:l atIndex:0];
+ l = r;
+ }
+ }
+ else if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPOrPredicateType)
+ {
+ [[l subpredicates] addObject:r];
+ }
+ else
+ {
+ l = [CPCompoundPredicate orPredicateWithSubpredicates:[CPArray arrayWithObjects:l, r]];
+ }
+ }
+ return l;
+}
+
+- (CPPredicate)parseComparison
+{
+ var modifier = CPDirectPredicateModifier,
+ type = 0,
+ opts = 0,
+ left,
+ right,
+ p,
+ negate = NO;
+
+ if ([self scanPredicateKeyword:@"ANY"])
+ {
+ modifier = CPAnyPredicateModifier;
+ }
+ else if ([self scanPredicateKeyword:@"ALL"])
+ {
+ modifier = CPAllPredicateModifier;
+ }
+ else if ([self scanPredicateKeyword:@"NONE"])
+ {
+ modifier = CPAnyPredicateModifier;
+ negate = YES;
+ }
+ else if ([self scanPredicateKeyword:@"SOME"])
+ {
+ modifier = CPAllPredicateModifier;
+ negate = YES;
+ }
+
+ left = [self parseExpression];
+ if ([self scanString:@"!=" intoString:NULL] || [self scanString:@"<>" intoString:NULL])
+ {
+ type = CPNotEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@"<=" intoString:NULL] || [self scanString:@"=<" intoString:NULL])
+ {
+ type = CPLessThanOrEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@">=" intoString:NULL] || [self scanString:@"=>" intoString:NULL])
+ {
+ type = CPGreaterThanOrEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@"<" intoString:NULL])
+ {
+ type = CPLessThanPredicateOperatorType;
+ }
+ else if ([self scanString:@">" intoString:NULL])
+ {
+ type = CPGreaterThanPredicateOperatorType;
+ }
+ else if ([self scanString:@"==" intoString:NULL] || [self scanString:@"=" intoString:NULL])
+ {
+ type = CPEqualToPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"MATCHES"])
+ {
+ type = CPMatchesPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"LIKE"])
+ {
+ type = CPLikePredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"BEGINSWITH"])
+ {
+ type = CPBeginsWithPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"ENDSWITH"])
+ {
+ type = CPEndsWithPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"IN"])
+ {
+ type = CPInPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"CONTAINS"])
+ {
+ type = CPContainsPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"BETWEEN"])
+ {
+ type = CPBetweenPredicateOperatorType;
+ }
+ else
+ CPRaiseParseError(self, @"comparison predicate");
+
+ if ([self scanString:@"[cd]" intoString:NULL])
+ {
+ opts = CPCaseInsensitivePredicateOption | CPDiacriticInsensitivePredicateOption;
+ }
+ else if ([self scanString:@"[c]" intoString:NULL])
+ {
+ opts = CPCaseInsensitivePredicateOption;
+ }
+ else if ([self scanString:@"[d]" intoString:NULL])
+ {
+ opts = CPDiacriticInsensitivePredicateOption;
+ }
+
+ right = [self parseExpression];
+
+ p = [CPComparisonPredicate predicateWithLeftExpression:left
+ rightExpression:right
+ modifier:modifier
+ type:type
+ options:opts];
+
+ return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p]:p;
+}
+
+- (CPExpression)parseExpression
+{
+ return [self parseBinaryExpression];
+}
+
+- (CPExpression)parseSimpleExpression
+{
+ var identifier,
+ location,
+ ident,
+ dbl;
+
+ if ([self scanDouble:REFERENCE(dbl)])
+ return [CPExpression expressionForConstantValue:dbl];
+
+ // FIXME: handle integer, hex constants, 0x 0o 0b
+ if ([self scanString:@"-" intoString:NULL])
+ return [CPExpression expressionForFunction:@"chs:" arguments:[CPArray arrayWithObject:[self parseExpression]]];
+
+ if ([self scanString:@"(" intoString:NULL])
+ {
+ var arg = [self parseExpression];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return arg;
+ }
+
+ if ([self scanString:@"{" intoString:NULL])
+ {
+ if ([self scanString:@"}" intoString:NULL])
+ return [CPExpression expressionForConstantValue:a];
+
+ var a = [];
+
+ [a addObject:[self parseExpression]];
+
+ while ([self scanString:@"," intoString:NULL])
+ [a addObject:[self parseExpression]];
+
+ if (![self scanString:@"}" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForAggregate:a];
+ }
+
+ if ([self scanPredicateKeyword:@"NULL"] || [self scanPredicateKeyword:@"NIL"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNull null]];
+ }
+ if ([self scanPredicateKeyword:@"TRUE"] || [self scanPredicateKeyword:@"YES"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNumber numberWithBool:YES]];
+ }
+ if ([self scanPredicateKeyword:@"FALSE"] || [self scanPredicateKeyword:@"NO"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNumber numberWithBool:NO]];
+ }
+ if ([self scanPredicateKeyword:@"SELF"])
+ {
+ return [CPExpression expressionForEvaluatedObject];
+ }
+
+ if ([self scanString:@"$" intoString:NULL])
+ {
+ var variable = [self parseSimpleExpression];
+
+ if (![variable keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForVariable:variable];
+ }
+
+ location = [self scanLocation];
+
+ if ([self scanString:@"%" intoString:NULL])
+ {
+ if ([self isAtEnd] == NO)
+ {
+ var c = [[self string] characterAtIndex:[self scanLocation]];
+
+ switch (c)
+ {
+ case '%':// '%%' is treated as '%'
+ location = [self scanLocation];
+ break;
+ case 'K':
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForKeyPath:[self nextArg]];
+ case '@':
+ case 'c':
+ case 'C':
+ case 'd':
+ case 'D':
+ case 'i':
+ case 'o':
+ case 'O':
+ case 'u':
+ case 'U':
+ case 'x':
+ case 'X':
+ case 'e':
+ case 'E':
+ case 'f':
+ case 'g':
+ case 'G':
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ case 'h':
+ [self scanString:@"h" intoString:NULL];
+ if ([self isAtEnd] == NO)
+ {
+ c = [[self string] characterAtIndex:[self scanLocation]];
+ if (c == 'i' || c == 'u')
+ {
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ }
+ }
+ break;
+ case 'q':
+ [self scanString:@"q" intoString:NULL];
+ if ([self isAtEnd] == NO)
+ {
+ c = [[self string] characterAtIndex:[self scanLocation]];
+ if (c == 'i' || c == 'u' || c == 'x' || c == 'X')
+ {
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ }
+ }
+ break;
+ }
+ }
+
+ [self setScanLocation:location];
+ }
+
+ if ([self scanString:@"\"" intoString:NULL])
+ {
+ var skip = [self charactersToBeSkipped],
+ str = @"";
+
+ [self setCharactersToBeSkipped:nil];
+ [self scanUpToString:@"\"" intoString:REFERENCE(str)];
+
+ if ([self scanString:@"\"" intoString:NULL] == NO)
+ CPRaiseParseError(self, @"expression");
+
+ [self setCharactersToBeSkipped:skip];
+
+ return [CPExpression expressionForConstantValue:str];
+ }
+
+ if ([self scanString:@"'" intoString:NULL])
+ {
+ var skip = [self charactersToBeSkipped],
+ str = @"";
+
+ [self setCharactersToBeSkipped:nil];
+ [self scanUpToString:@"'" intoString:REFERENCE(str)];
+
+ if ([self scanString:@"'" intoString:NULL] == NO)
+ CPRaiseParseError(self, @"expression");
+
+ [self setCharactersToBeSkipped:skip];
+
+ return [CPExpression expressionForConstantValue:str];
+ }
+
+ if ([self scanString:@"@" intoString:NULL])
+ {
+ var e = [self parseExpression];
+
+ if (![e keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForKeyPath:[e keyPath] + "@"];
+ }
+
+ if ([self scanString:@"SUBQUERY" intoString:NULL])
+ {
+ if (![self scanString:@"(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ var collection = [self parseExpression],
+ variableExpression,
+ subpredicate;
+
+ if (![self scanString:@"," intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ variableExpression = [self parseExpression];
+
+ if (![self scanString:@"," intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ subpredicate = [self parsePredicate];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return [[CPExpression_subquery alloc] initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate];
+ }
+
+ if ([self scanString:@"FUNCTION" intoString:NULL])
+ {
+ if (![self scanString:@"(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ var args = [CPArray arrayWithObject:[self parseExpression]];
+ while ([self scanString:@"," intoString:NULL])
+ [args addObject:[self parseExpression]];
+
+ if (![self scanString:@")" intoString:NULL] || [args count] < 2 || [args[1] expressionType] != CPConstantValueExpressionType)
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForFunction:args[0] selectorName:[args[1] constantValue] arguments:args.slice(2)];
+ }
+
+ [self scanString:@"#" intoString:NULL];
+ if (!identifier)
+ identifier = [CPCharacterSet characterSetWithCharactersInString:@"_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"];
+
+ if (![self scanCharactersFromSet:identifier intoString:REFERENCE(ident)])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForKeyPath:ident];
+}
+
+- (CPExpression)parseFunctionalExpression
+{
+ var left = [self parseSimpleExpression];
+
+ while (YES)
+ {
+ if ([self scanString:@"." intoString:NULL])
+ {
+ var right = [self parseSimpleExpression],
+ expressionType = [right expressionType];
+
+ if (expressionType == CPKeyPathExpressionType)
+ left = [[CPExpression_keypath alloc] initWithOperand:left andKeyPath:[right keyPath]];
+ else if (expressionType == CPVariableExpressionType)
+ left = [CPExpression expressionForFunction:left selectorName:@"valueForKey:" arguments:[right]];
+ else
+ CPRaiseParseError(self, @"expression");
+ }
+ else if ([self scanString:@"[" intoString:NULL])
+ {
+ // index expression
+ if ([self scanPredicateKeyword:@"FIRST"])
+ {
+ left = [CPExpression expressionForFunction:@"first:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else if ([self scanPredicateKeyword:@"LAST"])
+ {
+ left = [CPExpression expressionForFunction:@"last:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else if ([self scanPredicateKeyword:@"SIZE"])
+ {
+ left = [CPExpression expressionForFunction:@"count:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else
+ {
+ var index = [self parseExpression];
+ left = [CPExpression expressionForFunction:@"fromObject:index:" arguments:[CPArray arrayWithObjects:left, index]];
+ }
+
+ if (![self scanString:@"]" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ }
+ else if ([self scanString:@":" intoString:NULL])
+ {
+ // function - this parser allows for (max)(a, b, c) to be properly
+ // recognized and even (%K)(a, b, c) if %K evaluates to "max"
+
+ if (![left keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ var selector = [left keyPath] + @":",
+ args = [];
+
+ if (![self scanString:@"(" intoString:NULL])
+ {
+ var str;
+ [self scanCharactersFromSet:[CPCharacterSet lowercaseLetterCharacterSet] intoString:REFERENCE(str)];
+
+ if (![self scanString:@":(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ selector += str + @":";
+ }
+
+ if (![self scanString:@")" intoString:NULL])
+ {
+ [args addObject:[self parseExpression]];
+ while ([self scanString:@"," intoString:NULL])
+ [args addObject:[self parseExpression]];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ }
+
+ left = [CPExpression expressionForFunction:selector arguments:args];
+ }
+ else if ([self scanString:@"UNION" intoString:NULL])
+ {
+ left = [CPExpression expressionForUnionSet:left with:[self parseExpression]];
+ }
+ else if ([self scanString:@"INTERSECT" intoString:NULL])
+ {
+ left = [CPExpression expressionForIntersectSet:left with:[self parseExpression]];
+ }
+ else if ([self scanString:@"MINUS" intoString:NULL])
+ {
+ left = [CPExpression expressionForMinusSet:left with:[self parseExpression]];
+ }
+ else
+ {
+ // done with suffixes
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parsePowerExpression
+{
+ var left = [self parseFunctionalExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"**" intoString:NULL])
+ {
+ right = [self parseFunctionalExpression];
+ left = [CPExpression expressionForFunction:@"raise:to:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseMultiplicationExpression
+{
+ var left = [self parsePowerExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"*" intoString:NULL])
+ {
+ right = [self parsePowerExpression];
+ left = [CPExpression expressionForFunction:@"multiply:by:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else if ([self scanString:@"/" intoString:NULL])
+ {
+ right = [self parsePowerExpression];
+ left = [CPExpression expressionForFunction:@"divide:by:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseAdditionExpression
+{
+ var left = [self parseMultiplicationExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"+" intoString:NULL])
+ {
+ right = [self parseMultiplicationExpression];
+ left = [CPExpression expressionForFunction:@"add:to:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else if ([self scanString:@"-" intoString:NULL])
+ {
+ right = [self parseMultiplicationExpression];
+ left = [CPExpression expressionForFunction:@"from:substract:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseBinaryExpression
+{
+ var left = [self parseAdditionExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@":=" intoString:NULL]) // assignment
+ {
+ // check left to be a variable?
+ right = [self parseAdditionExpression];
+ // FIXME
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+@end
+
+var CPRaiseParseError = function CPRaiseParseError(aScanner, target)
+{
+ [CPException raise:CPInvalidArgumentException reason:@"unable to parse " + target + " at index " + [aScanner scanLocation]];
+}
+
+@import "CPCompoundPredicate.j"
+@import "CPComparisonPredicate.j"
+@import "CPExpression.j"
+/*
+ * CPPredicate.j
+ *
+ * CPPredicate parsing based on NSPredicate.m in GNUStep Base Library (http://www.gnustep.org/)
+ * Copyright (c) 2005 Free Software Foundation.
+ *
+ * Created by cacaodev.
+ * Copyright 2010.
+ *
+ * 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 "CPArray.j"
+@import "CPException.j"
+@import "CPNull.j"
+@import "CPObject.j"
+@import "CPScanner.j"
+@import "CPSet.j"
+@import "CPValue.j"
+@import "CPCharacterSet.j"
+
+/*!
+ @ingroup foundation
+ @class CPPredicate
+ @brief The CPPredicate class is used to define logical conditions used to constrain a search either for a fetch or for in-memory filtering.
+
+ You use predicates to represent logical conditions, used for describing objects in persistent stores
+ and in-memory filtering of objects. Although it is common to create predicates directly from instances
+ of CPComparisonPredicate, CPCompoundPredicate, and CPExpression, you often create predicates from a
+ format string which is parsed by the class methods on CPPredicate. Examples of predicate format strings include:
+
+ - Simple comparisons, such as grade == "7" or firstName like "Shaffiq"
+ - Case/diacritic insensitive lookups, such as name contains[cd] "itroen"
+ - Logical operations, such as (firstName like "Mark") OR (lastName like "Adderley")
+ - “Between” predicates such as date between {$YESTERDAY, $TOMORROW}.
+
+ You can create predicates for relationships, such as:
+
+ - group.name like "work*"
+ - ALL children.age > 12
+ - ANY children.age > 12
+
+ You can create predicates for operations, such as @sum.items.price < 1000.
+
+ You can also create predicates that include variables, so that the predicate can be pre-defined before
+ substituting concrete values at runtime with the evaluateWithObject:substitutionVariables: method.
+*/
+
+@implementation CPPredicate : CPObject
+{
+}
+
+/*!
+ Creates and returns a new predicate formed by creating a new string with a given format and parsing the result.
+ @param format The format string for the new predicate.
+ @param … A comma-separated list of arguments to substitute into format.
+ @return A new predicate formed by creating a new string with format and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format, ...
+{
+ if (!format)
+ [CPException raise:CPInvalidArgumentException reason:_cmd + " the format can't be 'nil'"];
+
+ var args = Array.prototype.slice.call(arguments, 3);
+ return [self predicateWithFormat:arguments[2] argumentArray:args];
+}
+
+/*!
+ Creates and returns a new predicate by substituting the values in a given array into a format string and parsing the result.
+ @param format The format string for the new predicate.
+ @param arguments The arguments to substitute into predicateFormat. Values are substituted into predicateFormat in the order they appear in the array.
+ @return A new predicate by substituting the values in arguments into predicateFormat, and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format argumentArray:(CPArray)args
+{
+ if (!format)
+ [CPException raise:CPInvalidArgumentException reason:_cmd + " the format can't be 'nil'"];
+
+ var s = [[CPPredicateScanner alloc] initWithString:format args:args],
+ p = [s parse];
+
+ return p;
+}
+
+/*!
+ Creates and returns a new predicate by substituting the values in an argument list into a format string and parsing the result.
+ @param format The format string for the new predicate.
+ @param argList The arguments to substitute into predicateFormat. Values are substituted into predicateFormat in the order they appear in the argument list.
+ @return A new predicate by substituting the values in argList into predicateFormat and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format arguments:(va_list)argList
+{
+ // UNIMPLEMENTED
+ return nil;
+}
+
+/*!
+ Returns a copy of the receiver with the receiver’s variables substituted by values specified in a given substitution variables dictionary.
+ @param variables The substitution variables dictionary. The dictionary must contain key-value pairs for all variables in the receiver.
+ @return A copy of the receiver with the receiver’s variables substituted by values specified in variables.
+*/
+- (CPPredicate)predicateWithSubstitutionVariables:(CPDictionary)variables
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+/*!
+ Creates and returns a predicate that always evaluates to a given value.
+ @param value The value to which the new predicate should evaluate.
+ @return A predicate that always evaluates to value.
+*/
++ (CPPredicate)predicateWithValue:(BOOL)value
+{
+ return [[CPPredicate_BOOL alloc] initWithBool:value];
+}
+
+// Evaluating a Predicate
+/*!
+ Returns a Boolean value that indicates whether a given object matches the conditions specified by the receiver.
+ @param object The object against which to evaluate the receiver.
+ @return YES if object matches the conditions specified by the receiver, otherwise NO.
+*/
+- (BOOL)evaluateWithObject:(id)object
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+/*!
+ Returns a Boolean value that indicates whether a given object matches the conditions specified by the receiver after substituting in the values in a given variables dictionary.
+ @param object The object against which to evaluate the receiver.
+ @param variables The substitution variables dictionary. The dictionary must contain key-value pairs for all variables in the receiver.
+ @return YES if object matches the conditions specified by the receiver after substituting in the values in variables for any replacement tokens, otherwise NO.
+*/
+- (BOOL)evaluateWithObject:(id)object substitutionVariables:(CPDictionary)variables
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+// Getting Format Information
+/*!
+ Returns the receiver’s format string.
+ @return The receiver’s format string.
+*/
+- (CPString)predicateFormat
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+- (CPString)description
+{
+ return [self predicateFormat];
+}
+
+@end
+
+@implementation CPPredicate_BOOL : CPPredicate
+{
+ BOOL _value;
+}
+
+- (id)initWithBool:(BOOL)value
+{
+ _value = value;
+ return self;
+}
+
+- (BOOL)evaluateObject:(id)object
+{
+ return _value;
+}
+
+- (CPString)predicateFormat
+{
+ return (_value) ? @"TRUEPREDICATE" : @"FALSEPREDICATE";
+}
+
+@end
+
+
+@implementation CPArray (CPPredicate)
+
+- (CPArray)filteredArrayUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count],
+ result = [CPArray array],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var object = self[i];
+ if ([predicate evaluateWithObject:object])
+ result.push(object);
+ }
+
+ return result;
+}
+
+- (void)filterUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count];
+
+ while (count--)
+ {
+ if (![predicate evaluateWithObject:self[count]])
+ splice(count, 1);
+ }
+}
+
+@end
+
+@implementation CPSet (CPPredicate)
+
+- (CPSet)filteredSetUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count],
+ result = [CPSet set],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var object = [self objectAtIndex:i];
+
+ if ([predicate evaluateWithObject:object])
+ [result addObject:object];
+ }
+
+ return result;
+}
+
+- (void)filterUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count];
+
+ while (--count >= 0)
+ {
+ var object = [self objectAtIndex:count];
+
+ if (![predicate evaluateWithObject:object])
+ [self removeObjectAtIndex:count];
+ }
+}
+
+@end
+
+#define REFERENCE(variable) \
+function(newValue)\
+{\
+ var oldValue = variable;\
+ if (typeof newValue != 'undefined')\
+ variable = newValue;\
+ return oldValue;\
+}
+
+@implementation CPPredicateScanner : CPScanner
+{
+ CPEnumerator _args;
+ unsigned _retrieved;
+}
+
+- (id)initWithString:(CPString)format args:(CPArray)args
+{
+ self = [super initWithString:format]
+
+ if (self)
+ {
+ _args = [args objectEnumerator];
+ }
+ return self;
+}
+
+- (id)nextArg
+{
+ return [_args nextObject];
+}
+
+- (BOOL)scanPredicateKeyword:(CPString)key
+{
+ var loc = [self scanLocation];
+
+ [self setCaseSensitive:NO];
+ if (![self scanString:key intoString:NULL])
+ return NO;
+
+ if ([self isAtEnd])
+ return YES;
+
+ var c = [[self string] characterAtIndex:[self scanLocation]];
+ if (![[CPCharacterSet alphanumericCharacterSet] characterIsMember:c])
+ return YES;
+
+ [self setScanLocation:loc];
+
+ return NO;
+}
+
+- (CPPredicate)parse
+{
+ var r = nil;
+
+ try
+ {
+ [self setCharactersToBeSkipped:[CPCharacterSet whitespaceCharacterSet]];
+ r = [self parsePredicate];
+ }
+ catch(error)
+ {
+ CPLogConsole(@"Unable to parse predicate '" + [self string] + "' with " + error);
+ }
+ finally
+ {
+ if (![self isAtEnd])
+ {
+ var pstr = [self string],
+ loc = [self scanLocation];
+ CPLogConsole(@"Format string contains extra characters: '" + [pstr substringToIndex:loc] + "**" + [pstr substringFromIndex:loc] + "**'");
+ }
+ }
+
+ return r;
+}
+
+- (CPPredicate)parsePredicate
+{
+ return [self parseAnd];
+}
+
+- (CPPredicate)parseAnd
+{
+ var l = [self parseOr];
+
+ while ([self scanPredicateKeyword:@"AND"] || [self scanPredicateKeyword:@"&&"])
+ {
+ var r = [self parseOr];
+
+ if ([r isKindOfClass:[CPCompoundPredicate class]] && [r compoundPredicateType] == CPAndPredicateType)
+ {
+ if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPAndPredicateType)
+ {
+ [[l subpredicates] addObjectsFromArray:[r subpredicates]];
+ }
+ else
+ {
+ [[r subpredicates] insertObject:l atIndex:0];
+ l = r;
+ }
+ }
+ else if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPAndPredicateType)
+ {
+ [[l subpredicates] addObject:r];
+ }
+ else
+ {
+ l = [CPCompoundPredicate andPredicateWithSubpredicates:[CPArray arrayWithObjects:l, r]];
+ }
+ }
+ return l;
+}
+
+- (CPPredicate)parseNot
+{
+ if ([self scanString:@"(" intoString:NULL])
+ {
+ var r = [self parsePredicate];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"predicate");
+
+ return r;
+ }
+
+ if ([self scanPredicateKeyword:@"NOT"] || [self scanPredicateKeyword:@"!"])
+ {
+ return [CPCompoundPredicate notPredicateWithSubpredicate:[self parseNot]];
+ }
+ if ([self scanPredicateKeyword:@"TRUEPREDICATE"])
+ {
+ return [CPPredicate predicateWithValue:YES];
+ }
+ if ([self scanPredicateKeyword:@"FALSEPREDICATE"])
+ {
+ return [CPPredicate predicateWithValue:NO];
+ }
+
+ return [self parseComparison];
+}
+
+- (CPPredicate)parseOr
+{
+ var l = [self parseNot];
+ while ([self scanPredicateKeyword:@"OR"] || [self scanPredicateKeyword:@"||"])
+ {
+ var r = [self parseNot];
+
+ if ([r isKindOfClass:[CPCompoundPredicate class]] && [r compoundPredicateType] == CPOrPredicateType)
+ {
+ if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPOrPredicateType)
+ {
+ [[l subpredicates] addObjectsFromArray:[r subpredicates]];
+ }
+ else
+ {
+ [[r subpredicates] insertObject:l atIndex:0];
+ l = r;
+ }
+ }
+ else if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPOrPredicateType)
+ {
+ [[l subpredicates] addObject:r];
+ }
+ else
+ {
+ l = [CPCompoundPredicate orPredicateWithSubpredicates:[CPArray arrayWithObjects:l, r]];
+ }
+ }
+ return l;
+}
+
+- (CPPredicate)parseComparison
+{
+ var modifier = CPDirectPredicateModifier,
+ type = 0,
+ opts = 0,
+ left,
+ right,
+ p,
+ negate = NO;
+
+ if ([self scanPredicateKeyword:@"ANY"])
+ {
+ modifier = CPAnyPredicateModifier;
+ }
+ else if ([self scanPredicateKeyword:@"ALL"])
+ {
+ modifier = CPAllPredicateModifier;
+ }
+ else if ([self scanPredicateKeyword:@"NONE"])
+ {
+ modifier = CPAnyPredicateModifier;
+ negate = YES;
+ }
+ else if ([self scanPredicateKeyword:@"SOME"])
+ {
+ modifier = CPAllPredicateModifier;
+ negate = YES;
+ }
+
+ left = [self parseExpression];
+ if ([self scanString:@"!=" intoString:NULL] || [self scanString:@"<>" intoString:NULL])
+ {
+ type = CPNotEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@"<=" intoString:NULL] || [self scanString:@"=<" intoString:NULL])
+ {
+ type = CPLessThanOrEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@">=" intoString:NULL] || [self scanString:@"=>" intoString:NULL])
+ {
+ type = CPGreaterThanOrEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@"<" intoString:NULL])
+ {
+ type = CPLessThanPredicateOperatorType;
+ }
+ else if ([self scanString:@">" intoString:NULL])
+ {
+ type = CPGreaterThanPredicateOperatorType;
+ }
+ else if ([self scanString:@"==" intoString:NULL] || [self scanString:@"=" intoString:NULL])
+ {
+ type = CPEqualToPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"MATCHES"])
+ {
+ type = CPMatchesPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"LIKE"])
+ {
+ type = CPLikePredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"BEGINSWITH"])
+ {
+ type = CPBeginsWithPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"ENDSWITH"])
+ {
+ type = CPEndsWithPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"IN"])
+ {
+ type = CPInPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"CONTAINS"])
+ {
+ type = CPContainsPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"BETWEEN"])
+ {
+ type = CPBetweenPredicateOperatorType;
+ }
+ else
+ CPRaiseParseError(self, @"comparison predicate");
+
+ if ([self scanString:@"[cd]" intoString:NULL])
+ {
+ opts = CPCaseInsensitivePredicateOption | CPDiacriticInsensitivePredicateOption;
+ }
+ else if ([self scanString:@"[c]" intoString:NULL])
+ {
+ opts = CPCaseInsensitivePredicateOption;
+ }
+ else if ([self scanString:@"[d]" intoString:NULL])
+ {
+ opts = CPDiacriticInsensitivePredicateOption;
+ }
+
+ right = [self parseExpression];
+
+ p = [CPComparisonPredicate predicateWithLeftExpression:left
+ rightExpression:right
+ modifier:modifier
+ type:type
+ options:opts];
+
+ return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p]:p;
+}
+
+- (CPExpression)parseExpression
+{
+ return [self parseBinaryExpression];
+}
+
+- (CPExpression)parseSimpleExpression
+{
+ var identifier,
+ location,
+ ident,
+ dbl;
+
+ if ([self scanDouble:REFERENCE(dbl)])
+ return [CPExpression expressionForConstantValue:dbl];
+
+ // FIXME: handle integer, hex constants, 0x 0o 0b
+ if ([self scanString:@"-" intoString:NULL])
+ return [CPExpression expressionForFunction:@"chs:" arguments:[CPArray arrayWithObject:[self parseExpression]]];
+
+ if ([self scanString:@"(" intoString:NULL])
+ {
+ var arg = [self parseExpression];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return arg;
+ }
+
+ if ([self scanString:@"{" intoString:NULL])
+ {
+ if ([self scanString:@"}" intoString:NULL])
+ return [CPExpression expressionForConstantValue:a];
+
+ var a = [];
+
+ [a addObject:[self parseExpression]];
+
+ while ([self scanString:@"," intoString:NULL])
+ [a addObject:[self parseExpression]];
+
+ if (![self scanString:@"}" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForAggregate:a];
+ }
+
+ if ([self scanPredicateKeyword:@"NULL"] || [self scanPredicateKeyword:@"NIL"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNull null]];
+ }
+ if ([self scanPredicateKeyword:@"TRUE"] || [self scanPredicateKeyword:@"YES"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNumber numberWithBool:YES]];
+ }
+ if ([self scanPredicateKeyword:@"FALSE"] || [self scanPredicateKeyword:@"NO"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNumber numberWithBool:NO]];
+ }
+ if ([self scanPredicateKeyword:@"SELF"])
+ {
+ return [CPExpression expressionForEvaluatedObject];
+ }
+
+ if ([self scanString:@"$" intoString:NULL])
+ {
+ var variable = [self parseSimpleExpression];
+
+ if (![variable keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForVariable:variable];
+ }
+
+ location = [self scanLocation];
+
+ if ([self scanString:@"%" intoString:NULL])
+ {
+ if ([self isAtEnd] == NO)
+ {
+ var c = [[self string] characterAtIndex:[self scanLocation]];
+
+ switch (c)
+ {
+ case '%':// '%%' is treated as '%'
+ location = [self scanLocation];
+ break;
+ case 'K':
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForKeyPath:[self nextArg]];
+ case '@':
+ case 'c':
+ case 'C':
+ case 'd':
+ case 'D':
+ case 'i':
+ case 'o':
+ case 'O':
+ case 'u':
+ case 'U':
+ case 'x':
+ case 'X':
+ case 'e':
+ case 'E':
+ case 'f':
+ case 'g':
+ case 'G':
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ case 'h':
+ [self scanString:@"h" intoString:NULL];
+ if ([self isAtEnd] == NO)
+ {
+ c = [[self string] characterAtIndex:[self scanLocation]];
+ if (c == 'i' || c == 'u')
+ {
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ }
+ }
+ break;
+ case 'q':
+ [self scanString:@"q" intoString:NULL];
+ if ([self isAtEnd] == NO)
+ {
+ c = [[self string] characterAtIndex:[self scanLocation]];
+ if (c == 'i' || c == 'u' || c == 'x' || c == 'X')
+ {
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ }
+ }
+ break;
+ }
+ }
+
+ [self setScanLocation:location];
+ }
+
+ if ([self scanString:@"\"" intoString:NULL])
+ {
+ var skip = [self charactersToBeSkipped],
+ str = @"";
+
+ [self setCharactersToBeSkipped:nil];
+ [self scanUpToString:@"\"" intoString:REFERENCE(str)];
+
+ if ([self scanString:@"\"" intoString:NULL] == NO)
+ CPRaiseParseError(self, @"expression");
+
+ [self setCharactersToBeSkipped:skip];
+
+ return [CPExpression expressionForConstantValue:str];
+ }
+
+ if ([self scanString:@"'" intoString:NULL])
+ {
+ var skip = [self charactersToBeSkipped],
+ str = @"";
+
+ [self setCharactersToBeSkipped:nil];
+ [self scanUpToString:@"'" intoString:REFERENCE(str)];
+
+ if ([self scanString:@"'" intoString:NULL] == NO)
+ CPRaiseParseError(self, @"expression");
+
+ [self setCharactersToBeSkipped:skip];
+
+ return [CPExpression expressionForConstantValue:str];
+ }
+
+ if ([self scanString:@"@" intoString:NULL])
+ {
+ var e = [self parseExpression];
+
+ if (![e keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForKeyPath:[e keyPath] + "@"];
+ }
+
+ if ([self scanString:@"SUBQUERY" intoString:NULL])
+ {
+ if (![self scanString:@"(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ var collection = [self parseExpression],
+ variableExpression,
+ subpredicate;
+
+ if (![self scanString:@"," intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ variableExpression = [self parseExpression];
+
+ if (![self scanString:@"," intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ subpredicate = [self parsePredicate];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return [[CPExpression_subquery alloc] initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate];
+ }
+
+ if ([self scanString:@"FUNCTION" intoString:NULL])
+ {
+ if (![self scanString:@"(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ var args = [CPArray arrayWithObject:[self parseExpression]];
+ while ([self scanString:@"," intoString:NULL])
+ [args addObject:[self parseExpression]];
+
+ if (![self scanString:@")" intoString:NULL] || [args count] < 2 || [args[1] expressionType] != CPConstantValueExpressionType)
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForFunction:args[0] selectorName:[args[1] constantValue] arguments:args.slice(2)];
+ }
+
+ [self scanString:@"#" intoString:NULL];
+ if (!identifier)
+ identifier = [CPCharacterSet characterSetWithCharactersInString:@"_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"];
+
+ if (![self scanCharactersFromSet:identifier intoString:REFERENCE(ident)])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForKeyPath:ident];
+}
+
+- (CPExpression)parseFunctionalExpression
+{
+ var left = [self parseSimpleExpression];
+
+ while (YES)
+ {
+ if ([self scanString:@"." intoString:NULL])
+ {
+ var right = [self parseSimpleExpression],
+ expressionType = [right expressionType];
+
+ if (expressionType == CPKeyPathExpressionType)
+ left = [[CPExpression_keypath alloc] initWithOperand:left andKeyPath:[right keyPath]];
+ else if (expressionType == CPVariableExpressionType)
+ left = [CPExpression expressionForFunction:left selectorName:@"valueForKey:" arguments:[right]];
+ else
+ CPRaiseParseError(self, @"expression");
+ }
+ else if ([self scanString:@"[" intoString:NULL])
+ {
+ // index expression
+ if ([self scanPredicateKeyword:@"FIRST"])
+ {
+ left = [CPExpression expressionForFunction:@"first:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else if ([self scanPredicateKeyword:@"LAST"])
+ {
+ left = [CPExpression expressionForFunction:@"last:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else if ([self scanPredicateKeyword:@"SIZE"])
+ {
+ left = [CPExpression expressionForFunction:@"count:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else
+ {
+ var index = [self parseExpression];
+ left = [CPExpression expressionForFunction:@"fromObject:index:" arguments:[CPArray arrayWithObjects:left, index]];
+ }
+
+ if (![self scanString:@"]" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ }
+ else if ([self scanString:@":" intoString:NULL])
+ {
+ // function - this parser allows for (max)(a, b, c) to be properly
+ // recognized and even (%K)(a, b, c) if %K evaluates to "max"
+
+ if (![left keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ var selector = [left keyPath] + @":",
+ args = [];
+
+ if (![self scanString:@"(" intoString:NULL])
+ {
+ var str;
+ [self scanCharactersFromSet:[CPCharacterSet lowercaseLetterCharacterSet] intoString:REFERENCE(str)];
+
+ if (![self scanString:@":(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ selector += str + @":";
+ }
+
+ if (![self scanString:@")" intoString:NULL])
+ {
+ [args addObject:[self parseExpression]];
+ while ([self scanString:@"," intoString:NULL])
+ [args addObject:[self parseExpression]];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ }
+
+ left = [CPExpression expressionForFunction:selector arguments:args];
+ }
+ else if ([self scanString:@"UNION" intoString:NULL])
+ {
+ left = [CPExpression expressionForUnionSet:left with:[self parseExpression]];
+ }
+ else if ([self scanString:@"INTERSECT" intoString:NULL])
+ {
+ left = [CPExpression expressionForIntersectSet:left with:[self parseExpression]];
+ }
+ else if ([self scanString:@"MINUS" intoString:NULL])
+ {
+ left = [CPExpression expressionForMinusSet:left with:[self parseExpression]];
+ }
+ else
+ {
+ // done with suffixes
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parsePowerExpression
+{
+ var left = [self parseFunctionalExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"**" intoString:NULL])
+ {
+ right = [self parseFunctionalExpression];
+ left = [CPExpression expressionForFunction:@"raise:to:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseMultiplicationExpression
+{
+ var left = [self parsePowerExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"*" intoString:NULL])
+ {
+ right = [self parsePowerExpression];
+ left = [CPExpression expressionForFunction:@"multiply:by:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else if ([self scanString:@"/" intoString:NULL])
+ {
+ right = [self parsePowerExpression];
+ left = [CPExpression expressionForFunction:@"divide:by:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseAdditionExpression
+{
+ var left = [self parseMultiplicationExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"+" intoString:NULL])
+ {
+ right = [self parseMultiplicationExpression];
+ left = [CPExpression expressionForFunction:@"add:to:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else if ([self scanString:@"-" intoString:NULL])
+ {
+ right = [self parseMultiplicationExpression];
+ left = [CPExpression expressionForFunction:@"from:substract:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseBinaryExpression
+{
+ var left = [self parseAdditionExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@":=" intoString:NULL]) // assignment
+ {
+ // check left to be a variable?
+ right = [self parseAdditionExpression];
+ // FIXME
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+@end
+
+var CPRaiseParseError = function CPRaiseParseError(aScanner, target)
+{
+ [CPException raise:CPInvalidArgumentException reason:@"unable to parse " + target + " at index " + [aScanner scanLocation]];
+}
+
+@import "CPCompoundPredicate.j"
+@import "CPComparisonPredicate.j"
+@import "CPExpression.j"
+/*
+ * CPPredicate.j
+ *
+ * CPPredicate parsing based on NSPredicate.m in GNUStep Base Library (http://www.gnustep.org/)
+ * Copyright (c) 2005 Free Software Foundation.
+ *
+ * Created by cacaodev.
+ * Copyright 2010.
+ *
+ * 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 "CPArray.j"
+@import "CPException.j"
+@import "CPNull.j"
+@import "CPObject.j"
+@import "CPScanner.j"
+@import "CPSet.j"
+@import "CPValue.j"
+@import "CPCharacterSet.j"
+
+/*!
+ @ingroup foundation
+ @class CPPredicate
+ @brief The CPPredicate class is used to define logical conditions used to constrain a search either for a fetch or for in-memory filtering.
+
+ You use predicates to represent logical conditions, used for describing objects in persistent stores
+ and in-memory filtering of objects. Although it is common to create predicates directly from instances
+ of CPComparisonPredicate, CPCompoundPredicate, and CPExpression, you often create predicates from a
+ format string which is parsed by the class methods on CPPredicate. Examples of predicate format strings include:
+
+ - Simple comparisons, such as grade == "7" or firstName like "Shaffiq"
+ - Case/diacritic insensitive lookups, such as name contains[cd] "itroen"
+ - Logical operations, such as (firstName like "Mark") OR (lastName like "Adderley")
+ - “Between” predicates such as date between {$YESTERDAY, $TOMORROW}.
+
+ You can create predicates for relationships, such as:
+
+ - group.name like "work*"
+ - ALL children.age > 12
+ - ANY children.age > 12
+
+ You can create predicates for operations, such as @sum.items.price < 1000.
+
+ You can also create predicates that include variables, so that the predicate can be pre-defined before
+ substituting concrete values at runtime with the evaluateWithObject:substitutionVariables: method.
+*/
+
+@implementation CPPredicate : CPObject
+{
+}
+
+/*!
+ Creates and returns a new predicate formed by creating a new string with a given format and parsing the result.
+ @param format The format string for the new predicate.
+ @param … A comma-separated list of arguments to substitute into format.
+ @return A new predicate formed by creating a new string with format and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format, ...
+{
+ if (!format)
+ [CPException raise:CPInvalidArgumentException reason:_cmd + " the format can't be 'nil'"];
+
+ var args = Array.prototype.slice.call(arguments, 3);
+ return [self predicateWithFormat:arguments[2] argumentArray:args];
+}
+
+/*!
+ Creates and returns a new predicate by substituting the values in a given array into a format string and parsing the result.
+ @param format The format string for the new predicate.
+ @param arguments The arguments to substitute into predicateFormat. Values are substituted into predicateFormat in the order they appear in the array.
+ @return A new predicate by substituting the values in arguments into predicateFormat, and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format argumentArray:(CPArray)args
+{
+ if (!format)
+ [CPException raise:CPInvalidArgumentException reason:_cmd + " the format can't be 'nil'"];
+
+ var s = [[CPPredicateScanner alloc] initWithString:format args:args],
+ p = [s parse];
+
+ return p;
+}
+
+/*!
+ Creates and returns a new predicate by substituting the values in an argument list into a format string and parsing the result.
+ @param format The format string for the new predicate.
+ @param argList The arguments to substitute into predicateFormat. Values are substituted into predicateFormat in the order they appear in the argument list.
+ @return A new predicate by substituting the values in argList into predicateFormat and parsing the result.
+*/
++ (CPPredicate)predicateWithFormat:(CPString)format arguments:(va_list)argList
+{
+ // UNIMPLEMENTED
+ return nil;
+}
+
+/*!
+ Returns a copy of the receiver with the receiver’s variables substituted by values specified in a given substitution variables dictionary.
+ @param variables The substitution variables dictionary. The dictionary must contain key-value pairs for all variables in the receiver.
+ @return A copy of the receiver with the receiver’s variables substituted by values specified in variables.
+*/
+- (CPPredicate)predicateWithSubstitutionVariables:(CPDictionary)variables
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+/*!
+ Creates and returns a predicate that always evaluates to a given value.
+ @param value The value to which the new predicate should evaluate.
+ @return A predicate that always evaluates to value.
+*/
++ (CPPredicate)predicateWithValue:(BOOL)value
+{
+ return [[CPPredicate_BOOL alloc] initWithBool:value];
+}
+
+// Evaluating a Predicate
+/*!
+ Returns a Boolean value that indicates whether a given object matches the conditions specified by the receiver.
+ @param object The object against which to evaluate the receiver.
+ @return YES if object matches the conditions specified by the receiver, otherwise NO.
+*/
+- (BOOL)evaluateWithObject:(id)object
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+/*!
+ Returns a Boolean value that indicates whether a given object matches the conditions specified by the receiver after substituting in the values in a given variables dictionary.
+ @param object The object against which to evaluate the receiver.
+ @param variables The substitution variables dictionary. The dictionary must contain key-value pairs for all variables in the receiver.
+ @return YES if object matches the conditions specified by the receiver after substituting in the values in variables for any replacement tokens, otherwise NO.
+*/
+- (BOOL)evaluateWithObject:(id)object substitutionVariables:(CPDictionary)variables
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+// Getting Format Information
+/*!
+ Returns the receiver’s format string.
+ @return The receiver’s format string.
+*/
+- (CPString)predicateFormat
+{
+ // IMPLEMENTED BY SUBCLASSES
+}
+
+- (CPString)description
+{
+ return [self predicateFormat];
+}
+
+@end
+
+@implementation CPPredicate_BOOL : CPPredicate
+{
+ BOOL _value;
+}
+
+- (id)initWithBool:(BOOL)value
+{
+ _value = value;
+ return self;
+}
+
+- (BOOL)evaluateObject:(id)object
+{
+ return _value;
+}
+
+- (CPString)predicateFormat
+{
+ return (_value) ? @"TRUEPREDICATE" : @"FALSEPREDICATE";
+}
+
+@end
+
+
+@implementation CPArray (CPPredicate)
+
+- (CPArray)filteredArrayUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count],
+ result = [CPArray array],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var object = self[i];
+ if ([predicate evaluateWithObject:object])
+ result.push(object);
+ }
+
+ return result;
+}
+
+- (void)filterUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count];
+
+ while (count--)
+ {
+ if (![predicate evaluateWithObject:self[count]])
+ splice(count, 1);
+ }
+}
+
+@end
+
+@implementation CPSet (CPPredicate)
+
+- (CPSet)filteredSetUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count],
+ result = [CPSet set],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var object = [self objectAtIndex:i];
+
+ if ([predicate evaluateWithObject:object])
+ [result addObject:object];
+ }
+
+ return result;
+}
+
+- (void)filterUsingPredicate:(CPPredicate)predicate
+{
+ var count = [self count];
+
+ while (--count >= 0)
+ {
+ var object = [self objectAtIndex:count];
+
+ if (![predicate evaluateWithObject:object])
+ [self removeObjectAtIndex:count];
+ }
+}
+
+@end
+
+#define REFERENCE(variable) \
+function(newValue)\
+{\
+ var oldValue = variable;\
+ if (typeof newValue != 'undefined')\
+ variable = newValue;\
+ return oldValue;\
+}
+
+@implementation CPPredicateScanner : CPScanner
+{
+ CPEnumerator _args;
+ unsigned _retrieved;
+}
+
+- (id)initWithString:(CPString)format args:(CPArray)args
+{
+ self = [super initWithString:format]
+
+ if (self)
+ {
+ _args = [args objectEnumerator];
+ }
+ return self;
+}
+
+- (id)nextArg
+{
+ return [_args nextObject];
+}
+
+- (BOOL)scanPredicateKeyword:(CPString)key
+{
+ var loc = [self scanLocation];
+
+ [self setCaseSensitive:NO];
+ if (![self scanString:key intoString:NULL])
+ return NO;
+
+ if ([self isAtEnd])
+ return YES;
+
+ var c = [[self string] characterAtIndex:[self scanLocation]];
+ if (![[CPCharacterSet alphanumericCharacterSet] characterIsMember:c])
+ return YES;
+
+ [self setScanLocation:loc];
+
+ return NO;
+}
+
+- (CPPredicate)parse
+{
+ var r = nil;
+
+ try
+ {
+ [self setCharactersToBeSkipped:[CPCharacterSet whitespaceCharacterSet]];
+ r = [self parsePredicate];
+ }
+ catch(error)
+ {
+ CPLogConsole(@"Unable to parse predicate '" + [self string] + "' with " + error);
+ }
+ finally
+ {
+ if (![self isAtEnd])
+ {
+ var pstr = [self string],
+ loc = [self scanLocation];
+ CPLogConsole(@"Format string contains extra characters: '" + [pstr substringToIndex:loc] + "**" + [pstr substringFromIndex:loc] + "**'");
+ }
+ }
+
+ return r;
+}
+
+- (CPPredicate)parsePredicate
+{
+ return [self parseAnd];
+}
+
+- (CPPredicate)parseAnd
+{
+ var l = [self parseOr];
+
+ while ([self scanPredicateKeyword:@"AND"] || [self scanPredicateKeyword:@"&&"])
+ {
+ var r = [self parseOr];
+
+ if ([r isKindOfClass:[CPCompoundPredicate class]] && [r compoundPredicateType] == CPAndPredicateType)
+ {
+ if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPAndPredicateType)
+ {
+ [[l subpredicates] addObjectsFromArray:[r subpredicates]];
+ }
+ else
+ {
+ [[r subpredicates] insertObject:l atIndex:0];
+ l = r;
+ }
+ }
+ else if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPAndPredicateType)
+ {
+ [[l subpredicates] addObject:r];
+ }
+ else
+ {
+ l = [CPCompoundPredicate andPredicateWithSubpredicates:[CPArray arrayWithObjects:l, r]];
+ }
+ }
+ return l;
+}
+
+- (CPPredicate)parseNot
+{
+ if ([self scanString:@"(" intoString:NULL])
+ {
+ var r = [self parsePredicate];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"predicate");
+
+ return r;
+ }
+
+ if ([self scanPredicateKeyword:@"NOT"] || [self scanPredicateKeyword:@"!"])
+ {
+ return [CPCompoundPredicate notPredicateWithSubpredicate:[self parseNot]];
+ }
+ if ([self scanPredicateKeyword:@"TRUEPREDICATE"])
+ {
+ return [CPPredicate predicateWithValue:YES];
+ }
+ if ([self scanPredicateKeyword:@"FALSEPREDICATE"])
+ {
+ return [CPPredicate predicateWithValue:NO];
+ }
+
+ return [self parseComparison];
+}
+
+- (CPPredicate)parseOr
+{
+ var l = [self parseNot];
+ while ([self scanPredicateKeyword:@"OR"] || [self scanPredicateKeyword:@"||"])
+ {
+ var r = [self parseNot];
+
+ if ([r isKindOfClass:[CPCompoundPredicate class]] && [r compoundPredicateType] == CPOrPredicateType)
+ {
+ if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPOrPredicateType)
+ {
+ [[l subpredicates] addObjectsFromArray:[r subpredicates]];
+ }
+ else
+ {
+ [[r subpredicates] insertObject:l atIndex:0];
+ l = r;
+ }
+ }
+ else if ([l isKindOfClass:[CPCompoundPredicate class]] && [l compoundPredicateType] == CPOrPredicateType)
+ {
+ [[l subpredicates] addObject:r];
+ }
+ else
+ {
+ l = [CPCompoundPredicate orPredicateWithSubpredicates:[CPArray arrayWithObjects:l, r]];
+ }
+ }
+ return l;
+}
+
+- (CPPredicate)parseComparison
+{
+ var modifier = CPDirectPredicateModifier,
+ type = 0,
+ opts = 0,
+ left,
+ right,
+ p,
+ negate = NO;
+
+ if ([self scanPredicateKeyword:@"ANY"])
+ {
+ modifier = CPAnyPredicateModifier;
+ }
+ else if ([self scanPredicateKeyword:@"ALL"])
+ {
+ modifier = CPAllPredicateModifier;
+ }
+ else if ([self scanPredicateKeyword:@"NONE"])
+ {
+ modifier = CPAnyPredicateModifier;
+ negate = YES;
+ }
+ else if ([self scanPredicateKeyword:@"SOME"])
+ {
+ modifier = CPAllPredicateModifier;
+ negate = YES;
+ }
+
+ left = [self parseExpression];
+ if ([self scanString:@"!=" intoString:NULL] || [self scanString:@"<>" intoString:NULL])
+ {
+ type = CPNotEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@"<=" intoString:NULL] || [self scanString:@"=<" intoString:NULL])
+ {
+ type = CPLessThanOrEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@">=" intoString:NULL] || [self scanString:@"=>" intoString:NULL])
+ {
+ type = CPGreaterThanOrEqualToPredicateOperatorType;
+ }
+ else if ([self scanString:@"<" intoString:NULL])
+ {
+ type = CPLessThanPredicateOperatorType;
+ }
+ else if ([self scanString:@">" intoString:NULL])
+ {
+ type = CPGreaterThanPredicateOperatorType;
+ }
+ else if ([self scanString:@"==" intoString:NULL] || [self scanString:@"=" intoString:NULL])
+ {
+ type = CPEqualToPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"MATCHES"])
+ {
+ type = CPMatchesPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"LIKE"])
+ {
+ type = CPLikePredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"BEGINSWITH"])
+ {
+ type = CPBeginsWithPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"ENDSWITH"])
+ {
+ type = CPEndsWithPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"IN"])
+ {
+ type = CPInPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"CONTAINS"])
+ {
+ type = CPContainsPredicateOperatorType;
+ }
+ else if ([self scanPredicateKeyword:@"BETWEEN"])
+ {
+ type = CPBetweenPredicateOperatorType;
+ }
+ else
+ CPRaiseParseError(self, @"comparison predicate");
+
+ if ([self scanString:@"[cd]" intoString:NULL])
+ {
+ opts = CPCaseInsensitivePredicateOption | CPDiacriticInsensitivePredicateOption;
+ }
+ else if ([self scanString:@"[c]" intoString:NULL])
+ {
+ opts = CPCaseInsensitivePredicateOption;
+ }
+ else if ([self scanString:@"[d]" intoString:NULL])
+ {
+ opts = CPDiacriticInsensitivePredicateOption;
+ }
+
+ right = [self parseExpression];
+
+ p = [CPComparisonPredicate predicateWithLeftExpression:left
+ rightExpression:right
+ modifier:modifier
+ type:type
+ options:opts];
+
+ return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p]:p;
+}
+
+- (CPExpression)parseExpression
+{
+ return [self parseBinaryExpression];
+}
+
+- (CPExpression)parseSimpleExpression
+{
+ var identifier,
+ location,
+ ident,
+ dbl;
+
+ if ([self scanDouble:REFERENCE(dbl)])
+ return [CPExpression expressionForConstantValue:dbl];
+
+ // FIXME: handle integer, hex constants, 0x 0o 0b
+ if ([self scanString:@"-" intoString:NULL])
+ return [CPExpression expressionForFunction:@"chs:" arguments:[CPArray arrayWithObject:[self parseExpression]]];
+
+ if ([self scanString:@"(" intoString:NULL])
+ {
+ var arg = [self parseExpression];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return arg;
+ }
+
+ if ([self scanString:@"{" intoString:NULL])
+ {
+ if ([self scanString:@"}" intoString:NULL])
+ return [CPExpression expressionForConstantValue:a];
+
+ var a = [];
+
+ [a addObject:[self parseExpression]];
+
+ while ([self scanString:@"," intoString:NULL])
+ [a addObject:[self parseExpression]];
+
+ if (![self scanString:@"}" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForAggregate:a];
+ }
+
+ if ([self scanPredicateKeyword:@"NULL"] || [self scanPredicateKeyword:@"NIL"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNull null]];
+ }
+ if ([self scanPredicateKeyword:@"TRUE"] || [self scanPredicateKeyword:@"YES"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNumber numberWithBool:YES]];
+ }
+ if ([self scanPredicateKeyword:@"FALSE"] || [self scanPredicateKeyword:@"NO"])
+ {
+ return [CPExpression expressionForConstantValue:[CPNumber numberWithBool:NO]];
+ }
+ if ([self scanPredicateKeyword:@"SELF"])
+ {
+ return [CPExpression expressionForEvaluatedObject];
+ }
+
+ if ([self scanString:@"$" intoString:NULL])
+ {
+ var variable = [self parseSimpleExpression];
+
+ if (![variable keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForVariable:variable];
+ }
+
+ location = [self scanLocation];
+
+ if ([self scanString:@"%" intoString:NULL])
+ {
+ if ([self isAtEnd] == NO)
+ {
+ var c = [[self string] characterAtIndex:[self scanLocation]];
+
+ switch (c)
+ {
+ case '%':// '%%' is treated as '%'
+ location = [self scanLocation];
+ break;
+ case 'K':
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForKeyPath:[self nextArg]];
+ case '@':
+ case 'c':
+ case 'C':
+ case 'd':
+ case 'D':
+ case 'i':
+ case 'o':
+ case 'O':
+ case 'u':
+ case 'U':
+ case 'x':
+ case 'X':
+ case 'e':
+ case 'E':
+ case 'f':
+ case 'g':
+ case 'G':
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ case 'h':
+ [self scanString:@"h" intoString:NULL];
+ if ([self isAtEnd] == NO)
+ {
+ c = [[self string] characterAtIndex:[self scanLocation]];
+ if (c == 'i' || c == 'u')
+ {
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ }
+ }
+ break;
+ case 'q':
+ [self scanString:@"q" intoString:NULL];
+ if ([self isAtEnd] == NO)
+ {
+ c = [[self string] characterAtIndex:[self scanLocation]];
+ if (c == 'i' || c == 'u' || c == 'x' || c == 'X')
+ {
+ [self setScanLocation:[self scanLocation] + 1];
+ return [CPExpression expressionForConstantValue:[self nextArg]];
+ }
+ }
+ break;
+ }
+ }
+
+ [self setScanLocation:location];
+ }
+
+ if ([self scanString:@"\"" intoString:NULL])
+ {
+ var skip = [self charactersToBeSkipped],
+ str = @"";
+
+ [self setCharactersToBeSkipped:nil];
+ [self scanUpToString:@"\"" intoString:REFERENCE(str)];
+
+ if ([self scanString:@"\"" intoString:NULL] == NO)
+ CPRaiseParseError(self, @"expression");
+
+ [self setCharactersToBeSkipped:skip];
+
+ return [CPExpression expressionForConstantValue:str];
+ }
+
+ if ([self scanString:@"'" intoString:NULL])
+ {
+ var skip = [self charactersToBeSkipped],
+ str = @"";
+
+ [self setCharactersToBeSkipped:nil];
+ [self scanUpToString:@"'" intoString:REFERENCE(str)];
+
+ if ([self scanString:@"'" intoString:NULL] == NO)
+ CPRaiseParseError(self, @"expression");
+
+ [self setCharactersToBeSkipped:skip];
+
+ return [CPExpression expressionForConstantValue:str];
+ }
+
+ if ([self scanString:@"@" intoString:NULL])
+ {
+ var e = [self parseExpression];
+
+ if (![e keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForKeyPath:[e keyPath] + "@"];
+ }
+
+ if ([self scanString:@"SUBQUERY" intoString:NULL])
+ {
+ if (![self scanString:@"(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ var collection = [self parseExpression],
+ variableExpression,
+ subpredicate;
+
+ if (![self scanString:@"," intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ variableExpression = [self parseExpression];
+
+ if (![self scanString:@"," intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ subpredicate = [self parsePredicate];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ return [[CPExpression_subquery alloc] initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate];
+ }
+
+ if ([self scanString:@"FUNCTION" intoString:NULL])
+ {
+ if (![self scanString:@"(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ var args = [CPArray arrayWithObject:[self parseExpression]];
+ while ([self scanString:@"," intoString:NULL])
+ [args addObject:[self parseExpression]];
+
+ if (![self scanString:@")" intoString:NULL] || [args count] < 2 || [args[1] expressionType] != CPConstantValueExpressionType)
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForFunction:args[0] selectorName:[args[1] constantValue] arguments:args.slice(2)];
+ }
+
+ [self scanString:@"#" intoString:NULL];
+ if (!identifier)
+ identifier = [CPCharacterSet characterSetWithCharactersInString:@"_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"];
+
+ if (![self scanCharactersFromSet:identifier intoString:REFERENCE(ident)])
+ CPRaiseParseError(self, @"expression");
+
+ return [CPExpression expressionForKeyPath:ident];
+}
+
+- (CPExpression)parseFunctionalExpression
+{
+ var left = [self parseSimpleExpression];
+
+ while (YES)
+ {
+ if ([self scanString:@"." intoString:NULL])
+ {
+ var right = [self parseSimpleExpression],
+ expressionType = [right expressionType];
+
+ if (expressionType == CPKeyPathExpressionType)
+ left = [[CPExpression_keypath alloc] initWithOperand:left andKeyPath:[right keyPath]];
+ else if (expressionType == CPVariableExpressionType)
+ left = [CPExpression expressionForFunction:left selectorName:@"valueForKey:" arguments:[right]];
+ else
+ CPRaiseParseError(self, @"expression");
+ }
+ else if ([self scanString:@"[" intoString:NULL])
+ {
+ // index expression
+ if ([self scanPredicateKeyword:@"FIRST"])
+ {
+ left = [CPExpression expressionForFunction:@"first:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else if ([self scanPredicateKeyword:@"LAST"])
+ {
+ left = [CPExpression expressionForFunction:@"last:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else if ([self scanPredicateKeyword:@"SIZE"])
+ {
+ left = [CPExpression expressionForFunction:@"count:" arguments:[CPArray arrayWithObject:left]];
+ }
+ else
+ {
+ var index = [self parseExpression];
+ left = [CPExpression expressionForFunction:@"fromObject:index:" arguments:[CPArray arrayWithObjects:left, index]];
+ }
+
+ if (![self scanString:@"]" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ }
+ else if ([self scanString:@":" intoString:NULL])
+ {
+ // function - this parser allows for (max)(a, b, c) to be properly
+ // recognized and even (%K)(a, b, c) if %K evaluates to "max"
+
+ if (![left keyPath])
+ CPRaiseParseError(self, @"expression");
+
+ var selector = [left keyPath] + @":",
+ args = [];
+
+ if (![self scanString:@"(" intoString:NULL])
+ {
+ var str;
+ [self scanCharactersFromSet:[CPCharacterSet lowercaseLetterCharacterSet] intoString:REFERENCE(str)];
+
+ if (![self scanString:@":(" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+
+ selector += str + @":";
+ }
+
+ if (![self scanString:@")" intoString:NULL])
+ {
+ [args addObject:[self parseExpression]];
+ while ([self scanString:@"," intoString:NULL])
+ [args addObject:[self parseExpression]];
+
+ if (![self scanString:@")" intoString:NULL])
+ CPRaiseParseError(self, @"expression");
+ }
+
+ left = [CPExpression expressionForFunction:selector arguments:args];
+ }
+ else if ([self scanString:@"UNION" intoString:NULL])
+ {
+ left = [CPExpression expressionForUnionSet:left with:[self parseExpression]];
+ }
+ else if ([self scanString:@"INTERSECT" intoString:NULL])
+ {
+ left = [CPExpression expressionForIntersectSet:left with:[self parseExpression]];
+ }
+ else if ([self scanString:@"MINUS" intoString:NULL])
+ {
+ left = [CPExpression expressionForMinusSet:left with:[self parseExpression]];
+ }
+ else
+ {
+ // done with suffixes
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parsePowerExpression
+{
+ var left = [self parseFunctionalExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"**" intoString:NULL])
+ {
+ right = [self parseFunctionalExpression];
+ left = [CPExpression expressionForFunction:@"raise:to:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseMultiplicationExpression
+{
+ var left = [self parsePowerExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"*" intoString:NULL])
+ {
+ right = [self parsePowerExpression];
+ left = [CPExpression expressionForFunction:@"multiply:by:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else if ([self scanString:@"/" intoString:NULL])
+ {
+ right = [self parsePowerExpression];
+ left = [CPExpression expressionForFunction:@"divide:by:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseAdditionExpression
+{
+ var left = [self parseMultiplicationExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@"+" intoString:NULL])
+ {
+ right = [self parseMultiplicationExpression];
+ left = [CPExpression expressionForFunction:@"add:to:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else if ([self scanString:@"-" intoString:NULL])
+ {
+ right = [self parseMultiplicationExpression];
+ left = [CPExpression expressionForFunction:@"from:substract:" arguments:[CPArray arrayWithObjects:left, right]];
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+- (CPExpression)parseBinaryExpression
+{
+ var left = [self parseAdditionExpression];
+
+ while (YES)
+ {
+ var right;
+
+ if ([self scanString:@":=" intoString:NULL]) // assignment
+ {
+ // check left to be a variable?
+ right = [self parseAdditionExpression];
+ // FIXME
+ }
+ else
+ {
+ return left;
+ }
+ }
+}
+
+@end
+
+var CPRaiseParseError = function CPRaiseParseError(aScanner, target)
+{
+ [CPException raise:CPInvalidArgumentException reason:@"unable to parse " + target + " at index " + [aScanner scanLocation]];
+}
+
+@import "CPCompoundPredicate.j"
+@import "CPComparisonPredicate.j"
+@import "CPExpression.j"
+/*
+ * CPPredicate.j
+ *
+ * CPPredicate parsing based on NSPredicate.m in GNUStep Base Library (http://www.gnustep.org/)
+ * Copyright (c) 2005 Free Software Foundation.
+ *
+ * Created by cacaodev.
+ * Copyright 2010.
+ *
+ * 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 "CPArray.j"
+@import "CPException.j"
+@import "CPNull.j"
+@import "CPObject.j"
+@import "CPScanner.j"
+@import "CPSet.j"
+@import "CPValue.j"
+@import "CPCharacterSet.j"
+
+/*!
+ @ingroup foundation
+ @class CPPredicate
+ @brief The CPPredicate class is used to define logical conditions used to constrain a search either for a fetch or for in-memory filtering.
+
+ You use predicates to represent logical conditions, used for describing objects in persistent stores
+ and in-memory filtering of objects. Although it is common to create predicates directly from instances
+ of CPComparisonPredicate, CPCompoundPredicate, and CPExpression, you often create predicates from a
+ format string which is parsed by the class methods on CPPredicate. Examples of predicate format strings include:
+
+ - Simple comparisons, such as grade == "7" or firstName like "Shaffiq"
+ - Case/diacritic insensitive lookups, such as name contains[cd] "itroen"
+ - Logical operations, such as (firstName like "Mark") OR (lastName like "Adderley")
+ - “Between” predicates such as date between {$YESTERDAY, $TOMORROW}.
+
+ You can create predicates for relationships, such as:
+
+ - group.name like "work*"
+ - ALL children.age > 12
+ - ANY children.age > 12
+
+ You can create predicates for operations, such as @sum.items.price < 1000.
+
+ You can also create predicates that include variables, so that the predicate can be pre-defined before
+ substituting concrete values at runtime with the evaluateWithObject:substitutionVariables: method.
*/
@implementation CPPredicate : CPObject