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