Query max NSDate in core data - ios

Is there a way to query NSDate in CoreData. For example if I want an entity with the highest NSDate value? I see that NSExpression "max:" only takes an NSNumber.

You can actually ask SQL for just that value, not the object with that value:
NSExpression *date = [NSExpression expressionForKeyPath:#"date"];
NSExpression *maxDate = [NSExpression expressionForFunction:#"max:"
arguments:[NSArray arrayWithObject:maxDate]];
NSExpressionDescription *d = [[[NSExpressionDescription alloc] init] autorelease];
[d setName:#"maxDate"];
[d setExpression:maxSalaryExpression];
[d setExpressionResultType:NSDateAttributeType];
[request setPropertiesToFetch:[NSArray arrayWithObject:d]];
NSError *error = nil;
NSArray *objects = [managedObjectContext executeFetchRequest:request error:&error];
if (objects == nil) {
// Handle the error.
} else {
if (0 < [objects count]) {
NSLog(#"Maximum date: %#", [[objects objectAtIndex:0] valueForKey:#"maxDate"]);
}
}
more detail under Fetching Managed Objects -> Fetching Specific Values in the CoreData documentation.
or
Perfomed a query, ordered on Date field DESCENDING, and using setFetchLim it:1.
Its not perfect, but at least it worked.

You can do this directly in SQLite-- without fetching everything and then filtering the result, and without the complexity of NSExpression.
To get the one object that has the max date, do something like (assuming entity name Entity and date attribute timeStamp):
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timeStamp = self.#max.timeStamp"];
fetchRequest.predicate = predicate;
Do the fetch. You'll get (at most) one result, which will be the instance with the max date.
If you want to get just the date and not the entire managed object, add this before doing the fetch:
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = #[ #"timeStamp" ];

you can get it with
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"tablename"];
fetchRequest.fetchLimit = 1;
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"yourDate" ascending:NO]];
NSError *error = nil;
id person = [managedObjectContext executeFetchRequest:fetchRequest error:&error].firstObject;

Related

How to fetch specific records in Core data

