I have a NSMutableArray containing custom objects of type Episode. Each of these objects has multiple NSStrings as properties. Now I would like to filter the array to check if I have this episode (parsed from an XML) already and update it or create a new Episode object.
I use the following code:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"weblink = %#", currentEpisode.weblink];
NSArray* hits = [appDelegate.episodesList filteredArrayUsingPredicate:predicate];
currentEpisode is the episode I parsed from the XML and I want to check for, episodeList is my NSMutableArray with Episode objects. weblink is one of the NSString properties containing an URL.
When I check on weblink everything works fine. BUT URLs in Podcastfeeds can change so I want to check on an other property called kuhid which is a unique identifier provided in the feed. 'kuhid' is also an NSString (example: 644ED540-EDCA-4D4F-882E-4B3106DDAAB3). When I check on 'kuhid' the predicate never matches and I get duplicates.
Both properties are NSStrings, both correctly synthesized. Same if I try one of my other NSString (e.g. title) propierties.
Have anybody an idea why that work only with weblink and not with any of my other properties?
Are you sure your string exactly matches, namely you don't have leading or trailing spaces, or lowercase vs. uppercase, or different dashes used (long dash vs. short dash for example) or invisible characters?
Try to log the NSData representation of both strings to compare them byte by byte in the debugger just to be sure
Related
We're trying to use an NSFetchedResultsController to return people names and populate a UITableView in sorted order, using localizedCompare:. We're also trying to provide a section index in the UI (the right column of first characters of each section). We provide the NSFetchedResultsController with a selector on our entity which provides the section each entity should belong to (specifically, the first character of the person's name, capitalized).
When dealing with people names which utilize Unicode code points we've run into an issue. NSFetchedResultsController complains the entities are not sorted by section.
Specifically:
reason=The fetched object at index 103 has an out of order section name 'Ø. Objects must be sorted by section name'}, {
reason = "The fetched object at index 103 has an out of order section name '\U00d8. Objects must be sorted by section name'";
The issue appears to be that the comparison value returned by localizedCompare: is different for the whole "word" versus the leading character.
The following tests pass though I would expect consistent comparison results between ("Ø" and "O") vs. ("Østerhus" and "Osypowicz").
- (void)testLocalizedSortOrder300
{
NSString *str1 = #"Osowski";
NSString *str2 = #"Østerhus";
NSString *str3 = #"Osypowicz";
NSString *letter1 = #"O";
NSString *letter2 = #"Ø";
//localizedCompare:
//"Osowski" < "Østerhus"
NSComparisonResult res = [str1 localizedCompare:str2];
XCTAssertTrue(res == NSOrderedAscending, #"(localizedCompare:) Expected '%#' and '%#' to be NSOrderedAscending, but got %#", str1, str2, res == NSOrderedSame ? #"NSOrderedSame" : #"NSOrderedDescending");
//"Østerhus" < "Osypowicz"
res = [str2 localizedCompare:str3];
XCTAssertTrue(res == NSOrderedAscending, #"(localizedCompare:) Expected '%#' and '%#' to be NSOrderedAscending, but got %#", str2, str3, res == NSOrderedSame ? #"NSOrderedSame" : #"NSOrderedDescending");
//"O" < "Ø"
res = [letter1 localizedCompare:letter2];
XCTAssertTrue(res == NSOrderedAscending, #"(localizedCompare:) Expected '%#' and '%#' to be NSOrderedAscending, but got %#", letter1, letter2, res == NSOrderedSame ? #"NSOrderedSame" : #"NSOrderedDescending");
}
So, the question ultimately is, given a person name (or any other string) which utilize Unicode code points, how do we properly (in a localized manner) return a section name which will correspond with the sort order as dictated by localizedCompare:?
Additionally, what's going on with the localizedCompare: apparently treating "Ø" and "O" as NSOrderedSame when followed by additional characters?
I expect localizedCompare: is using a specific combination of NSStringCompareOptions flags that are causing this behavior.
https://developer.apple.com/documentation/foundation/nsstringcompareoptions?preferredLanguage=occ
You might get the outcome you want by using compare:options: and turning on NSDiacriticInsensitiveSearch.
For generating the section index, it might be best to strip the value of all extended characters first, and then take the first letter. Something like:
[[str1 stringByFoldingWithOptions:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch] substringToIndex:1]
That way a name starting with an accented letter such as "Édward" will get converted to "Edward" before you take the first letter for the section.
Yeah, been there. The only solution I found was to create a second field for search that simplifies the characters (don't remember off hand the method) and store it as a second field which is used for search. Not super elegant but it worked.
Ultimately the approach which solved this was to store normalized section names in the database.
#MartinR suggested SO post lead me to https://stackoverflow.com/a/13292767/397210 which talks about this approach and was the key "ah ha" moment to solve it.
While this does not explain the goofy behavior of localizedCompare: apparently treating "Ø" and "O" as NSOrderedSame when followed by additional characters it is, IMHO, a more robust and complete solution which works for all Unicode code points, in our testing.
Specifically, the approach is:
Create (or utilize an existing) field on your entity to receive a normalized section name for the entity (let's call it sectionName).
Populate this field (sectionName) with the normalized section name*, initially, and as needed (when the person name changes, for instance).
Use this section name field (sectionName) for the sectionNameKeyPath of NSFetchedResultsController -initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:
For the sort descriptors used by the fetch request passed to the NSFetchedResultsController be sure to sort first by section name then by how to sort the contents of the section (person name, for instance), paying attention to the use of the localized version of the comparison selectors. e.g.:
[NSSortDescriptor sortDescriptorWithKey:#"sectionName" ascending:YES selector:#selector(localizedStandardCompare:)],
[NSSortDescriptor sortDescriptorWithKey:#"personName" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]
Test.
*Normalized Section Name
We need to be careful about assuming what the first "character" is when dealing with unicode. "Characters" may be composed of more than one character.
See https://www.objc.io/issues/9-strings/unicode/
and also Compare arabic strings with special characters ios
This is the direction I used to generate a normalized section name:
NSString *decomposedString = name.decomposedStringWithCanonicalMapping;
NSRange firstCharRange = [decomposedString rangeOfComposedCharacterSequenceAtIndex:0];
NSString *firstChar = [decomposedString substringWithRange:firstCharRange];
retVal = [firstChar localizedUppercaseString];
Hopefully this approach is clear and useful to others, and thanks for the assist, all.
I have an API, from which I load Shop objects.
Shop object has NSString *assortment field, containing products you can buy in shop, in my syntax it's eg. "water,milk,potatoes,bread,ketchup".
NSString *assortment = #"water, milk, potatoes, bread, ketchup";
I load shops from my local database to an NSArray *shops.
NSArray *shops = [Shop MR_findAll];
I want to find all objects in my shops array (using MR_findByAttribute maybe?) that contain word "bread" in their assortment field.
I know I can split my string and check for every single assortment position - but that sounds not intuitive to me. I feel like there's better solution, maybe:
NSArray *shopsWithBread = [Shop MR_findByAttribute:#"assortment" withValue:#"<bread regex?>"];
Do you know any nice way to do it? Do you have maybe another good idea how to find all those shops? Maybe you're sure it can't be done this way and I have to write search engine myself?
Thank you in advance for all answers.
Karol
Ok, I've found the solution - I'm using predicate.
NSPredicate *breadSearchPredicate = [NSPredicate predicateWithFormat:#"assortment contains[cd] %#", #"bread"];
NSArray *shopsWithBread = [Shop MR_findAllWithPredicate:breadSearchPredicate];
assortment is my field name; contains[cd] is a kind of instruction, checking for containing a string give - here #"bread".
NSPredicate - nicely explained here: http://nshipster.com/nspredicate/
I have an NSArray of objects each of with contain a "key" field
I have another NSArray with nested NSArrays grouping those objects by a different field "group by field" for instance a display name (alphabetic grouping)
For instance
#interface MyObject
#property (nonatomic) int key;
#property (nonatomic,strong) NSString *groupByKey;
#end
NSArray *aBounchOfObjectsInAFlatList = #[obj1,obj2,obj3....];
NSArray *groupByArrayOfObjects = #[#[obj1_tag,obj2_tag],#[objn_tag,objk_tag]....];
Important to note that obj1 != obj1_tag - they only share values of "groupByKey" and/or "key".
I would like to find the index of each obj_tag where (obj_tag.key == obj.key) from the flat array.
Now I am running 2 loops and saving the counter values - creating index paths for the matches.
I assume there is a clear way using NSPredicates to create an array of NSIndexPaths for the corresponding objects but I am not clear how to do this
NSPredicate is just a predicate and nothing more. It is applied to some object and
yields YES or NO. A predicate can be used for filtering an array, but since the predicate itself is applied to each single array element, it does not "know" the location
(or index) of the object being tested.
Therefore a NSPredicate cannot create an array of NSIndexPaths.
The NSArray method indexOfObjectPassingTest: can be useful to find the location
of an object in an array, but even that method has to loop over all array elements
to find the object. So it might help to make the code shorter or better readable, but
it does not change the performance.
If groupByArrayOfObjects is really large, then you can create a mapping (an NSDictionary) from each key to the corresponding index path first. This requires only one enumeration of the (nested) array. Then you can use this mapping to get
the index path for each element in the other array aBounchOfObjectsInAFlatList.
I have an NSArray of 5 NSArrays that each contain NSDictionaries. The dictionaries contain only strings. I'm trying to use NSPredicate to filter out particular dictionaries, however it only seems to work with an exact match. I would like for it to ignore any punctuation such as : or -. Here's what I have currently.
NSString *seriesTitle = #" Fullmetal Alchemist Brotherhood - ";
NSPredicate *titlePredicate = [NSPredicate predicateWithFormat:
#"series_title CONTAINS[cd]%#", seriesTitle];
NSArray *filtered = [arraytoEvaluate filteredArrayUsingPredicate:titlePredicate]
Within "arraytoEvaluate" is an NSDictionary containing this key-value pair #"series_title" : #"Fullmetal Alchemist: Brotherhood". That's the one I want it to match to. My predicate does not pick this up even though I'm requesting a [cd] search, which I believe ignores punctuation/whitespace, correct?
Using series_title LIKE[cd]%#gives me this crash:
'NSInvalidArgumentException', reason: 'Can't do regex matching on object
Using BEGINSWITH instead gives me this:
'NSInvalidArgumentException', reason: 'Can't do a substring operation with something that isn't a string
If I pass in the string exactly, it will pick it up and give me the the Array that contains the dictionary that contains a match, but only if the search term is an exactly match.
Also, it will give me all of them, not an NSArray containing the single NSDictionary that contains a match. I'm not sure what to do about that. I understand that filteredArrayUsingPredicate is supposed to return an Array, but I will then need to iterate through that Array for the specific NSDictionary that contains a match for the search terms. That's a secondary issue however. Any help would be greatly appreciated.
You have two problems (a) the match is wrong and (b) you have nested arrays.
(a) [cd] means ignore case and diacritics, not punctuation and whitespace. A solution here is to use a regular expression and MATCHES[CD] as the predicate. You need a regular expression which will match the terms you are looking for. A basic re is:
#"fullmetal[^a-z]*alchemist[^a-z]*brotherhood"
where [^a-z]* matches zero or more (*) characters from the set [...] of every character which is not (^) a letter a-z.
While this works it probably matches far more than you wish - you should refine the regular expression.
(b) The error you got Can't do regex matching on object... is because you cannot match a regular expression against an array, and the elements of your array are themselves array. You can address this using a simple loop over the elements, something along the lines of:
NSMutableArray *filtered = [NSMutableArray new]; // allocate an empty mutable array in which to accumulate the matches
for(NSArray *subArray in arraytoEvaluate) // loop over each sub array
[filtered addObjectsFromArray:[subArray filteredArrayUsingPredicate: titlePredicate]]; // and perform the predicate
HTH
I have an array of data that is displayed in a table. The array has multiple fields, including two specific ones I want to filter, the "call type" and the "county". The value for "call type" is either an "f" or "e" and the value for the county is either "w" or "c". I want to have 4 UISwitch's to to either turn on/off the "w", turn on/off the "c" etc. Its hard to explain but if you go to this website and look at the top right corner, its exactly what I want to do. http://www.wccca.com/PITS/ Out of the 4 filters, two filters control the county field, and two filters control the call type field. but they all operate independently. How would I go about accomplishing this? Would I use NSPredicate to create a new array each time something is filtered or what? Thanks.
You could definitely use an NSPredicate for this. Probably the easiest thing to do would be to use the same IBAction for all four switches and have it do a recalculation:
- (IBAction)anySwitchDidChange:(id)sender
{
// make a set of all acceptable call types
NSMutableSet *acceptableCallTypes = [NSMutableSet set];
if(self.fSwitch.on) [acceptableCallTypes addObject:#"f"];
// ... etc, to create acceptableCallTypes and acceptableCounties
NSPredicate *predicate =
[NSPredicate predicateWithFormat:
#"(%# contains callType) and (%# contains county)",
acceptableCallTypes, acceptableCounties];
/*
this predicate assumes your objects have the properties 'callType' and
'county', and that you've filled the relevant sets with objects that would
match those properties via isEqual:, whether strings or numbers or
anything else.
NSDictionaries are acceptable since the internal mechanism used here is
key-value coding.
*/
NSArray *filteredArray = [_sourceArray filteredArrayUsingPredicate:predicate];
// send filteredArray to wherever it needs to go
}
Using predicateWithFormat: causes the text to be parsed right there and then. In this case that should be no problem whatsoever but in general you can create the predicates in advance and supply only the parameters at the relevant moment, should you ever end up using one in a really time critical area.