0
votes

I'm using Core Data to store simple entities that consist of a value and a timestamp. I'm looking to make a fetch request which returns the latest value added, as well as a running average of all values. This all seemed straightforward enough until I attempted to use an NSPredicate to filter the results to within a certain time period.

I'm using NSExpression's expressionForFunction:withArguments: method to determine the average. By setting the launch flag -com.apple.CoreData.SQLDebug 1, I can clearly see that only the latest value is adhering to my date predicate. The average calculating is instead being performed as a subquery, but not taking my date predicate into account:

SELECT (SELECT avg(t1.ZVALUE) FROM ZEVENT t1 WHERE t1.Z_ENT = ?),  t0.ZVALUE FROM ZEVENT t0 WHERE ( t0.ZTIMESTAMP >= ? AND  t0.Z_ENT = ?) ORDER BY t0.ZTIMESTAMP DESC

What's the most efficient (and scalable) way of determining the average value while still honouring my NSFetchRequest's predicate?

For reference, here is my code in it's entirety:

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp" ascending:NO];

NSEntityDescription *entity = [NSEntityDescription entityForName:@"Reading" inManagedObjectContext:moc];
[request setEntity:entity];
[request setSortDescriptors:@[sortDescriptor]];
[request setResultType:NSDictionaryResultType];

// Filter by date where necessary
NSPredicate *datePredicate = [NSPredicate predicateWithFormat:@"(timestamp >= %@)", toDate];
[request setPredicate:datePredicate];

NSExpression *valueExpression = [NSExpression expressionForKeyPath:valueKey];
NSExpressionDescription *valueDescription = [[NSExpressionDescription alloc] init];
[valueDescription setName:@"value"];
[valueDescription setExpression:valueExpression];
[valueDescription setExpressionResultType:NSDoubleAttributeType];

NSExpression *avgExpression = [NSExpression expressionForFunction:@"average:" arguments:[NSArray arrayWithObject:valueExpression]];
NSExpressionDescription *avgDescription = [[NSExpressionDescription alloc] init];
[avgDescription setName:@"averageReading"];
[avgDescription setExpression:avgExpression];
[avgDescription setExpressionResultType:NSDoubleAttributeType];

[request setPropertiesToFetch:@[avgDescription, valueDescription]];
1

1 Answers

0
votes

I see two errors. There's no initialization shown for toDate. I also notice that you are passing setPropertiesToFetch: an array of NSExpressions, but the documentation calls for an array of NSPropertyDescriptions. I would expect that discrepancy to cause a null result, and populated NSError, when you execute the fetch request.

What result do you see from executeFetchRequest:error:? Be sure to check the NSError result. Idiom is something like this:

    NSError *error;
    NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
    if (!results) {
        NSLog(@"%@ fetch error for request %@: %@ %@", fetchRequest,
              error.localizedDescription, error.localizedFailureReason);
    }

I would take a different approach. One fetch request has a limit of 1, sort timestamp descending, and returns the latest timestamp. Add a predicate if you like, for time restriction. Then use a second fetch request to compute the average of the timestamps. You could even encapsulate these calls into their own methods:

-(NSDate *)latestTimestamp:(NSManagedObjectContext *)moc;

-(NSNumber *)averageValueSinceTime:(NSDate *)intervalStart
                           context:(NSManagedObjectContext *)moc;