Error when combining a subquery in a NSPredicate with valueForKeyPath - objective-c

I store time periods in Core Data. Each time period has an DateTime attribute called EndDate. I am trying to get the maximum end date, which is before (<) the date specified.
This is how I have coded this using a subquery and ValueForKeyPath:
NSString *keyPath = [NSString stringWithFormat:#"SUBQUERY(SELF, $x, $x.EndDate < %#).#max.EndDate", date];
IBFinPeriod *periodBeforeCurrentDate = [self.finperiod valueForKeyPath:keyPath];
However, when running this code, I get the runtime error: the entity IBFinPeriod is not key value coding-compliant for the key "SUBQUERY(SELF, $x, $x".'
What is wrong with my code?
Do I need to specify the subquery differently?
Thank you for your help!!

You could use a fetch request with fetchLimit set to 1 and a descending sort descriptor.
If you insist on the valueForKeyPath: I would first filter the results with filteredArrayUsingPredicate: (with a straight forward predicate selecting the records with dates prior to your date) and then simply using #"#max.EndDate" as the key path.
If you need the entire object rather than just the date, just sort your set:
NSSet *periodsBeforeCurrentDate = [self.finperiod
filteredSetUsingPredicate:[NSPredicate predicateWithFormat:
#"EndDate < %#", date]];
if (periodsBeforeCurrentDate.count) {
*sort = [NSSortDescriptor sortDescriptorWithKey:#"EndDate" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:sort];
IBFinPeriod *lastPeriodBeforeCurrentDate =[[periodsBeforeCurrentDate
sortedArrayUsingDescriptors:sortDescriptors] objectAtIndex:0];
}
In my opinion it would be easier to just fetch.

Related

NSFetchRequest fails when using date as predicate

I am trying to fetch all records from a core data entity that was created "today", I have a field in the Entity called createdDate that stores the date as string using local timeZone of the device in the following format
2017-06-26T11:06:43+08:00
I create the following date string for comparison:
NSDateFormatter *dateFormatterDayOnly = [[NSDateFormatter alloc] init];
[dateFormatterDayOnly setDateFormat:#"yyyy-MM-dd"];
[dateFormatterDayOnly setTimeZone:[NSTimeZone localTimeZone]];
NSString *todaysDate = [dateFormatterDayOnly stringFromDate:[NSDate date]];
Debugs tell me the Predicate looks ok as follows
2017-06-26 11:33:22.277 NWMobileTill[1842:482539] -[EodView tillTotalCashIn] todaysDate:2017-06-26
and then I create the predicate as follows
NSPredicate *predicateTodaysDate = [NSPredicate predicateWithFormat:#"createdDate like[cd] %#", todaysDate];
fetchPts.predicate = predicateTodaysDate;
NSArray *todaysTendersArray = [[context executeFetchRequest:fetchPts error:&errorPts] mutableCopy];
But this returns 0 hits
I would have expected this to match all that were created today.
What do I need to change to make this return all records created today?
instead of like[cd] use BEGINSWITH
[NSPredicate predicateWithFormat:#"createdDate BEGINSWITH %#", todaysDate];
or regular expression evaluation:
NSString* regex = [NSString stringWithFormat:#"^%#.*$", todaysDate];
[NSPredicate predicateWithFormat:#"createdDate MATCHES %#", regex];
Storing a date object as a string is WRONG. It is not a little wrong. It is not 'reasonable people will disagree wrong. It is complete wrong.
As a string you can't compare which is greater. You can't select a range. You can't sort by date. And on top of that the one thing that you would think a string would do better - displaying the date as a string - is also harder! Because of localization you have to parse the string into a date and then back into a correct localized string.
If you haven't release your app yet then simply change the type in the core-data model, delete the app, and reinstall. Make sure that everyone else also deletes the app or it will crash for them.
If you have already release you app then create a new model version with ANOTHER property with the createdDate as a date. When the app starts (but not in app delegate - because it may take more than 5 seconds and if it does then the OS will force quit the app) go though the database and parse all of the createdDate Strings and put them into the createdDate date field.
To do a search when the value is a date get the start and end of the date and search for values that are between the two.
NSDate* startOfDay = [self.calendar startOfDayForDate:[NSDate date]];
NSDate* endOfDay = [self.calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startOfDay options:0];
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:#[[NSPredicate predicateWithFormat:#"createdDate > %#", startOfDay],[NSPredicate predicateWithFormat:#"createdDate < %#", endOfDay]]];

NSArray filter using NSPredicate, comparing dates

I have a Person entity where the attribute date_of_birth is declared as NSString. If I have an array of 'Person' instances and I need to filter them down to only those whose date_of_birth is less that 25/11/2005 I am using a predicate whose format when NSLogged is:
FUNCTION(date_of_birth, "yyyy_MM_dd_dateFormat") <[cd] CAST(154575908.000000, "NSDate")
where yyyy_MM_dd_dateFormat() is a category method on NSString that returns the string instance as a date.
I am not getting the expected results. Am I doing something wrong, and what is the bit where it says CAST(154575908.000000, "NSDate" and is that valid?
UPDATE: changing the date_of_birth attribute type to NSDate is not an option at the moment due to the size, maturity and complexity of the project.
Dates are best represented by NSDate, which implements inequality via earlierDate: and laterDate: methods. These answer the earlier/later date between the receiver and the parameter.
Your conversion method probably looks something like this ...
// return an NSDate for a string given in dd/MM/yyyy
- (NSDate *)dateFromString:(NSString *)string {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"dd/MM/yyyy"];
return [formatter dateFromString:string];
}
The array can be filtered with a block-based NSPredicate that uses NSDate comparison...
NSDate *november25 = [self dateFromString:#"25/11/2005"];
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(Person *person, NSDictionary *bind){
// this is the important part, lets get things in NSDate form so we can use them.
// of course it would be quicker to alter the data type, but we can covert on the fly
NSDate *dob = [self dateFromString:person.date_of_birth];
return date_of_birth == [november25 earlierDate:dob];
}];
// assumes allPeople is an NSArray of Person objects to be filtered
// and assumes Person has an NSString date_of_birth property
NSArray *oldPeople = [allPeople filteredArrayUsingPredicate:predicate];
Here, person date is type of NSDate.
NSPredicate *predicate2 = [NSPredicate predicateWithFormat:#"date >= %#",person.date];
list=[list filteredArrayUsingPredicate:predicate2];
But in case you saved with NSString type , than you need to cast comparison date into string than use like instead of >=

Sort NSMutableArray by date and then by alphabetical order

I have an NSMutable array of objects. The objects represent football (soccer) matches and have an NSString parameter called title (ed "Arsenal v Chelsea"), and an NSDate parameter called ActualDate.
I am sorting the array by date at the moment using the following code:
NSMutableArray* a = [self getMatchListFromURL:#"http://www.url.com"];
[a sortUsingDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"ActualDate" ascending:YES]]];
Obviously there are multiple games that happen on the same date. I would like to sort games that happen on the same date in alphabetical order. Is there a simple way to do this?
The method sortUsingDescriptors takes array of NSSortDescriptor as an argument. So you can pass multiple sort descriptors to method as follow:
NSSortDescriptor *sortAlphabetical = [NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES];
NSSortDescriptor *sortByDate = [NSSortDescriptor sortDescriptorWithKey:#"ActualDate" ascending:YES];
NSArray *sortDescriptors = #[sortAlphabetical, sortByDate];
//perform sorting
[a sortUsingDescriptors:sortDescriptors];
There are a few ways to implement this, but I think the most readable is to implement a comparison block, like so:
[a sortUsingComparator:^NSComparisonResult(SomeClass *obj1, SomeClass *obj2) {
NSComparisonResult dateCompare = [obj1.actualDate compare:obj2.actualDate];
if (dateCompare != NSOrderedSame) {
return dateCompare;
} else {
return [obj1.title compare:obj2.title];
}
}];
This will sort a first by its actualDate property, and if they're the same, then by the titleproperty. You can add additional logic if you need to.
You could, alternatively, add additional NSSortDescriptor objects to the array you pass to sortUsingDescriptors:, but I think that's less readable.

NSSortComparator having no affect on NSMutableArray

I am running the following code to sort my array in the order of ending soonest (nearest expiry_date to [NSDate date] (now).
However, whenever I run the comparator, it has no affect, at all on the array, and all objects retain their current positions. Please can you tell me where I am going wrong?
[self.questions sortUsingComparator:^NSComparisonResult(NSDictionary * question1, NSDictionary *question2) {
NSDate* dateq1 = [NSDate dateWithTimeIntervalSinceNow:[[question1 objectForKey:#"expiry_date"] floatValue]];
NSDate * dateq2 = [NSDate dateWithTimeIntervalSinceNow:[[question2 objectForKey:#"expiry_date"] floatValue]];
NSComparisonResult result = [dateq1 compare:dateq2];
NSLog(#"%#", result == NSOrderedAscending ? #"ASC" : result == NSOrderedDescending ? #"DESC" : #"SAME");
return result;
}];
The NSLog() returns the following:
DESC
DESC
ASC
I have tried reversing the comparison to have dateq2 in the place of dateq1 and vice versa, but the array never changes, even though the returned values do.
It looks like it is working to me (judging by your output), but maybe this is more succinct and therefore less error prone?
NSArray* unsorted;
NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey: #"expiry_date" ascending: NO];
self.questions = [unsorted sortedArrayUsingDescriptors: #[ descriptor ] ];
Sort returns a new array; I don't think it changes the receiver. I can't tell if you're doing an assignment or not.

Is this a bug I should submit to Apple, or is this expected behavior?

When using CoreData, the following multi-column index predicate is very slow - it takes almost 2 seconds for 26,000 records.
Please note both columns are indexed, and I am purposefully doing the query with > and <=, instead of beginswith, to make it fast:
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"airportNameUppercase >= %# AND airportNameUppercase < %# \
OR cityUppercase >= %# AND cityUppercase < %# \
upperText, upperTextIncremented,
upperText, upperTextIncremented];
However, if I run two separate fetchRequests, one for each column, and then I merge the results, then each fetchRequest takes just 1-2 hundredths of a second, and merging the lists (which are sorted) takes about 1/10th of a second.
Is this a bug in how CoreData handles multiple indices, or is this expected behavior? The following is my full, optimized code, which works very fast:
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init]autorelease];
[fetchRequest setFetchBatchSize:15];
// looking up a list of Airports
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Airport"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// sort by uppercase name
NSSortDescriptor *nameSortDescriptor = [[[NSSortDescriptor alloc]
initWithKey:#"airportNameUppercase"
ascending:YES
selector:#selector(compare:)] autorelease];
NSArray *sortDescriptors = [[[NSArray alloc] initWithObjects:nameSortDescriptor, nil]autorelease];
[fetchRequest setSortDescriptors:sortDescriptors];
// use > and <= to do a prefix search that ignores locale and unicode,
// because it's very fast
NSString *upperText = [text uppercaseString];
unichar c = [upperText characterAtIndex:[text length]-1];
c++;
NSString *modName = [[upperText substringToIndex:[text length]-1]
stringByAppendingString:[NSString stringWithCharacters:&c length:1]];
// for the first fetch, we look up names and codes
// we'll merge these results with the next fetch for city name
// because looking up by name and city at the same time is slow
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"airportNameUppercase >= %# AND airportNameUppercase < %# \
OR iata == %# \
OR icao == %#",
upperText, modName,
upperText,
upperText,
upperText];
[fetchRequest setPredicate:predicate];
NSArray *nameArray = [context executeFetchRequest:fetchRequest error:nil];
// now that we looked up all airports with names beginning with the prefix
// look up airports with cities beginning with the prefix, so we can merge the lists
predicate = [NSPredicate predicateWithFormat:
#"cityUppercase >= %# AND cityUppercase < %#",
upperText, modName];
[fetchRequest setPredicate:predicate];
NSArray *cityArray = [context executeFetchRequest:fetchRequest error:nil];
// now we merge the arrays
NSMutableArray *combinedArray = [NSMutableArray arrayWithCapacity:[cityArray count]+[nameArray count]];
int cityIndex = 0;
int nameIndex = 0;
while( cityIndex < [cityArray count]
|| nameIndex < [nameArray count]) {
if (cityIndex >= [cityArray count]) {
[combinedArray addObject:[nameArray objectAtIndex:nameIndex]];
nameIndex++;
} else if (nameIndex >= [nameArray count]) {
[combinedArray addObject:[cityArray objectAtIndex:cityIndex]];
cityIndex++;
} else if ([[[cityArray objectAtIndex:cityIndex]airportNameUppercase] isEqualToString:
[[nameArray objectAtIndex:nameIndex]airportNameUppercase]]) {
[combinedArray addObject:[cityArray objectAtIndex:cityIndex]];
cityIndex++;
nameIndex++;
} else if ([[cityArray objectAtIndex:cityIndex]airportNameUppercase] <
[[nameArray objectAtIndex:nameIndex]airportNameUppercase]) {
[combinedArray addObject:[cityArray objectAtIndex:cityIndex]];
cityIndex++;
} else if ([[cityArray objectAtIndex:cityIndex]airportNameUppercase] >
[[nameArray objectAtIndex:nameIndex]airportNameUppercase]) {
[combinedArray addObject:[nameArray objectAtIndex:nameIndex]];
nameIndex++;
}
}
self.airportList = combinedArray;
CoreData has no affordance for the creation or use of multi-column indices. This means that when you execute the query corresponding to your multi-property predicate, CoreData can only use one index to make the selection. Subsequently it uses the index for one of the property tests, but then SQLite can't use an index to gather matches for the second property, and therefore has to do it all in memory instead of using its on-disk index structure.
That second phase of the select ends up being slow because it has to gather all the results into memory from the disk, then make the comparison and drop results in-memory. So you end up doing potentially more I/O than if you could use a multi-column index.
This is why, if you will be disqualifying a lot of potential results in each column of your predicate, you'll see much faster results by doing what you're doing and making two separate fetches and merging in-memory than you would if you made one fetch.
To answer your question, this behavior isn't unexpected by Apple; it's just an effect of a design decision to not support multi-column indices in CoreData. But you should to file a bug at https://feedbackassistant.apple.com/ requesting support of multi-column indices if you'd like to see that feature in the future.
In the meantime, if you really want to get max database performance on iOS, you could consider using SQLite directly instead of CoreData.
When in doubt, you should file a bug.
There isn't currently any API to instruct Core Data to create a compound index. If a compound index were to exist, it would be used without issue.
Non-indexed columns are not processed entirely in memory. They result in a table scan, which isn't the same thing as loading the entire file (well, unless your file only has 1 table). Table scans on strings tend to be very slow.
SQLite itself is limited in the number of indices it will used per query. Basically just 1, give or take some circumstances.
You should use the [n] flag for this query to do a binary search against normalized text. There is a sample project on ADC called 'DerivedProperty'. It will show how to normalize text so you can use binary collations as opposed to the default ICU integration for fancy localized Unicode aware text comparisons.
There's a much longer discussion about fast string searching in Core Data at https://devforums.apple.com/message/363871