Search in all objects of a NSArray of dictionaries - objective-c

I have implemented the search method for a UITableView populate from a NSArray (mylist):
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope{
// reset array
[self.filteredListContent removeAllObjects];
// check the elements contained in the list
NSString *cellTitle;
for (cellTitle in myList){ //CHANGE HERE
NSComparisonResult result = [cellTitle compare:searchText options:NSCaseInsensitiveSearch range:NSMakeRange(0, [searchText length])];
if (result == NSOrderedSame){
[filteredListContent addObject:cellTitle];
}
}
}
Now i would the same method for search a char in objectForKey:#"name" of a List of NSDictionary:
myList [0]:
{
gender
id
name
picture }
myList [1]
{
gender
id
name
picture }
I would something like this:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope{
[self.filteredFriendsList removeAllObjects];
NSString *cellTitle;
for (cellTitle in [[friendsList /* all objects */] objectForKey:#"name"]){
NSComparisonResult result = [cellTitle compare:searchText options:NSCaseInsensitiveSearch range:NSMakeRange(0, [searchText length])];
if (result == NSOrderedSame){
[filteredFriendsList addObject:cellTitle];
}
}
}
Someone has some ideas?
Thank you.

Try this one:
for (NSString *name in [friendsList valueForKey:#"name"])
{
if ([name isEqualToString:searchText]) [filteredFriendsList addObject:name];
}
The key here is the fact that valueForKey: method once invoked on an NSArray it calls valueForKey: on every of its objects.
Ps. If you want a case insensitive comparison you can do so like:
NSComparisonResult res = [searchTex caseInsensitiveCompare:name];
if (res == NSOrderedSame) [filteredFriendsList addObject:name];
EDIT: (To include solution for substring matching)
if ([name rangeOfString:searchText].location =! NSNotFound)
{
// 'name' contains 'searchText'
}

Why don't you just have the method do:
Friend *friend;
for (friend in friendsList) {
NSString *cellTitle = [friend objectForKey:#"name"];
/* do comparison */
}

Related

Formatting NSString, Objective-C

What would be the best way to transform NSStrings like these (mixed case)
#"Hello lorem ipsum";
#"i am a test";
to these (camel case without spaces)
#"helloLoremIpsum";
#"iAmATest";
Try this
Using For Loop
- (NSString *)camelCased:(NSString *)aString {
NSMutableString *result = [NSMutableString new];
NSArray *words = [aString componentsSeparatedByString: #" "];
for (NSUInteger i = 0; i < words.count; i++) {
if (i==0) {
[result appendString:([words[i] lowercaseString])];
}
else {
[result appendString:([words[i] capitalizedString])];
}
}
return result;
}
Using Block
- (NSString *)camelCasedUsingBlock:(NSString *)aString {
NSMutableArray *words = [[NSMutableArray alloc] init];
[[aString componentsSeparatedByString:#" "] enumerateObjectsUsingBlock:^(id anObject, NSUInteger idx, BOOL *stop) {
if (idx == 0) {
[words addObject:[anObject lowercaseString]];
}
else{
[words addObject:[anObject capitalizedString]];
}
}];
return [words componentsJoinedByString:#""];
}
NSLog(#"%#",[self camelCased:#"Hello lorem ipsum"]);//helloLoremIpsum
NSLog(#"%#",[self camelCased:#"i am a test"]);//iAmATest
It's overkill but TransformerKit has a bunch of NSValueTransformers that can be used, one is for camel casing. The relevant file is TTTStringTransformers.m if you want to build your own solution for lightness.

Sort NSmutablearray with two special keys

I have a tableview, its header is stored in a mutablearray, the array looks like
(2005 fall, 2005 spring, 2007 summer...)
When I output the tableview, I want the header in time ascending displayed.
2005 spring
2005 fall
2007 summer
I used the code here:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
[self.sectionKeys sortUsingSelector:#selector(localizedCaseInsensitiveCompare:)];
NSString *key = [self.sectionKeys objectAtIndex:section];
return key;
}
It works fine with year. However, fall comes before spring and summer because of alphabetreason , what to do to fix it please?
Use a custom comparator to get a custom sort order:
NSMutableArray *array = [#[ #"2005 fall", #"2005 spring", #"2007 summer" ] mutableCopy];
NSArray *seasons = #[ #"spring", #"summer", #"fall", #"winter" ];
[array sortUsingComparator:^NSComparisonResult(NSString *str1, NSString *str2) {
NSArray *parts1 = [str1 componentsSeparatedByString:#" "];
NSArray *parts2 = [str1 componentsSeparatedByString:#" "];
NSString *year1 = parts1[0];
NSString *year2 = parts2[0];
NSComparisonResult yearRes = [year1 compare:year2 options:NSNumericSearch];
if (yearRes == NSOrderedSame) {
NSString *season1 = parts1[1];
NSString *season2 = parts2[1];
NSUInteger index1 = [seasons indexOfObject:season1];
NSUInteger index2 = [seasons indexOfObject:season2];
if (index1 < index2) {
return NSOrderedAscending;
} else if (index1 > index2) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
} else {
return yearRes;
}
}];
Note - I might have the NSOrderedAscending and NSOrderedDescending backwards. Swap them if the sort of the seasons in the same year come out in the reverse order.
You need a lookup mechanism to define the ordering of the seasons
NSArray *seasons = #[#"spring", #"summer", #"fall", #"winter"];
NSArray *strings = #[#"2005 fall",#"2007 spring", #"2005 spring", #"2007 winter", #"2005 winter"];
strings = [strings sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
NSArray *string1Comps = [obj1 componentsSeparatedByString:#" "];
NSArray *string2Comps = [obj2 componentsSeparatedByString:#" "];
NSComparisonResult compareYearResult = [#([string1Comps[0] integerValue]) compare:#([string2Comps[0] integerValue]) ];
if (compareYearResult == NSOrderedSame) {
return [#([seasons indexOfObject:string1Comps[1]]) compare:#([seasons indexOfObject:string2Comps[1]])];
}
return compareYearResult;
}];
result
(
2005 spring,
2005 fall,
2005 winter,
2007 spring,
2007 winter
)
Another look up mechanism could be a block
NSNumber* (^lookUpSeason)(NSString *) = ^(NSString *seasonname){
static NSArray *seasons;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
seasons = #[#"spring", #"summer", #"fall", #"winter"];
});
return #([seasons indexOfObject:seasonname]);
};
This might look a bit cumbersome at first, but increases readability when used.
return [#([seasons indexOfObject:string1Comps[1]]) compare:#([seasons indexOfObject:string2Comps[1]])];
becomes
return [lookUpSeason(string1Comps[1]) compare:lookUpSeason(string2Comps[1])];
in both cases you could also give the lookup code into the comparator block, this will give you the opportunity to remove the same comparator with the lookup in other places.
like:
NSArray *strings = #[#"2005 fall", #"2007 spring", #"2005 spring", #"2007 winter", #"2005 winter", #"2005 summer", #"2000 hhh"];
NSComparisonResult (^yearAndSeasonComparator)(id,id) = ^NSComparisonResult(NSString *obj1, NSString *obj2) {
NSNumber* (^lookUpSeason)(NSString *) = ^(NSString *seasonname){
static NSArray *seasons;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
seasons = #[#"spring", #"summer", #"fall", #"winter"];
});
return #([seasons indexOfObject:seasonname]);
};
NSArray *string1Comps = [obj1 componentsSeparatedByString:#" "];
NSArray *string2Comps = [obj2 componentsSeparatedByString:#" "];
NSComparisonResult compareYearResult = [#([string1Comps[0] integerValue]) compare:#([string2Comps[0] integerValue]) ];
if (compareYearResult == NSOrderedSame) {
return [lookUpSeason(string1Comps[1]) compare:lookUpSeason(string2Comps[1])];
}
return compareYearResult;
};
strings = [strings sortedArrayUsingComparator:yearAndSeasonComparator];
The block assigned to yearAndSeasonComparator could now be reused in other places that would sort similar strings.
So you have an array with section keys. But the sections are not in order of the array, they need to be sorted. You will notice that cellForRowAtIndexPath: needs the exact same information. So sorting in this place is wrong.
What I do to handle this: I have a property "unsortedSectionKeys" and a property "sortedSectionKeys". sortedSectionKeys has a getter that checks for nil and stores a sorted copy of unsortedSectionKeys if it is nil. And whenever unsortedSectionKeys changes, you just set sortedSectionKeys to nil. (That solves at least some problems).
For your sorting, you need to write proper code. Use (void)sortUsingComparator:(NSComparator)cmptr to sort a mutable, or - (NSArray *)sortedArrayUsingComparator:(NSComparator)cmptr to get a sorted copy of an array.
Example:
[self.sectionKeys sortArrayUsingComparator:^NSComparisonResult(NSString* obj1, NSString* obj2) {
NSInteger year1 = obj1.integerValue;
NSInteger year2 = obj2.integerValue;
if (year1 < year2) return NSOrderedAscending;
if (year1 > year2) return NSOrderedDescending;
NSInteger season1 = 0;
if ([obj1 rangeOfString:#"spring" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 1;
if ([obj1 rangeOfString:#"summer" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 2;
if ([obj1 rangeOfString:#"fall" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 3;
if ([obj1 rangeOfString:#"winter" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 4;
NSInteger season2 = 0;
if ([obj2 rangeOfString:#"spring" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 1;
if ([obj2 rangeOfString:#"summer" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 2;
if ([obj2 rangeOfString:#"fall" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 3;
if ([obj2 rangeOfString:#"winter" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 4;
if (season1 < season2) return NSOrderedAscending;
if (season1 > season2) return NSOrderedDescending;
return NSOrderedSame;
}];
Your decision if winter is the first or last season in the year, since usually it's December to February.

Simplest way to concatenate objective-C strings and achieve grammatical correctness (with 'and' conjunction)

What is the easiest and best way in Objective-C to combine a list (NSArray) of NSStrings into a single NSString separated by commas, with the grammatically correct terminal conjunction ", and " before the final item of the list?
NSArray *anArray = [NSArray arrayWithObjects:#"milk", #"butter", #"eggs", #"spam", nil];
From this array, I want the NSString #"milk, butter, eggs, and spam".
More generally, if the list is more than two items long, I want ", " between every item except the last and second-to-last (which should have ", and "). If the list is two items long, I want just the ' and ' with no comma. If the list is one item long, I want the single string from the array.
I like something as simple as:
NSString *newString = [anArray componentsJoinedByString:#", "];
But this of course omits the 'and' conjunction.
Is there a simpler and/or faster Objective-C way than the following:
- (NSString *)grammaticallyCorrectStringFromArrayOfStrings:(NSArray *)anArray {
if (anArray == nil) return nil;
int arrayCount = [anArray count];
if (arrayCount == 0) return #"";
if (arrayCount == 1) return [anArray objectAtIndex:0];
if (arrayCount == 2) return [anArray componentsJoinedByString:#" and "];
// arrayCount > 2
NSString *newString = #"";
for (NSString *thisString in anArray) {
if (thisString != [anArray objectAtIndex:0] && thisString != [anArray lastObject]) {
newString = [newString stringByAppendingString:#", "];
}
else if (thisString == [anArray lastObject]) {
newString = [newString stringByAppendingString:#", and "];
}
newString = [newString stringByAppendingString:thisString];
}
return newString;
}
For the loop, I'd probably do something like
NSMutableString *newString = [NSMutableString string];
NSUInteger lastIndex = arrayCount - 1;
[anArray enumerateObjectsUsingBlock:^(NSString *thisString, NSUInteger idx, BOOL *stop) {
if (idx != 0)
[newString appendString:#","];
if (idx == lastIndex)
[newString appendString:#" and "];
[newString appendString:thisString];
}];
Though I guess that's not really less lines.

Check for substring in string with NSString from NSArray?

So pretty much I want to check if my NSString from my NSArray is a substring of my string named imageName.
So lets say this:
My Image name is: picture5of-batman.png
My Array contains strings and one of them is: Batman
So pretty much I want to eliminate the: picture5of- part of the image name and replace it with the NSString from the NSArray.
This is how I try to do it but it never makes it to the if statement. And no my Array is not nil either. Here is the code:
for (NSString *string in superheroArray) {
if ([string rangeOfString:imageName].location != NSNotFound) {
//Ok so some string in superheroArray is equal to the file name of the image
imageName = [imageName stringByReplacingOccurrencesOfString:#"" withString:string
options:NSCaseInsensitiveSearch range:NSMakeRange(0, string.length)];
}
}
Edit1: This still does not work
for (NSString *string in superheroArray) {
if ([imageName rangeOfString:string options:NSCaseInsensitiveSearch].location != NSNotFound) {
//Ok so some string in superheroArray is equal to the file name of the image
imageName = string;
//HOW ABOUT THAT FOR EFFICIENCY :P
}
}
[imageName rangeOfString:string options: NSCaseInsensitiveSearch]
I don't see why it's not working in your code, maybe split the NSString stuff from the NSRage test.
but this work here :
NSArray *ar = [NSArray arrayWithObjects:#"Batman", #"Maurice", nil];
__block NSString *imageName = #"picture5of-batman.png";
__block NSUInteger theIndex = -1;
[ar enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSRange r = [imageName rangeOfString: obj
options: NSCaseInsensitiveSearch];
if (r.location != NSNotFound)
{
theIndex = idx;
NSString *str = [imageName pathExtension];
imageName = [(NSString *)obj stringByAppendingPathExtension:str];
// you found it, so you can stop now
*stop = YES;
}
}];
if (theIndex != -1)
{
NSLog(#"The index is : %d and new imageName == %#", theIndex, imageName);
}
And here is the NSLog statement :
2011-12-10 23:04:28.967 testSwitch1[2493:207] The index is : 0 and new imageName == Batman.png

How can I optimise out this nested for loop?

How can I optimise out this nested for loop?
The program should go through each word in the array created from the word text file, and if it's greater than 8 characters, add it to the goodWords array. But the caveat is that I only want the root word to be in the goodWords array, for example:
If greet is added to the array, I don't want greets or greetings or greeters, etc.
NSString *string = [NSString stringWithContentsOfFile:#"/Users/james/dev/WordParser/word.txt" encoding:NSUTF8StringEncoding error:NULL];
NSArray *words = [string componentsSeparatedByString:#"\r\n"];
NSMutableArray *goodWords = [NSMutableArray array];
BOOL shouldAddToGoodWords = YES;
for (NSString *word in words)
{
NSLog(#"Word: %#", word);
if ([word length] > 8)
{
NSLog(#"Word is greater than 8");
for (NSString *existingWord in [goodWords reverseObjectEnumerator])
{
NSLog(#"Existing Word: %#", existingWord);
if ([word rangeOfString:existingWord].location != NSNotFound)
{
NSLog(#"Not adding...");
shouldAddToGoodWords = NO;
break;
}
}
if (shouldAddToGoodWords)
{
NSLog(#"Adding word: %#", word);
[goodWords addObject:word];
}
}
shouldAddToGoodWords = YES;
}
How about something like this?
//load the words from wherever
NSString * allWords = [NSString stringWithContentsOfFile:#"/usr/share/dict/words"];
//create a mutable array of the words
NSMutableArray * words = [[allWords componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] mutableCopy];
//remove any words that are shorter than 8 characters
[words filterUsingPredicate:[NSPredicate predicateWithFormat:#"length >= 8"]];
//sort the words in ascending order
[words sortUsingSelector:#selector(caseInsensitiveCompare:)];
//create a set of indexes (these will be the non-root words)
NSMutableIndexSet * badIndexes = [NSMutableIndexSet indexSet];
//remember our current root word
NSString * currentRoot = nil;
NSUInteger count = [words count];
//loop through the words
for (NSUInteger i = 0; i < count; ++i) {
NSString * word = [words objectAtIndex:i];
if (currentRoot == nil) {
//base case
currentRoot = word;
} else if ([word hasPrefix:currentRoot]) {
//word is a non-root word. remember this index to remove it later
[badIndexes addIndex:i];
} else {
//no match. this word is our new root
currentRoot = word;
}
}
//remove the non-root words
[words removeObjectsAtIndexes:badIndexes];
NSLog(#"%#", words);
[words release];
This runs very very quickly on my machine (2.8GHz MBP).
A Trie seems suitable for your purpose. It is like a hash, and is useful for detecting if a given string is a prefix of an already seen string.
I used an NSSet to ensure that you only have 1 copy of a word added at a time. It will add a word if the NSSet does not already contain it. It then checks to see if the new word is a substring for any word that has already been added, if true then it won't add the new word. It's case-insensitive as well.
What I've written is a refactoring of your code. It's probably not that much faster but you really do want a tree data structure if you want to make it a lot faster when you want to search for words that have already been added to your tree.
Take a look at RedBlack Trees or B-Trees.
Words.txt
objective
objectively
cappucin
cappucino
cappucine
programme
programmer
programmatic
programmatically
Source Code
- (void)addRootWords {
NSString *textFile = [[NSBundle mainBundle] pathForResource:#"words" ofType:#"txt"];
NSString *string = [NSString stringWithContentsOfFile:textFile encoding:NSUTF8StringEncoding error:NULL];
NSArray *wordFile = [string componentsSeparatedByString:#"\n"];
NSMutableSet *goodWords = [[NSMutableSet alloc] init];
for (NSString *newWord in wordFile)
{
NSLog(#"Word: %#", newWord);
if ([newWord length] > 8)
{
NSLog(#"Word '%#' contains 8 or more characters", newWord);
BOOL shouldAddWord = NO;
if ( [goodWords containsObject:newWord] == NO) {
shouldAddWord = YES;
}
for (NSString *existingWord in goodWords)
{
NSRange textRange = [[newWord lowercaseString] rangeOfString:[existingWord lowercaseString]];
if( textRange.location != NSNotFound ) {
// newWord contains the a substring of existingWord
shouldAddWord = NO;
break;
}
NSLog(#"(word:%#) does not contain (substring:%#)", newWord, existingWord);
shouldAddWord = YES;
}
if (shouldAddWord) {
NSLog(#"Adding word: %#", newWord);
[goodWords addObject:newWord];
}
}
}
NSLog(#"***Added words***");
int count = 1;
for (NSString *word in goodWords) {
NSLog(#"%d: %#", count, word);
count++;
}
[goodWords release];
}
Output:
***Added words***
1: cappucino
2: programme
3: objective
4: programmatic
5: cappucine