Core Data: Fetch dictionaries grouped by multiple attributes - cocoa-touch

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.

Related

Core Data group by NSDate without Time

I have start Date attribute in core data, and i want to fetch the items along with grouping according to startDate,
But startDate is basically having timeComponent in it, but i want grouping to be based on yyyy-mm-dd,
This is the code i am using
NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest new];
NSManagedObjectContext *context = self.managedObjectContext;
request.entity = [CalendarItem entityInManagedObjectContext:context];
NSExpression *startExpr = [NSExpression expressionForKeyPath:#"start"];
NSExpression *countExpr = [NSExpression expressionForFunction:#"count:" arguments:[NSArray arrayWithObject:startExpr]];
NSExpressionDescription *exprDesc = [[NSExpressionDescription alloc] init];
[exprDesc setExpression:countExpr];
[exprDesc setExpressionResultType:NSInteger64AttributeType];
[exprDesc setName:#"count"];
[request setPropertiesToGroupBy:#[#"start"]];
[request setPropertiesToFetch:[NSArray arrayWithObjects:#"start", exprDesc, nil]];
[request setResultType:NSDictionaryResultType];
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
This is the Output i am getting:
Printing description of results:
<_PFArray 0x600001e3a700>(
{
count = 1;
start = "2021-09-14 03:30:00 +0000";
},
{
count = 1;
start = "2021-09-14 04:00:00 +0000";
},
{
count = 1;
start = "2021-09-16 09:30:00 +0000";
},
{
count = 1;
start = "2021-11-11 00:00:00 +0000";
},
{
count = 1;
start = "2021-11-11 04:00:00 +0000";
},
{
count = 1;
start = "2021-11-11 06:00:00 +0000";
},
{
count = 1;
start = "2021-11-12 00:00:00 +0000";
}
)
Expected Result:
{
count = 2
start = 2021-09-14
}
{
count = 1
start = 2021-09-16
}
{
count = 3
start = 2021-11-11
}
{
count = 1;
start = "2021-11-12
}
Core Data "date" properties, as you've found, are actually timestamps. They include the time of day, even though they're called "date". You can't tell Core Data to use only part of the value of a date attribute-- it's all or nothing.
To get the kind of grouping you want, you need to create another property where the value is only the value you need instead of a timestamp that includes extra details you don't need. Here, that would be a date where the hour, minute, and second are all set to zero, so that those details don't affect grouping.
One way to do this is to make a new date object with that change and save it in a new property. You could call it trimmedDate. Then any time you set the date on an instance, also set trimmedDate, with code that's something like
NSDate *trimmed = [[NSCalendar currentCalendar] dateBySettingHour:0 minute:0 second:0 ofDate:date options:0];
Then use this new value for grouping.

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.

Chained expressions to perform calculations in Core Data

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

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.

Core data, count and group by

I have an entity with 2 attributes, an NSDate and a boolean value. (this is going to be a large "table")
I need to count all YES and NO values for the boolean between two dates, grouped by days. How can i do this?
The result I'm looking for is
{
totalYes = 10,
totalNo = 5,
date = dd-mm-yyyy
},
{
totalYes = 15,
totalNo = 3,
date = dd-mm-yyyy
},
etc
Thanks
You can try this approach:
1)Get all the enteties with YES,sorted by date
2)Go trough this array and fill array with dictionaries with day value and number of yeses
3)Then, do the ame thing with noe's,adding number of no to that array of dictionaries.
NSFetchRequest *request = [[NSFetchRequest alloc]init];
request.entity = [NSEntityDescription entityForName:#"Day" inManagedObjectContext:context];
request.predicate = [NSPredicate predicateWithFormat:#"yesorno = %#",YES];
NSError *error = nil;
request.sortDescriptors =[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"date" ascending:YES]];
//Here you get all the enteties with YES,sorted by date
NSArray *days = [context executeFetchRequest:request error:&error];
NSMutableArray *arrayOfDates = [NSMutableArray array];
int firstDay = [[[NSCalendar currentCalendar]components:NSDayCalendarUnit fromDate:[[days objectAtIndex:0]date]]day];
//Add the first day dictionary
[arrayOfDates addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[[days objectAtIndex:0]date],#"Day", nil];
int numberOfYes = 0;
int dayNumber = 0;
for(NSManagedObject *day in days)
{
if( [[[NSCalendar currentCalendar]components:NSDayCalendarUnit fromDate:[day date]]day]>firstDay)
{
//save number of yeses for the previous day,because we are done with it
[[arrayOfDates objectAtIndex:dayNumber]setValue:[NSNumber numberWithInt:numberOfYes] forKey:#"NumberOfYes"];
numberOfYes = 1;
dayNumber++;
firstDay = [[[NSCalendar currentCalendar]components:NSDayCalendarUnit fromDate:[day date]]day];//date with new day
[arrayOfDates addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:[day date],#"Day", nil];//Add this day dictionary to array
}else
{
numberOfYes++;
}
}
//And somrthing similar to No