NSString localizedCompare: inconsistent results given longer strings - ios

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.

Related

Core Data - Fetch Request results not matching the predicate

I'm pretty confused as to why the following is not working:
As you can see in the debug console, the value for newerContentAvailable is 0 even though I only want objects who have this value set to 1. But it made its way into the results anyway.
Yes, I'm using MagicalRecord, but doubtful this has anything to do with it. It's an old, mature codebase and MR_findAllWithPredicate:... just creates a fetch request on that data model and sets the predicate of the fetch.
Is there something I've not understood about Core Data? I admit it is a beast of a framework and best practices are scarce.
Would be seriously grateful for some help!
I believe the problem may be a result of the attribute name you have used: names beginning with new... seem to cause some unexpected behaviour (*). Try changing the attribute name to see if that sorts it.
(*) See for example this question and answer.
NSNumber value of NSNumber is not substituted when used with %# format. You have to get the intValue or floatValue (doubleValue etc) which returns the correct type.
The predicate should be,
NSPredicate *findPred = [NSPredicate predicateWithFormat:#"newerContentAvailable == 1"];
OR
NSNumber *number = #1; //Or any other number
NSPredicate *findPred = [NSPredicate predicateWithFormat:#"newerContentAvailable == %d", [number intValue]];

Line breaks using FMDB and Obj-C

I'm relatively new to iOS programming, so please bear with me.
I'm creating an app that calls recipes from a table view, then listing the measurements and ingredients as labels in the detail view. I catalogued my recipes in Google Sheets, downloaded it as a .csv file and populated a table using SQLiteStudio. I then exported the database, and using FMDB, plopped it in my app. Everything works fine there and I'm able to retrieve various fields.
What I'm trying to do is list out the measurements and ingredients so that it displays with line breaks:
0.5 oz.
1.0 oz.
1 jigger
And not as: 0.5 oz., 1.0 oz., 1 jigger which is how I wrote it into the Google doc.
I've tried using \n in the SQLite viewer but it outputs it as a String rather than encoding it as a new line. However, when I view the accompanying .sql file, TextMate2 reads it as new lines. I'm not sure if I have to apply some code within my tableviewcontroller implementation file where I call the FMDB or elsewhere.
I have been looking for a solution for awhile now but no luck. Any help in steering me in the right direction would be greatly appreciated. Thanks!
CocktailListTableViewController.m
- (NSMutableArray *)recipeCocktails {
recipeCocktails = [[NSMutableArray alloc] init];
NSString *databasePath = [(AppDelegate *) [[UIApplication sharedApplication] delegate] databasePath];
FMDatabase *fmDB = [FMDatabase databaseWithPath:databasePath];
if(![fmDB open])
{
NSLog(#"Could not open DB, try again");
return nil;
}
FMResultSet *results = nil;
results = [fmDB executeQuery:#"SELECT * FROM recipesCocktails"];
NSLog(#"result %# ",results);
if ([fmDB hadError]) {
NSLog(#"DB Error %d: %#", [fmDB lastErrorCode], [fmDB lastErrorMessage]);
}
while ([results next]) {
Cocktails *cocktails = [[Cocktails alloc] init];
cocktails.recipeName = [results stringForColumn:#"recipeName"];
cocktails.recipeMeasure = [results stringForColumn:#"recipeMeasure"];
cocktails.recipeIngred = [results stringForColumn:#"recipeIngred"];
cocktails.recipeGlass = [results stringForColumn:#"recipeGlass"];
cocktails.recipeShaker = [results stringForColumn:#"recipeShaker"];
cocktails.recipeDirections = [results stringForColumn:#"recipeDirections"];
[recipeCocktails addObject:cocktails];
}
[fmDB close];
return recipeCocktails;
CocktailsDetailTableViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = cocktails.recipeName;
self.recipeIngred.text = cocktails.recipeIngred;
self.recipeMeasure.text = cocktails.recipeMeasure;
I can see you're trying to store all the ingredients as one long, pre-built string, and I think that approach is not the best.
It's gonna work, but it's not future-friendly or simply... usual.
I strongly suggest having a new table that knows all the possible ingredients, with ID's and other informations of your will (color, average weight, picture* and whatnot)
( please note that it's not advised to store images in a SQL database and you'd rather store references to where you saved them as files or URL's)*
Once you have that table, you're golden. You simply reference an array of ingredients in your recipe table. For example [1,27,133], and with that you can do what you want. You have an array of ingredient objects, which you can treat how you see fit.
I would, for example, loop over it and create labels under each other and you'd have your lines.
I could also simply use an NSMutableString and fill it with the ingredients.name (again, using examples)
You could fill a UITextView with newlines between each ingredient (by using the NSMutableString and adding newlines between each object name).
And more importantly, you can modify everything without having to re-work your layouts or labels or re-write your whole database because you have static newlines ingredients for each recipe.
That is the most important point I wanted to make. With that approach you can remove any ingredient from any recipe without having to think really far or risk forgetting something. You could also permanently destroy any ingredient to every recipe by removing it from the ingredient table. Same for adding new ones. Or saying you want to change the name of ingredient X, you dont' have to do it in every recipe, and so on.
EDIT: Answering your question in comment
To load the recipes in the tableview and the ingredients on the detail page, you can do it like so :
You load all your recipes in an array and display that (I think you're successful already on that part).
Note that all the recipes have a property called "Ingredients" which is an array of ingredient keys/ids/something
On cell tap, you want to load the ingredients that you only know the ID of, you simply get them from the array of Id's that you stored in your recipe, and SELECT them from your db.
Globally, you have no idea what the ingredients are until the user taps : if you don't need to display them in the table view, then you don't need to laod them yet. But you already know their id (otherwise you wouldn't be able to find them!).
There are two approaches to this problem.
One approach is to store the list of ingredients in as a single text field, with new line characters inside the string. SQLite (and FMDB) handle this fine.
From the UI perspective, you would then show this text field in a multiline UITextField or a UILabel. If you use UILabel, make sure the number of lines is set to something other than 1 (i.e. either zero or the actual number of lines).
The trick here is now to get this newline string into the database. And it depends upon how you're doing this. But if you're using some SQLite tool, you cannot use \n sequence, as that's not acceptable SQL syntax. You can use x'0a' or char(10):
insert into foo (bar) values ('baz' || x'0a' || 'qux');
or:
insert into foo (bar) values ('baz' || char(10) || 'qux');
or, if using SQLite command line tool, you can just insert newline:
insert into foo (bar) values ('baz
qux');
But you cannot use \n sequence. You can do this from your Objective-C code, but not from within a third party SQLite tool.
The other approach is to store the list of recipes and the list of ingredients for each recipe in two different tables (e.g. recipes and ingredients). Then you could store each of the ingredients in a separate row in the ingredients table.
This strikes me as a more logical implementation. If you did this, you would then have the choice as to how you wanted to show the list of ingredients (separate UILabel controls, a UITableView, or, if you really wanted to, concatenate them together, separated with newline characters and show them in a single multiline control.

Searching and Sorting Quickly

I've been working on a search algorithm all afternoon and I'd like some opinions. Some of what I'm doing is specific to iOS, but the general concepts are not.
I'm trying to display a set of data, a directory. In the directory I have departments and people. I know this sounds like a textbook example, hear me out. It's not homework, I promise. (I can provide screenshots of what I'm working on.)
I have an array of entries, where there are those two kinds of directory entries. I need to sort the entries by name, then break up the array into smaller arrays, where each sub-array contains the entries that begin with the same letter.
Additionally, I need to account for a search string that the user may enter.
My general process is this:
Filter all the entries that match the type and search string if there is one. For this step I use an NSPredicate:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"type == %i AND searchableContents B[cd] %#", type, searchString];
if (!searchString || searchString.length == 0)
{
predicate = [NSPredicate predicateWithFormat:#"type == %i", type];
}
NSArray *array = [_directoryContents filteredArrayUsingPredicate:predicate];
Sort the results alphabetically.
array = [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
return [((BRKDirectoryEntry *)obj1).comperableTitle compare:((BRKDirectoryEntry *)obj2).comperableTitle];
}];
Break up the results into smaller arrays. For performance, I skip this step if we're searching, but it doesn't seem to help.
if(alphabetized)
{
array = [self _alphabetizedArrayFromPresortedArray:array];
}
The performance of this on a total of 950 entries is abysmal.
Now, for my default display, I can get away with simply caching the sorted data in memory, and then display and scrolling performs nicely, but for search-as-I-type, there's simply no way to achieve the smooth performance that users expect.
Any pointers or tips?
Yes. Forget files and keep it in a database. Create your indexes Everything becomes a simple SQL statement.

CoreData predicate: string property length?

Say if I have an entity Fragment, it has an attribute 'text' which is a string, I want to query the list of Fragment whose text is of length 5:
[NSPredicate predicateWithFormat:#"position == %# AND text.length == %d", pos, 5];
It does not work (ie returns no result), but if I remove text.length in the query it works and I'm certain that there are texts of length 5, so what do I need to change it to?
Thanks!
There is no length attribute for strings in the NSPredicate. Use regex instead.
Your predicate should look as follows:
[NSPredicate predicateWithFormat:#"position == %# AND text MATCHES %#", pos, #".{5}"];
You cannot use Objective-C functions like length in a Core Data fetch request. But you
can replace it with the "LIKE" operator, which does a simple pattern matching:
[NSPredicate predicateWithFormat:#"text LIKE %#", #"?????"];
An interesting point is that Core Data does not throw an exception or return with an error,
but just ignores the length method, i.e. it just uses the predicate "text = '5' instead.
This can be seen by activating Core Data debug output by setting the launch argument
-com.apple.CoreData.SQLDebug 3
(which is generally a good method to locate Core Data fetch problems).
I think that it's taking text.lengh as a relationship. Try to set a predicate only with position and then do a loop looking for text.length == 5.

Filtering NSMutable Array of custom objects

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

Resources