I am a new bid in iOS development. I am using NSManagedObject of Core Data to perform Insert and Fetch operations. It works perfectly fine. But the problem is, I want to fetch only some selected records (where condition in MySQL) from the table.
For e.g. "select name from users where city='Pune'";
I found NSPredicate to fetch filtered data. But it gives all the data in array and not just the selected one. For e.g. if result for above query is:
Anu
Then the NSPredicate result will give:
fname = Anu
lname = Padhye
city = Pune
id = 3
Is there a way to only fetch selected record/s in iOS Objective-c? Following is the code snippet I am using for NSManagedObject:
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"User"];
valueData = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
NSEntityDescription *productEntity=[NSEntityDescription entityForName:#"User" inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetch=[[NSFetchRequest alloc] init];
[fetch setEntity:productEntity];
NSPredicate *p=[NSPredicate predicateWithFormat:#"id == %d", 3];
[fetch setPredicate:p];
//... add sorts if you want them
NSError *fetchError;
NSArray *fetchedProducts=[valueData filteredArrayUsingPredicate:p];
Try this:
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"User"];
request.predicate = [NSPredicate predicateWithFormat:#"city == %# && id == %d", #"Pune", 3];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"id" ascending:YES]];
NSArray *results = [managedObjectContext executeFetchRequest:request error:nil];
Results array should now contain all records who have Pune as their city with the id of 3.

Is it possible to use a group by count in the havingPredicate for a CoreData fetch (for dupe detection)?

For reference, the problem I'm trying to solve is efficiently finding and removing duplicates in a table that could have a lot of entries.
The table I am working with is called PersistedDay with a dayString object in it (it's a string. :-P). There are more columns that aren't relevant to this question. I'd like to find any PersistedDay's that have duplicates.
In SQL, this is one of the efficient ways you can do that (FYI, I can do this query on the CoreData backing SQLite DB):
SELECT ZDAYSTRING FROM ZPERSISTEDDAY GROUP BY ZDAYSTRING HAVING COUNT(ZDAYSTRING) > 1;
This returns ONLY the dayStrings that have duplicates and you can then get all of the fields for those objects by querying using the resulting day strings (you can use it as a sub query to do it all in one request).
NSFetchRequest seems to have all of the required pieces to do this too, but it doesn't quite seem to work. Here's what I tried to do:
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PersistedDay" inManagedObjectContext:context];
[request setEntity:entity];
NSPropertyDescription* dayStringProperty = entity.propertiesByName[#"dayString"];
request.propertiesToFetch = #[dayStringProperty];
request.propertiesToGroupBy = #[dayStringProperty];
request.havingPredicate = [NSPredicate predicateWithFormat: #"dayString.#count > 1"];
request.resultType = NSDictionaryResultType;
NSArray *results = [context executeFetchRequest:request error:NULL];
That doesn't work. :-P If I try that I get an error "Unsupported function expression count:(dayString)" when trying to do the fetch. I don't think the dayString in "dayString.#count" even matters in that code above...but, I put it in for clarity (SQL count just operates on the grouped rows).
So, my question is: is this possible and, if so, what is the syntax to do it? I couldn't find anything in the CoreData docs to indicate how to do this.
I found one similar SO posts that I now unfortunately can't find again that was about running a count in a having clause (I don't think there was a group by). But, the poster gave up and did it a different way after not finding a solution. I'm hoping this is more explicit so maybe someone has an answer. :)
For reference, this is what I am doing for now that DOES work, but requires returning almost all the rows since there are very few duplicates in most cases:
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PersistedDay"
inManagedObjectContext:context];
[request setEntity:entity];
NSPropertyDescription* dayStringProperty = entity.propertiesByName[#"dayString"];
// Get the count of dayString...
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: #"dayString"]; // Does not really matter
NSExpression *countExpression = [NSExpression expressionForFunction: #"count:" arguments: [NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: #"dayStringCount"];
[expressionDescription setExpression: countExpression];
[expressionDescription setExpressionResultType: NSInteger32AttributeType];
request.propertiesToFetch = #[dayStringProperty, expressionDescription];
request.propertiesToGroupBy = #[dayStringProperty];
request.resultType = NSDictionaryResultType;
NSArray *results = [context executeFetchRequest:request error:NULL];
I then have to loop over the result and only return the results that have dayStringCount > 1. Which is what the having clause should do. :-P
NOTE: I know CoreData isn't SQL. :) Just would like to know if I can do the equivalent type of operation with the same efficiency as SQL.
Yes it is possible. You cannot reference count as key path, however you can reference it as variable. Just like in SQL. In my example I have cities created with duplicate names.
let fetchRequest = NSFetchRequest(entityName: "City")
let nameExpr = NSExpression(forKeyPath: "name")
let countExpr = NSExpressionDescription()
let countVariableExpr = NSExpression(forVariable: "count")
countExpr.name = "count"
countExpr.expression = NSExpression(forFunction: "count:", arguments: [ nameExpr ])
countExpr.expressionResultType = .Integer64AttributeType
fetchRequest.resultType = .DictionaryResultType
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ]
fetchRequest.propertiesToGroupBy = [ cityEntity.propertiesByName["name"]! ]
fetchRequest.propertiesToFetch = [ cityEntity.propertiesByName["name"]!, countExpr ]
// filter out group result and return only groups that have duplicates
fetchRequest.havingPredicate = NSPredicate(format: "%# > 1", countVariableExpr)
Complete playground file at:
https://gist.github.com/pronebird/cca9777af004e9c91f9cd36c23cc821c
Best I can come up with is:
NSError* error;
NSManagedObjectContext* context = self.managedObjectContext;
NSEntityDescription* entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:context];
// Construct a count group field
NSExpressionDescription* count = [NSExpressionDescription new];
count.name = #"count";
count.expression = [NSExpression expressionWithFormat:#"count:(value)"];
count.expressionResultType = NSInteger64AttributeType;
// Get list of all "value" fields (only)
NSPropertyDescription* value = [entity propertiesByName][#"value"];
NSFetchRequest* request = [[NSFetchRequest alloc] initWithEntityName:#"Event"];
request.propertiesToFetch = #[ value, count];
request.propertiesToGroupBy = #[ value ];
request.resultType = NSDictionaryResultType;
NSArray* values = [context executeFetchRequest:request error:&error];
// Filter count > 1
values = [values filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"count > 1"]];
// slice to get just the values
values = [values valueForKeyPath:#"value"];
But that's not really much different from what you're using.
The best way finding duplicates in Core Data depends on your data. According to Efficiently Importing Data and assuming that you have to import less than 1000 PersistedDays, I suggest this solution:
NSFetchRequest* fetchRequest = [NSFetchRequest new];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"PersistedDay" inManagedObjectContext:myMOC]];
[fetchRequest setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"dayString" ascending:NO]]];
NSArray* persistedDays = [myMOC executeFetchRequest:fetchRequest error:nil];
for (NSUInteger i = persistedDays.count - 1; i > 0; --i) {
PersistedDay *currentDay = persistedDays[i];
PersistedDay *nextDay = persistedDays[i-1];
if ([currentDay.dayString isEqualToString:nextDay.dayString]) {
/* Do stuff/delete with currentDay */
}
}
For speed up can index dayString in Core Data.
You also can reduce the the data set if you remember a timestamp or a date of the last duplicate clean up:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"importDate > %#", lastDuplicateCleanUp];

Sum of a column for distinct id using core data

I want to get the sum of all values in a column for a distinct id(date in my case). My code is
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Table"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(endTime!=%#)",nil];
[fetchRequest setPredicate:predicate];
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.returnsDistinctResults=YES;
NSExpressionDescription *aDescription = [[NSExpressionDescription alloc] init];
aDescription.name = #"A";
aDescription.expression = [NSExpression expressionForKeyPath:#"#sum.a"];
aDescription.expressionResultType = NSDecimalAttributeType;
NSExpressionDescription *bDescription = [[NSExpressionDescription alloc] init];
bDescription.name = #"B";
bDescription.expression = [NSExpression expressionForKeyPath:#"#sum.b"];
bDescription.expressionResultType = NSDecimalAttributeType;
NSArray *properties = [NSArray arrayWithObjects:#"date",aDescription, bDescription, nil];
[fetchRequest setPropertiesToFetch:properties];
NSArray *result = [temporaryContext executeFetchRequest:fetchRequest error:&error];
NSLog(#"Array result is : %#",result);
When i run the above query i get sum of column a, column b and distinct dates. The problem is it will not add up the sum for a distinct date, instead it will add up all the values in the column a and b.
In simple words i want to get results as in this question Link but in core data. Any help would be greatly appreciated.
Try setting the fetch request to group:
[fetchRequest setPropertiesToGroupBy:#[ #"date" ]];

Getting a maximum value from a core data relationship

I search the net for answers to this problem, but unfortunately I don't think I am fluent enough in core data procedures to actually include the right combo of keywords.
I have two entities... Users and Bookmarks with a one-to-many relationship.
Users : string:firstName, string:lastname, string:iconImage
with a relationship to Bookmarks - inverse
Bookmarks : string:title, string:url, string:content, image:Binary Data, order:Integer32
with a relationship to Users - inverse
My goal here is to query the bookmarks for a particular user and find the highest number for the key:order, so that when I add an new bookmark, it will be one larger than the max. I have seen the Apple example, which works and makes sense, but I need a little more. The example returns the maximum value for all records in that entity.
-(NSNumber*) getNextBookmarksOrderForUser:(NSManagedObjectID*)userID
{
NSNumber *highOrder;
Users *user =[self getUserByID:userID];
if (user)
{
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"order"];
NSExpression *highestOrderingNumber = [NSExpression expressionForFunction:#"max:" arguments:[NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc]init];
[expressionDescription setName:#"maxOrdering"];
[expressionDescription setExpression:highestOrderingNumber];
[expressionDescription setExpressionResultType:NSDecimalAttributeType];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Bookmarks" inManagedObjectContext:[_dataContext managedObjectContext]];
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:entity];
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
[request setResultType:NSDictionaryResultType];
NSError *error = nil;
NSArray *objects = [[_dataContext managedObjectContext] executeFetchRequest:request error:&error];
if(objects == nil) {
// Handle the error
}
else {
if ([objects count] > 0) {
highOrder = [NSNumber numberWithInt:[[[objects objectAtIndex:0] valueForKey:#"maxOrdering"]intValue]];
NSLog(#"Highest ordering number: %#", highOrder);
}
}
return highOrder;
}
return nil;
}
So if I have two users, one with 10 bookmark and the other with 25, the above code will always return 25. I tried adding a predicate:
NSPredicate *byUser = [NSPredicate predicateWithFormat:#"self == %#", user];
[request setPredicate:byUser];
Does anyone have any suggestions? I Know that I could return all bookmark for a user and the write the code to find my value, but I would like to do it the most efficient way.
Thank you for any help.
From my comment
I think you should use a predicate like this [NSPredicate predicateWithFormat:#"users == %#", user]; Since you are querying against Bookmarks.

Core Data equivalent for sqlite query

I use Core Data for an iPhone app.
There is "Flight" entity with a "start" and "duration" property.
The flights are listed on a paginated view, where I need to sum the duration per page and the duration rollup sum.
In native sqlite following solution works:
select sum(pg.zduration) from (select zduration,zstart from zflight order by zstart limit %i,%i) as pg",offset,limit
So on first page, with a page size of 5, I get duration sum and same rollup duration with offset=0 and limit=5.
On second page, I get the duration sum with offset=5 and limit=5. The rollup sum with offset=0 and limit=10.
And so on..
Now the Question:
How would I solve that with Core Data, NSExpression, NSExpressionDescription and NSFetchRequest instead of sqlite? Of course, I would not like to load all flight objects in memory...
So I am able to caculate the duration for all flights:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Flight" inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
[request setResultType:NSDictionaryResultType];
NSSortDescriptor *startSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"start"
ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:startSortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
request.fetchOffset=onPage*pageSize;//does not help, cause offset and limit are applied to the result
request.fetchLimit=pageSize;//does not help, cause offset and limit are applied to the result
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"duration"];
NSExpression *sumExpression = [NSExpression expressionForFunction:#"sum:" arguments:[NSArray arrayWithObject:keyPathExpression]];
// Create an expression description using the minExpression and returning a date.
NSExpressionDescription *expressionDescription1 = [[NSExpressionDescription alloc] init];
[expressionDescription1 setName:#"durationSum"];
[expressionDescription1 setExpression:sumExpression];
[expressionDescription1 setExpressionResultType:NSInteger64AttributeType];
[request setPropertiesToFetch:[NSArray arrayWithObjects:expressionDescription1,nil]];
// Execute the fetch.
NSError *error = nil;
NSArray *objects = [self.managedObjectContext executeFetchRequest:request error:&error];
if(error!=nil){
[NSException raise:#"Sum Page Duration failed" format:#"%# Error:%#", [[error userInfo] valueForKey:#"reason"],error];
}
if (objects!=nil && [objects count] > 0) {
return (NSNumber*)[[objects objectAtIndex:0] valueForKey:#"durationSum"];
}
return 0;
As you said, the limit and offset set on the fetch request are applied to the result and NSExpression won't work well in this case. You could operate on the returned objects, after they've been offset and limited by the fetch request, using a collection operator rather than NSExpression, e.g.:
NSNumber *durationSum = [objects valueForKeyPath:#"#sum.duration"];

Resources