4

Following a fantastic tutorial by Jeff Lamarche, I'm trying to aggregate data for a specific subclass of NSManagedObject.

This is the scenario. I created a class named Product that extends NSManagedObject class. Product class has three properties like the following:

@property (nonatomic, retain) NSString* name;
@property (nonatomic, retain) NSNumber* quantity;
@property (nonatomic, retain) NSNumber* price;

I also created a category, called Product+Aggregate, where I perform a sum aggregation. In particular, following Jeff tutorial, I managed the sum for quantity attribute.

+(NSNumber *)aggregateOperation:(NSString *)function onAttribute:(NSString *)attributeName withPredicate:(NSPredicate *)predicate inManagedObjectContext:(NSManagedObjectContext *)context
{
    NSString* className = NSStringFromClass([self class]);

    NSExpression *ex = [NSExpression expressionForFunction:function 
        arguments:[NSArray arrayWithObject:[NSExpression expressionForKeyPath:attributeName]]];

    NSExpressionDescription *ed = [[NSExpressionDescription alloc] init];
    [ed setName:@"result"];
    [ed setExpression:ex];
    [ed setExpressionResultType:NSInteger64AttributeType];

    NSArray *properties = [NSArray arrayWithObject:ed];
    [ed release];

    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setPropertiesToFetch:properties];
    [request setResultType:NSDictionaryResultType];

    if (predicate != nil)
        [request setPredicate:predicate];

    NSEntityDescription *entity = [NSEntityDescription entityForName:className
        inManagedObjectContext:context];
    [request setEntity:entity];

    NSArray *results = [context executeFetchRequest:request error:nil];
    NSDictionary *resultsDictionary = [results objectAtIndex:0];
    NSNumber *resultValue = [resultsDictionary objectForKey:@"result"];

    return resultValue;
}

This class method is called as follow from a specific UIViewController:

NSNumber *totalQuantity = [Product aggregateOperation:@"sum:" onAttribute:@"quantity" withPredicate:nil inManagedObjectContext:self.context];

The code works well. In fact, if I have say 3 product

NAME         QUANTITY      PRICE
PRODUCT 1    2             23.00 
PRODUCT 2    4             12.00
PRODUCT 3    1             2.00

The aggregateOperation method returns 7 as expected.

Now I would have one more step. Modifying that method, I need to return the total cost for product order. In other words, I need to calculate QUANTITY*PRICE value for each product and finally return the TOTAL.

Could you suggest me the right way? Thank you in advance.

EDIT This is the new code I use after Cyberfox suggestion but unfortunately it doesn't work.

NSString* className = NSStringFromClass([self class]);

NSArray *quantityPrice = [NSArray arrayWithObjects: [NSExpression expressionForKeyPath:@"quantity"], [NSExpression expressionForKeyPath:@"price"], nil];

NSArray *multiplyExpression = [NSArray arrayWithObject:[NSExpression expressionForFunction:@"multiply:by:" arguments:quantityPrice]];

NSExpression *ex = [NSExpression expressionForFunction:function arguments:multiplyExpression];

NSExpressionDescription *ed = [[NSExpressionDescription alloc] init];
[ed setName:@"result"];
[ed setExpression:ex];
[ed setExpressionResultType:NSInteger64AttributeType];

// same as before
Lorenzo B
  • 33,216
  • 24
  • 116
  • 190

2 Answers2

5

For those interested in, I found the solution for the problem above.

Here's the code:

static NSString* exprName1 = @"val1";
static NSString* exprName2 = @"val2";

NSString *className = NSStringFromClass([self class]); 

NSExpression *quantityPathExpression = [NSExpression expressionForKeyPath:firstAttribute]; //e.g. quantity    
NSExpression *unitaryPricePathExpression = [NSExpression expressionForKeyPath:secondAttribute]; //e.g. price

NSExpressionDescription *quantityED = [[NSExpressionDescription alloc] init];
[quantityED setName:exprName1];
[quantityED setExpression:quantityPathExpression];
[quantityED setExpressionResultType:NSDictionaryResultType];    

NSExpressionDescription *unitaryPriceED = [[NSExpressionDescription alloc] init];
[unitaryPriceED setName:exprName2];
[unitaryPriceED setExpression:unitaryPricePathExpression];
[unitaryPriceED setExpressionResultType:NSDictionaryResultType];

NSArray *properties = [NSArray arrayWithObjects:quantityED, unitaryPriceED, nil];
[quantityED release];
[unitaryPriceED release];

NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setPropertiesToFetch:properties];
[request setResultType:NSDictionaryResultType];

