Chained expressions to perform calculations in Core Data - objective-c

First of all: Happy new year :-)
What I am trying to do
I am trying to divide two attributes in Core Data and then calculate the average of these divisions. The attributes are specified by a key path (e.g. eur, usd, aud).
Example:
I have the following data set:
date eur usd aud
------------------------------
2010-01-01 0.5 1.0 1.5
2010-01-02 0.6 1.1 1.6
2010-01-03 0.4 1.0 1.3
Divide two attributes, e.g. eur / usd with the follwowing results...
divide eur / usd:
------------------
2010-01-01 0.5
2010-01-02 0.54
2010-01-03 0.4
... then calculate the average of these numbers (0.5 + 0.54 + 0.4)/3 = 0.48
My code
Since I would like to have these calculations performed directly by Core Data, I created the following expressions and fetch request:
NSExpression *fromCurrencyPathExpression = [NSExpression
expressionForKeyPath:fromCurrency.lowercaseString];
NSExpression *toCurrencyPathExpression = [NSExpression
expressionForKeyPath:toCurrency.lowercaseString];
NSExpression *divisionExpression = [NSExpression
expressionForFunction:#"divide:by:"
arguments:#[fromCurrencyPathExpression,
toCurrencyPathExpression]];
NSExpression *averageExpression = [NSExpression expressionForFunction:#"average:"
arguments:#[divisionExpression]];
NSString *expressionName = #"averageRate";
NSExpressionDescription *expressionDescription =
[[NSExpressionDescription alloc] init];
expressionDescription.name = expressionName;
expressionDescription.expression = averageExpression;
expressionDescription.expressionResultType= NSDoubleAttributeType;
NSFetchRequest *request = [NSFetchRequest
fetchRequestWithEntityName:NSStringFromClass([self class])];
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"date >= %# AND date <= %#",startDate,fiscalPeriod.endDate];
request.predicate = predicate;
request.propertiesToFetch = #[expressionDescription];
request.resultType = NSDictionaryResultType;
NSError *error;
NSArray *results = [context
executeFetchRequest:request error:&error];
The problem
However, when running the app, it crashes with the error message:
Unsupported argument to sum : (
"eur / usd"
What is wrong with my code?
How can I chain the two calculations and have them performed directly in Core Data?
Thank you!

It seems that "collection" function expressions like #average can only be used with key paths, but not in combination with other general expressions, when used as propertiesToFetch. I do not have a reference for that, but it is a problem that others have noticed also:
Fetch aggregate data from NSManagedObject using another expression as argument to sum: expression
Performing multiplication (aggregation) with CoreData: how to?
So you could proceed in two steps: First execute a fetch request that returns an array with the results of all divisions:
NSExpression *fromCurrencyPathExpression = [NSExpression
expressionForKeyPath:#"eur"];
NSExpression *toCurrencyPathExpression = [NSExpression
expressionForKeyPath:#"usd"];
NSExpression *divisionExpression = [NSExpression
expressionForFunction:#"divide:by:"
arguments:#[fromCurrencyPathExpression,
toCurrencyPathExpression]];
NSString *expressionName = #"ratio";
NSExpressionDescription *expressionDescription =
[[NSExpressionDescription alloc] init];
expressionDescription.name = expressionName;
expressionDescription.expression = divisionExpression;
expressionDescription.expressionResultType= NSDoubleAttributeType;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
request.propertiesToFetch = #[expressionDescription];
request.resultType = NSDictionaryResultType;
NSError *error;
NSArray *ratios = [context executeFetchRequest:request error:&error];
The result is an array of dictionaries:
(lldb) po ratios
(NSArray *) $0 = 0x00000001001170f0 <_PFArray 0x1001170f0>(
{
ratio = "0.5454545454545454";
},
{
ratio = "0.4";
},
{
ratio = "0.5";
}
)
Also, with the "-com.apple.CoreData.SQLDebug 1" option one can see that the divisions are already executed on the SQLite level:
sql: SELECT t0.ZEUR / t0.ZUSD FROM ZENTITY t0
Then you can compute the average of all ratios in memory, using Key-Value Coding:
NSNumber *average = [ratios valueForKeyPath:#"#avg.ratio"];
and the result is
(lldb) po average
(NSNumber *) $0 = 0x000000010012fd60 0.4818181818181818

Related

Core Data: Fetch dictionaries grouped by multiple attributes

I have an NSManagedObject subclass Entry with properties type (int) and date (NSDate). Right now I use the following code to get entries for the current date grouped by type.
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Entry"];
[request setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"type"];
NSExpression *countExpression = [NSExpression expressionForFunction:#"count:" arguments:#[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"entryCountByType"];
[expressionDescription setExpression:countExpression];
[expressionDescription setExpressionResultType:NSInteger64AttributeType];
[request setPropertiesToFetch:#[#"type",expressionDescription]];
[request setPropertiesToGroupBy:#[#"type"]];
[request setPredicate:[NSPredicate predicateWithFormat:#"date == %#", [NSDate date]]];
NSError *error;
NSArray *results = [defaultManagedObjectContext() executeFetchRequest:request error:&error];
Results for a day with 1 entry of type 3 and 1 entry of type 5 are:
<_PFArray 0x15e05b50>(
{
entryCountByType = 1;
type = 3;
},
{
entryCountByType = 1;
type = 5;
}
)
I want to fetch without a date predicate at all and have counts for each type listed by date like so (where 1 day has 2 of type 1 and 1 of type 2, and another day has 3 of type 1 and 2 of type 2):
(
{
date = 6/4/14 00:00:00;
type1 = 2;
type2 = 1;
},
{
date = 6/5/14 00:00:00;
type1 = 3;
type2 = 2;
}
)
Is this possible in the way I'm thinking, that is, with a single fetch request? Doing a fetch for each day individually (about 30 sequential fetches) is really slowing down my app. I've tried adding #"date" to propertiesToGroupBy (after removing the date predicate, of course) but all that does is return a similar result to the first output, just with a date param thrown in so that each type for a day is split out into separate dictionaries.
You are confusing "day" and "date". NSDate is accurate down to the millisecond, so you will likely have only unique dates. You cannot group by a date range.
One way is to store the day as a transient property. The scheme does not matter, but one possibility is e.g. year * 10000 + month * 100 + day.
If you want to dispense with the NSExpressions you could just use a method day in your entity that returns a day string (with NSDateFormatter) and use a NSFetchedResultsController with day as the sectionNameKeyPath to display the grouped data.

Objective-c. Fetching distinct values of attribute from core data

i have run into a problem that i can not solve. I have a "database" - read core data - where i have attribute that holds a value and level.
Something like that
value -------- level
55 -------------4
33 -------------4
50 -------------5
70 -------------6
44 -------------5
what i want now is a to extract all values from level 5 only and add them together. How can i achieve this ?I did found "fetch distinct values" on apple dev site, but this would apply to extracting all values from one attribute.
Help appreciated, thank you. If i have missed a similar topic then please provide me with a link. Thanks
You can use the following fetch request:
// Fetch request for your entity:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
[request setResultType:NSDictionaryResultType];
// Restrict result to "level == 5":
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"level == %d", 5];
[request setPredicate:predicate];
// Expression description for "#sum.value":
NSExpression *sumExpression = [NSExpression expressionForKeyPath:#"#sum.value"];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"sumValue"];
[expressionDescription setExpression:sumExpression];
[expressionDescription setExpressionResultType:NSInteger32AttributeType];
[request setPropertiesToFetch:#[expressionDescription]];
NSArray *result = [context executeFetchRequest:request error:&error];
The result for your data is
(
{
sumValue = 94;
}
)
i.e. an array containing one dictionary with the sum of values with level=5.

Fetch aggregate data from NSManagedObject using another expression as argument to sum: expression

Is it possible to use expression as argument for sum: expression?
I have entity (NSManagedObject class) Drink with properties costPerDrink, numberOfDrinks and drinkingDate.
I would like to get sum of total costs (numberOfDrinks multiplied by costPerDrink) within time period. While I have no problems getting sum on single property (e.g. sum of numberOfDrinks within time period), when I try to use sum expression on another (multiply) expression I got error:
NSInvalidArgumentException, reason: Unsupported argument to sum :
(
"costPerDrink * numberOfDrinks"
)
See code:
// Init fech
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Drink" inManagedObjectContext:[Service appContext]];
[request setEntity:entity];
// Return dictionary
[request setResultType:NSDictionaryResultType];
// Set conditions
[request setPredicate:[NSPredicate predicateWithFormat:#"(drinkingDate>=%#) AND (drinkingDate<=%#)", self.startDate, self.endDate]];
// Set 1st column to be returned - count of drinks
NSExpression *drinksExpression = [NSExpression expressionForKeyPath:#"numberOfDrinks"];
NSExpression *sumDrinksExpression = [NSExpression expressionForFunction:#"sum:" arguments:[NSArray arrayWithObject:drinksExpression]];
NSExpressionDescription *sumDrinksResultDescription = [[NSExpressionDescription alloc] init];
[sumDrinksResultDescription setName:#"sumDrinks"];
[sumDrinksResultDescription setExpression:sumDrinksExpression];
[sumDrinksResultDescription setExpressionResultType:NSInteger32AttributeType];
// Set 2nd column to be returned - total cost
NSExpression *costExpression = [NSExpression expressionForKeyPath:#"costPerDrink"];
NSExpression *totalExpression = [NSExpression expressionForFunction:#"multiply:by:"
arguments:[NSArray arrayWithObjects:costExpression,
drinksExpression, nil]];
NSExpression *sumCostExpression = [NSExpression expressionForFunction:#"sum:" arguments:[NSArray arrayWithObject:totalExpression]];
NSExpressionDescription *sumCostResultDescription = [[NSExpressionDescription alloc] init];
[sumCostResultDescription setName:#"sumCost"];
[sumCostResultDescription setExpression:sumCostExpression];
[sumCostResultDescription setExpressionResultType:NSDecimalAttributeType];
// Add expressions to fetch
[request setPropertiesToFetch:[NSArray arrayWithObjects: sumDrinksResultDescription, sumCostResultDescription, nil]];
// Execute fech
NSError *error;
NSArray *result = [[Service appContext] executeFetchRequest:request error:&error];
I perfectly understand that particular problem can be solved by adding auto-calculated totalCost property to Drink entity and then using sum: in fetch without multiply:by: expression but question remains - is it possible to use expression as argument for sum: expression?

Real World Peformance of SQL vs Core Data: Need Perspective and Perhaps Specific Fixes

I have a word game app that accesses a list of words and clues, which is about 100K entries. The data base is only accessed, never changed. I built the app using SQL methods and it performs pretty well on iOS 6 but the time to get a new clue from the data base is extremely slow on iOS 5:
iOS 5, getting one record from the 100K using SQL, takes about 12
seconds.
iOS 6, getting one record from the 100K using SQL, takes
about 700-1000 milliseconds.
Both of these are on 32 GB iPod Touch.
Given this performance, I made a version using Core Data. My approach gets a random data base entry by first counting the records that fit the query, then chosing one at random. Code follows. Everything I read suggested that Core Data would be faster:
iOS 5, counting records takes around 4 seconds, and retrieving one of
those records takes about 50 - 1500 millisecs. A total time of about 5 seconds.
iOS 6, counting records takes a bit over 2 seconds, and retrieving
one of those records takes about 300-500 millisecs. A total of about 3 seconds.
So Core Data is faster on iOS 5 but slower on iOS 6, compared to SQL. Either way the performance is too slow as far as I am concerned. I know the overhead comes from the methods given below (for the Core Data version). So, two questions:
Any general advice about this issue with an eye to understanding it and improving performance?
Specifically, what about the Core Data code appended below: have I done something foolish that slows it down? Or something else I should add to speed it up? This is my first attempt at Core Data.
Thanks.
- (NSArray *) randomClue {
NSManagedObjectContext* context = [self managedObjectContext];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"A"];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"WL28"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [self createSearchQuery];
[fetchRequest setPredicate:predicate];
NSError *error;
NSDate *date1 = [NSDate date];
NSString *timeString1 = [formatter stringFromDate:date1];
int resCount = [context countForFetchRequest:fetchRequest
error:&error];
NSDate *date2 = [NSDate date];
NSString *timeString2 = [formatter stringFromDate:date2];
int t1 = [timeString1 intValue];
int t2 = [timeString2 intValue];
int d1 = t2-t1;
NSLog(#"randomClue:");
NSLog(#" Time to count array entries: %i", d1);
int ranNum = arc4random_uniform(resCount-1);
int ranNum2 = ranNum + 1;
// Now we fetch just one answer object, not a whole database or even a piece of it!
[fetchRequest setReturnsObjectsAsFaults:YES];
[fetchRequest setPropertiesToFetch:nil];
[fetchRequest setFetchLimit:1];
[fetchRequest setFetchOffset:ranNum2];
NSDate *date3 = [NSDate date];
NSString *timeString3 = [formatter stringFromDate:date3];
self.wl28 = [context executeFetchRequest:fetchRequest error:&error];
NSDate *date4 = [NSDate date];
NSString *timeString4 = [formatter stringFromDate:date4];
int t3 = [timeString3 intValue];
int t4 = [timeString4 intValue];
int d2 = t4-t3;
NSLog(#" Time to retrieve one entry: %i", d2);
return self.wl28;
}
EDIT: createSearchQuery added below
- (NSPredicate *)createSearchQuery {
NSMutableArray *pD = [[GameData gameData].curData valueForKey:#"persData"];
NSNumber *currMin = [pD objectAtIndex:0];
NSNumber *currMax = [pD objectAtIndex:1];
NSNumber *dicNo = [pD objectAtIndex:2];
NSString *dict = nil;
if ([dicNo intValue] == 0) dict = #"TWL";
if ([dicNo intValue] == 1) dict = #"LWL";
NSPredicate *dictPred = [NSPredicate predicateWithFormat:#"dict == %#", dict];
NSPredicate *lowNoCPred = [NSPredicate predicateWithFormat:#"noC >= %#", currMin];
NSPredicate *highNoCPred = [NSPredicate predicateWithFormat:#"noC <= %#", currMax];
NSPredicate *query = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray
arrayWithObjects:dictPred, lowNoCPred, highNoCPred,nil]];
NSLog(#"%#", query);
return query;
}
You can try adding index to your noC and/or dict attributes inside your entity. It might speed up your query time.

'Unsupported function expression CAST...' when using NSPredicate with SQLite Core Data store type

I'm attempting a fetch request with the following code:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"dateModified > CAST(CAST(now(), \"NSNumber\") - %d, \"NSDate\")", (30*86400)];
NSFetchRequest *itemRequest = [NSFetchRequest fetchRequestWithEntityName:#"Item"];
[itemRequest setPredicate:predicate];
NSArray *items = [[self managedObjectContext] executeFetchRequest:itemRequest error:nil];
...in order to fetch items where the 'dateModified > |now() - 30 days|'. The problem is that the above predicate works correctly (fetches properly) until I save my NSPersistentDocument as SQLite and re-open the document. When I re-open the document I get the following error:
Unsupported function expression CAST(CAST(now(), "NSNumber") - 2592000, "NSDate")
Why does this predicate work correctly up until save or if I save the document as XML, but does not work correctly if I save as SQLite? Is there a way to use this predicate with the SQlite store type?
P.S. I have also tried forming the predicate with the following code but get the same error:
//"now()" is a function and takes no arguments
NSExpression * now = [NSExpression expressionForFunction:#"now" arguments:[NSArray array]];
//CAST(now(), 'NSNumber')
NSArray * castNowArguments = [NSArray arrayWithObjects:now, [NSExpression expressionForConstantValue:#"NSNumber"], nil];
NSExpression * castNow = [NSExpression expressionForFunction:#"castObject:toType:" arguments:castNowArguments];
//CAST(now(), 'NSNumber') [+/-] {a time interval}
NSArray * relativeTimestampArguments = [NSArray arrayWithObjects:castNow, [NSExpression expressionForConstantValue:[NSNumber numberWithDouble:(30*86400)]], nil];
NSExpression * relativeTimestamp = [NSExpression expressionForFunction:#"from:subtract:" arguments:relativeTimestampArguments];
//CAST(CAST(now(), 'NSNumber') [+/-] {a time interval}, 'NSDate')
NSArray * castToDateArguments = [NSArray arrayWithObjects:relativeTimestamp, [NSExpression expressionForConstantValue:#"NSDate"], nil];
NSExpression * castToDate = [NSExpression expressionForFunction:#"castObject:toType:" arguments:castToDateArguments];
NSPredicate *predicate = [NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:#"dateModified"]
rightExpression:castToDate
modifier:NSDirectPredicateModifier
type:NSGreaterThanOrEqualToComparison
options:0];
I found my own solution...
Function calls are unsupported when fetching data from a SQLite store type. It is only possible to use function calls in fetches for in-memory data. In order to still use this predicate I had to first fetch the data set and then filter the array using:
[allItems filteredArrayUsingPredicate:predicate];
Hope this helps someone!
Following code worked out just well in my case:
now() - 2592000