Predicate and expression to fetch complex request core data - iphone

I have to make a complex core data fetch request but I don't know if it can be made.
This is my scenario: just one entity (Expense) with these attributes:
Cost (NSDecimalNumber)
Deposit (NSDecimalNumber)
Category (NSString)
Paid (Boolean Value)
The request should return the 3 most expensive categories but these are the rules that must be respected:
If Paid == YES, Expense cost should be added to Expense category total
If Paid == NO && Deposit > 0, Expense deposit should be added to Expense category total
If Paid == NO, nothing should be added to Expense category total
Using NSExpression, I'm able to calculate every total per category but it also includes cost of Expenses not paid.
Is there a way to accomplish this?
Thank you so much!

You could, for example, use a NSFetchRequest:
// Build the fetch request
NSString *entityName = NSStringFromClass([Expense class]);
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = entity;
which filters only relevant expenses:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(paid == YES) OR ((paid == NO) AND (deposit > 0))"];
request.predicate = predicate;
and sums up the cost and depost attributes:
NSExpressionDescription *(^makeExpressionDescription)(NSString *, NSString *) = ^(NSString *keyPath, NSString *name)
{
// Create an expression for the key path.
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:keyPath];
// Create an expression to represent the function you want to apply
NSExpression *totalExpression = [NSExpression expressionForFunction: #"sum:" arguments: #[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
// The name is the key that will be used in the dictionary for the return value
expressionDescription.name = name;
expressionDescription.expression = totalExpression;
expressionDescription.expressionResultType = NSDecimalAttributeType;
return expressionDescription;
};
NSExpressionDescription *totalCostDescription = makeExpressionDescription(#"cost", #"totalCost");
NSExpressionDescription *totalDepositDescription = makeExpressionDescription(#"deposit", #"totalDeposit");
// Specify that the request should return dictionaries.
request.resultType = NSDictionaryResultType;
request.propertiesToFetch = #[categoryDescription,
paidDescription,
totalCostDescription,
totalDepositDescription];
and group the results by category and paid status:
// Get 'category' and 'paid' attribute descriptions
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName
inManagedObjectContext:context];
NSDictionary *attributes = [entity attributesByName];
NSAttributeDescription *categoryDescription = attributes[#"category"];
NSAttributeDescription *paidDescription = attributes[#"paid"];
// Group by 'category' and 'paid' attributes
request.propertiesToGroupBy = #[categoryDescription, paidDescription];
You'll get paid and unpaid expenses summed up
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
all you need to do is combine (and sort) then:
if (results) {
NSMutableDictionary *combined = [NSMutableDictionary dictionary];
for (NSDictionary *result in results) {
NSString *category = result[#"category"];
BOOL paid = [result[#"paid"] boolValue];
NSDecimalNumber *total = result[paid ? #"totalCost" : #"totalDeposit"];
NSDecimalNumber *sum = combined[category];
if (sum) {
total = [total decimalNumberByAdding:sum];
}
combined[category] = total;
}
NSArray *sortedCategories = [combined keysSortedByValueUsingSelector:#selector(compare:)];
[sortedCategories enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Category %#: %#", obj, combined[obj]);
}];
}
else {
NSLog(#"Error: %#", error);
}

Related

Optimizing this Core Data request

I have an entity in Core Data named MusicInterest. I have to add 5000 or so of these at a time and my current process is to query to see if the MusicInterest exists already, if not create a new one.
It seems this requires 5000 trips to the store to see if each title exists. There are also, of course, insert trips, but the 5000 queries is what's slowing me down.
Each FacebookFriend will have multiple music interests, and I enumerate through each one using an array of string titles, calling the following code.
Any ideas how to optimize this?
+ (MusicInterest*) musicInterestForFacebookFriend:(FacebookFriend*)facebookFriend WithTitle:(NSString*)musicTitle UsingManagedObjectContext:(NSManagedObjectContext*)moc
{
// query to see if there
NSArray *matches = [self queryForMusicTitle:musicTitle moc:moc];
if (([matches count] >= 1)) {
// NSLog(#"Music already in database");
MusicInterest *existingMusic = [matches lastObject];
[existingMusic addLikedByObject:facebookFriend];
return [matches lastObject];
} else {
// create new Music Interest
MusicInterest *newMusic = [NSEntityDescription insertNewObjectForEntityForName:#"MusicInterest" inManagedObjectContext:moc];
newMusic.title = musicTitle;
[newMusic addLikedByObject:facebookFriend];
return newMusic;
}
}
+ (NSArray *)queryForMusicTitle:(NSString *)MusicTitle moc:(NSManagedObjectContext *)moc
{
// query to see if there
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MusicInterest"];
request.predicate = [NSPredicate predicateWithFormat:#"title == %#", [NSString stringWithFormat:#"%#", MusicTitle]];
NSError *error = nil;
NSArray *matches = [moc executeFetchRequest:request error:&error];
if (error) {
NSLog(#"Error querying title in Music interest. Error = %#", error);
}
return matches;
}
UPDATE:
I employed the design suggested in the Core Data programming guide and it reduced my time from 12 seconds to 4 seconds (still needs some optimization in other areas :)
The guide only includes half the sample code - I thought I would share my complete implementation:
musicArray = [[music componentsSeparatedByString:#", "] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if (obj1 > obj2)
return NSOrderedDescending;
else if (obj1 < obj2)
return NSOrderedAscending;
return NSOrderedSame;
}];
if (musicArray) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MusicInterest"];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"title IN %#", musicArray]];
[fetchRequest setSortDescriptors:
#[[[NSSortDescriptor alloc] initWithKey: #"title" ascending:YES]]];
NSError *fetchError = nil;
NSArray *musicInterestMatchingTitles = [backgroundContext executeFetchRequest:fetchRequest error:&fetchError];
if ([musicArray count] > 0) {
// walk musicArray and musicInterestsMatchingTitles in parallel
for (int i = 0; i < [musicArray count]; i++) {
NSString *title = musicArray[i];
if (i < [musicInterestMatchingTitles count]) {
MusicInterest *comparingMusicInterest = musicInterestMatchingTitles[i];
// compare each title
if (![title isEqualToString:comparingMusicInterest.title]) {
// if it doesn't exist as a ManagedObject (a MusicInterest), create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
} else {
// otherwise, just establish the relationship
[comparingMusicInterest addLikedByObject:friend];
}
} else {
// if there are no existing matching managedObjects, create one
MusicInterest *musicInterest = [MusicInterest createNewMusicInterestUsingManagedObjectContext:backgroundContext];
musicInterest.title = title;
[musicInterest addLikedByObject:friend];
}
}
}
}
}];
[self saveBackgroundContext:backgroundContext];
Implementing Find-or-Create Efficiently in the "Core Data Programming Guide" describes a pattern that might be useful here. The basic idea is:
Sort your list of items that you want to insert/update by some unique id that is also stored in
the database.
Perform a single fetch request that fetches all objects from the database that have an id from your list, sorted by the same id.
Now traverse your list and the array of fetched items in parallel, to find which items have to be inserted and which items already exist and can be updated.

Core Data: does a fetch have to make a trip to persistent store?

Say I do this:
NSManagedObjectContext *context = #a managed object context";
NSString *entityName = #an entity name#;
NSFetchRequest *requestForAll = [NSFetchRequest requestWithEntityName:entityName];
NSArray *allObj = [context executeFetchRequest:requestForAll];
for (NSString *name in allNamesArray){
NSFetchRequest *requestForOne = [NSFetchRequest requestWithEntityName:entityName];
requestForOne.predicate = [NSPredicate predicateWithFormat:#"name == %#",name];
NSArray *ObjsWithName = [context executeFetchRequest:requestForOne];
#do some work with the obj#
}
Does the fetch in the loop incur a trip to the persistent store every time? Or those fetches will only be performed in coredata's row cache?
EDIT
I've written a fragment of testing code :
You need to create a core data entity named "Person" and it should have an attribute named "name", which is of type string.
use this code to populate some data:
self.array = #[#"alkjsdfkllaksjdf",#"asldjflkajdklsfjlk;aj",#"aflakjsdl;kfjalksdjfklajkldhkl;aj",#"aljdfkljalksdjfl;j" ,#"flajdl;kfjaklsdjflk;j",#"akldsjfklajdslkf",#"alkdjfkljaklsdjflkaj",#"alsdjflkajsdflj",#"adlkfjlkajsdfkljkla",#"alkdjfklajslkdfj"];
NSString *firstRunKey = #"oh its first run!";
NSString *firstRun = [[NSUserDefaults standardUserDefaults] objectForKey:firstRunKey];
if (!firstRun) {
for (NSString *name in self.array) {
Person *p = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.managedObjectContext];
p.name = name;
}
}
[self.managedObjectContext save];
[[NSUserDefaults standardUserDefaults] setObject:firstRunKey forKey:firstRunKey];
[[NSUserDefaults standardUserDefaults] synchronize];
profile this two methods and you'll find usingCoreData costs much more time than usingFilterArray!
static int caseCount = 1000;
-(void)usingCoreData
{
NSLog(#"core data");
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSArray *allPersons = [self.managedObjectContext executeFetchRequest:request error:nil];
for (int i = 0; i < caseCount; i++){
for (NSString *name in self.array) {
request.predicate = [NSPredicate predicateWithFormat:#"name == %#",name];
NSArray *result = [self.managedObjectContext executeFetchRequest:request error:nil];
}
}
}
-(void)usingFilterArray
{
NSLog(#"filter array");
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSArray *allPersons = [self.managedObjectContext executeFetchRequest:request error:nil];
for (int i = 0; i < caseCount; i++){
for (NSString *name in self.array) {
NSArray *array = [allPersons filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"name == %#",name]];
}
}
}
Guess I need to answer my question myself.
I tested it and found, every time a fetch executed, core data will translate your NSFetchRequest into SQL command and invoke a data base query,the query result is firstly NSManagedObjectIDs, caching is applied to get the NSManagedObject from a NSManagedObjectID.
In conclusion, it caches object, but doesn't cache query result.
That means you execute the same NSFetchRequest for 10 times, it will query your persistent store for 10 times, event though you will get 10 times the same result. So in such situation, filtering array in memory will perform better than fetching.
The fetch will come from the specified cache when available.
EDIT:
Here's a link to a great tutorial that shows how to set up a NSFetchedResultsController that uses a cache.
http://www.raywenderlich.com/?p=999

Filtering NSArray/NSDictionary using NSPredicate

I've been trying to filter this array (which is full of NSDictionaries) using NSPredicate...
I have a very small amount of code that just isn't working...
The following code should change label.text to AmyBurnett34, but it doesn't...
NSPredicate *pred = [NSPredicate predicateWithFormat:#"id = %#", [[mightyPlistDict objectForKey:#"pushesArr"] objectAtIndex:indexPath.row]];
NSLog(#"%#",pred);
label.text = [[[twitterInfo filteredArrayUsingPredicate:pred] lastObject] objectForKey:#"screen_name"];
NSLog(#"%#",twitterInfo);
And here is what gets NSLoged...
2012-08-05 11:39:45.929 VideoPush[1711:707] id == "101323790"
2012-08-05 11:39:45.931 VideoPush[1711:707] (
{
id = 101323790;
"screen_name" = AmyBurnett34;
},
{
id = 25073877;
"screen_name" = realDonaldTrump;
},
{
id = 159462573;
"screen_name" = ecomagination;
},
{
id = 285234969;
"screen_name" = "UCB_Properties";
},
{
id = 14315150;
"screen_name" = MichaelHyatt;
}
)
Just for the heads up if you also NSLog this... the array is empty...
NSLog(%#,[twitterInfo filteredArrayUsingPredicate:pred]);
The problem is that your predicate is using comparing with a string and your content is using a number. Try this:
NSNumber *idNumber = [NSNumber numberWithLongLong:[[[mightyPlistDict objectForKey:#"pushesArr"] objectAtIndex:indexPath.row] longLongValue]];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"id = %#", idNumber];
You don't know for sure that the value of "id" is a string - it might be a NSNumber. I suggest:
NSUInteger matchIdx = ...;
NSUInteger idx = [array indexOfObjectPassingTest:^BOOL(NSDictionary *dict, NSUInteger idx, BOOL *stop)
{
id obj = [dict objectForKey:#"id"];
// NSLog the class if curious using NSStringFromClass[obj class];
NSUInteger testIdx = [obj integerValue]; // works on strings and numbers
return testIdx == matchIdx;
}
if(idx == NSNotFound) // handle error
NSString *screenName = [[array objectAtIndex:idx] objectForKey:#"screen_name"];
NSPredicate is used for filtering arrays, not sorting them.
To sort an array, use the sortedArrayUsingDescriptors method of NSArray.
An an example:
// Define a sort descriptor based on last name.
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:#"lastName" ascending:YES];
// Sort our array with the descriptor.
NSArray *sortedArray = [originalArray sortedArrayUsingDescriptors:[NSArray arrayWithObjects:descriptor, nil]];

get date from an nsdictionary and match them

I am using nsdictionary to store dates.I am trying to give certain dates from my testcase and get the dates from the dictionary.Say ,my dictionary has 10 dates.I wish to match 3 dates and get only the 3 dates from the dictionary.I am unable to do that.Can anyone tell me how I can do that?I am getting all the 10 dates even I try to get the 3 dates.well,heres my codeif(([dd1 laterDate:startDate] || [dd1 isEqualToDate:startDate]) &&
([dd1 earlierDate:endDate] || [dd1 isEqualToDate:endDate] ) )
{
if ([startDate earlierDate:dd1] && [endDate earlierDate:dd1])
{
//dd==startDate;
NSManagedObject *allnew = Nil;
NSManagedObjectContext *allone=[self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Weather" inManagedObjectContext:allone];
NSLog(#" The NEW ENTITY IS ALLOCATED AS entity is %#",entity);
WeatherXMLParser *delegate = [[WeatherXMLParser alloc] initWithCity:city state:state country:country];
NSXMLParser *locationParser = [[NSXMLParser alloc] initWithContentsOfURL:delegate.url];
[locationParser setDelegate:delegate];
[locationParser setShouldResolveExternalEntities:YES];
[locationParser parse];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
predicate = [NSPredicate predicateWithFormat:
#"city == %# and state == %# and country == %# and date==%# and date==%#", city, state, country,startDate,endDate];
[request setPredicate:predicate];
NSError *err;
NSUInteger count = [allone countForFetchRequest:request error:&err];
NSLog(#" minimum salary is %#",predicate);
// If a predicate was passed, pass it to the query
if(predicate !=NULL){
//[self deleteobject];
}
Weather *weather = (Weather *)[NSEntityDescription insertNewObjectForEntityForName:#"Weather"
inManagedObjectContext:self.managedObjectContext];
weather.date = [fields objectForKey:#"date"];
weather.high =[fields objectForKey:#"high"];
weather.low = [fields objectForKey:#"low"];
weather.city =[fields objectForKey:#"city"];
weather.state =[fields objectForKey:#"state"];
weather.country =[fields objectForKey:#"country"];
NSString*icon=[fields objectForKey:#"icon"];
NSString *all=[icon lastPathComponent];
weather.condition = all;
[self saveContext];
I wish to get only 2 dates but I am getting all 4 elements from nsdictionary.I am passing my startdate and enddate from the testcase and I am getting dates from google weather api using nsxmlparser and storing them in nsdictionary.I am getting the first date and incrementing each date and storing them.My NSdictionary looks likegot {
city = #;
condition = "Mostly Sunny";
country = #;
date = "2011-08-11 05:00:00 +0000";
"day_of_week" = Thu;
high = 81;
icon = "/ig/images/weather/mostly_sunny.gif";
low = 65;
startdate = "2011-08-11 05:00:00 +0000";
state = #;
int count = 0;// global
-(void)threeDateMatches(NSDate*)testCaseDate{
for(NSDate* d1 in dateDictionary){
if( [[dateComparisonFormatter stringFromDate:testCaseDate] isEqualToString: [dateComparisonFormatter stringFromDate:d1]] ) {
count = count+1;
//save your d1 in another variable and that will give you three matches date
//with your testCaseDate
}
if(count>3){
break;
}
}
I have just given you an example, did not test it by running it.

What is the best way to organize a group of groups in Obj-c?

I have an unusual problem where I have a list of fruits (apple, orange, banana, grapes, etc etc). They can be organized into small groups for example:
Group 1: apple, green_apple, pineapple
Group 2: grapes, banana, strawberries, apple
Group 3: orange, grapes, green_apple
Each group is then associated with a price. So Group 1->9.99, Group 2-> 15.99 etc
And then later on when given a list, they need to be matched against existing groups, and if none was found, create one. If one exists, return the price.
What is the best way to do this? Basically the association needs to be an NSDictionary, but the key seems like it should be an array. The problem then is, how do I construct the key when given some input? I have to resort to keeping things in alphabetical order for keys to be consistent? But then this approach would not be too good when names includes symbols ($apple etc).
What would your approach be?
Use an NSSet instead of an NSArray (Sets are unordered lists, and it sounds like that's what you've got here). However, I'm not sure that NSDictionary will use the [NSSet isEqualToSet:] (or whatever it's called) method rather than pointer equality when determining keys, so you might have to implement your own key-checking method.
Seems like you can use NSSet as key to NSDictionary:
NSMutableDictionary * dict = [NSMutableDictionary dictionary];
NSSet * set;
set = [NSSet setWithObjects:#"a", #"b", #"c", #"d", nil];
[dict setObject:#"1" forKey:set];
set = [NSSet setWithObjects:#"b", #"c", #"d", #"e", nil];
[dict setObject:#"2" forKey:set];
id key;
NSEnumerator * enumerator = [dict keyEnumerator];
while ((key = [enumerator nextObject]))
NSLog(#"%# : %#", key, [dict objectForKey:key]);
set = [NSSet setWithObjects:#"c", #"b", #"e", #"d", nil];
NSString * value = [dict objectForKey:set];
NSLog(#"set: %# : key: %#", set, value);
Outputs:
2009-12-08 15:42:17.885 x[4989] (d, e, b, c) : 2
2009-12-08 15:42:17.887 x[4989] (d, a, b, c) : 1
2009-12-08 15:42:17.887 x[4989] set: (d, e, b, c) : key: 2
Another way is to use NSMutableDictionary that holds multiple NSSet's of items and price as a key, however is noted this will not work if the price is not unique.
You check manually if the set is in dictionary by iterating over items and for each set using isEqualToSet: - unless anyone can come up with better way.
If it is you return the price (key) if it is not you can insert it with a price, the main parts:
#interface ShoppingList : NSObject
{
NSMutableDictionary * shoppingList;
}
- (void)setList:(NSSet*)aList
forPrice:(double)aPrice
{
[shoppingList setObject:aList forKey:[NSNumber numberWithDouble:aPrice]];
}
- (double)priceForList:(NSSet*)aList
{
id key;
NSEnumerator * enumerator = [shoppingList keyEnumerator];
while ((key = [enumerator nextObject]))
{
NSSet * list = [shoppingList objectForKey:key];
if ([aList isEqualToSet:list])
{
return [(NSNumber*)key doubleValue];
}
}
return 0.0;
}
{
ShoppingList * shoppingList = [[ShoppingList alloc] init];
NSSet * list;
double price = 0.0;
list =
[NSSet setWithObjects:#"apple",#"green_apple",#"pineapple",nil];
[shoppingList setList:list forPrice:9.99];
list =
[NSSet setWithObjects:#"grapes",#"banana",#"strawberries",#"apple",nil];
[shoppingList setList:list forPrice:15.99];
list =
[NSSet setWithObjects:#"orange",#"grapes",#"green_apple",nil];
[shoppingList setList:list forPrice:7.50];
// searching for this
list =
[NSSet setWithObjects:#"grapes",#"banana",#"strawberries",#"apple",nil];
price = [shoppingList priceForList:list];
if (price != 0.0)
{
NSLog(#"price: %.2f, for pricelist: %#", price, list);
}
else
{
NSLog(#"shopping list not found: %#", list);
[shoppingList setList:list forPrice:15.99];
}
}
You could use a simple CoreData solution for this, and in the long run, it will probably be easiest. For your scenario, you could create two entities, let's say Item and PriceGroup. The Item entity could have a name attribute and a to-one priceGroup relationship. The PriceGroup entity could have a description attribute (although, in your scenario, it doesn't even need that), a price attribute and a to-many items relationship. Assuming you set up these two, simple entities, and get a context, you could easily create your associations like so in code:
- (void)createGroup:(NSNumber*)price withProducts:(NSSet*)productNames
{
// assume context exists and was retrieved from somewhere
NSManagedObject* priceGroup = [NSEntityDescription insertNewObjectForEntityForName:#"PriceGroup" inManagedObjectContext:context];
[priceGroup setValue:price forKey:#"price"];
for( NSString* productName in productNames ) {
NSManagedObject* product = [NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:context];
[product setValue:productName forKey:#"name"];
[product setValue:priceGroup forKey:#"priceGroup"];
}
}
You would now have all your products associated with your price group in your managed object context. That means you can do all kinds of queries on them, sort them easily, etc.
As another example, here is how you might get all items whose group price is over $9.99 but not in some specific group (let's say the $15.99 sale price group):
- (NSSet*)itemsAsDescribed
{
// Get this from somewhere
NSManagedObjectContext* context = /* get this or set this */
NSManagedObject* exclusionGroup = /* get this or set this */
NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"priceGroup.price >= 9.99 AND NOT SELF in %#", [exclusionGroup valueForKey:#"items"]];
[request setEntity:[NSEntityDescription entityForName:#"Item" inManagedObjectContext:context]];
[request setPredicate:predicate];
NSError* error = nil;
NSSet* results = nil;
NSArray* array = [context executeFetchRequest:request error:&error];
if( !array ) {
// handle the error
} else {
results = [NSSet setWithArray:array];
}
return results;
}
In this case, since there are not many-to-many relationships, having the set is unnecessary, however, if you changed it in the future so that an item could be in multiple groups (like a regular price group and a sale price group), this would be necessary to ensure you didn't get duplicate items in your result.