if (predicate != nil)
   [request setPredicate:predicate];

NSEntityDescription *entity = [NSEntityDescription entityForName:className inManagedObjectContext:context];
[request setEntity:entity];

NSError* error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];

if(error != nil)
{
   NSLog(@"An error occurred: %@", [error localizedDescription]);
   abort();
}

float total = 0;
for (NSDictionary *resultDict in results) 
{
   NSNumber* quantityNumber = [resultDict valueForKey:exprName1];
   NSNumber* unitaryPriceNumber = [resultDict valueForKey:exprName2];

   int moltVal = [quantityNumber intValue]*[unitaryPriceNumber intValue];

   total += moltVal;
}    

return [NSNumber numberWithInt:total];

P.S. to use it create a Class method that returns a NSNumber and accepts as parameters the managed context and 2 attributes (NSString) where you want to perform data retrieving.

Hope it helps!

Lorenzo B
  • 33,216
  • 24
  • 116
  • 190
0

This is a shot in the dark, because to replicate your code I'd need to build a database, etc., but I believe that you want:

NSArray *quantityPrice = [NSArray arrayWithObjects:
                          [NSExpression expressionForKeyPath:@"quantity"],
                          [NSExpression expressionForKeyPath:@"price"], nil];
NSArray *multiplyExpression = [NSArray arrayWithObject:[NSExpression expressionForFunction:@"multiply:by:" arguments:quantityPrice]];
NSExpression *ex = [NSExpression expressionForFunction:function arguments:multiplyExpression];

quantityPrice is an array of the pair of expressions referring to quantity and price. multiplyExpression is the expression multiply:by: with the parameters of quantityPrice. ex is your sum: expression, referencing the multiple expression referencing the quantity and price key paths.

I'm pretty sure you'd want to do something like that, but I can't test it without a DB set up like yours, etc., so it's just theory.

Cyberfox
  • 1,125
  • 7
  • 13
  • Thank you for your reply. Running it, the code stops at `NSArray *results = [context executeFetchRequest:request error:nil];` with the following exceptions **EXCEPTIONS: unwinding through frame** Any suggestion? – Lorenzo B Jan 24 '12 at 11:27
  • Did you get my edited version where I added the trailing ':' to the 'multiply:by:' expression? StackOverflow was giving me trouble fixing that for about 10 minutes. :( That said, there should be a more comprehensive error somewhere in the stack trace, something that reads like: `Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error while updating. 'constraint failed''` Can you put up a pastie of the full stack trace? – Cyberfox Jan 24 '12 at 11:49
  • Yes I did. The stacktrace presents that exception. Could I do something to see a complete stacktrace? Log exceptions are enabled in my scheme project. – Lorenzo B Jan 24 '12 at 11:55
  • 1
    Another useful thing to do is to edit your app's Run Scheme, and add `-com.apple.CoreData.SQLDebug 1` (it may need to be entered as two separate parameters) to the arguments. This will (on some platforms) trigger the Core Data dump-to-console of SQL statements as they're run against the database. Sometimes looking at the SQL generated can tell you what's going wrong. – Cyberfox Jan 24 '12 at 11:58
  • It also looks like I wrote 'multipleExpression' when I meant 'multiplyExpression'. Still, you're looking in your console view in XCode, and all it gives is one line 'unwinding through frame' with no additional details, or anything if you scroll up or down? Ergh; that'll make it hard to debug. There really should be a 'reason' deeper down. – Cyberfox Jan 24 '12 at 12:01
  • See my edit for the new code. Yes, only that exception (multiple times) is thrown. Thank you. – Lorenzo B Jan 24 '12 at 12:05
  • Quick note, I just added the nil sentinel to the `NSArray arrayWithObjects` call... That's what I get for coding out of the IDE. :) – Cyberfox Jan 24 '12 at 12:10
  • Sorry, I already added nil sentinel. I don't know but it doesn't work. – Lorenzo B Jan 24 '12 at 12:14
  • After tweaking a tiny bit, unfortunately I'm just getting: 2012-01-24 04:54:04.268 iBidwatcher[49074:fb03] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported argument to sum : ( "quantity * price" )' This suggests that it's not happy with pairing these operations. Frustrating, because the SQL generated for simpler statements is SO close... – Cyberfox Jan 24 '12 at 13:11
  • Related question: http://stackoverflow.com/questions/4744267/help-with-coredata-query – Cyberfox Jan 24 '12 at 13:24
  • Thank you for your support. I'll try to see that link discussion. – Lorenzo B Jan 24 '12 at 13:42