Check for substring in string with NSString from NSArray? - objective-c

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

Related

Objective-C: How to find the most common string in an array?

I have an array of strings from an online database that I trying to determine the most commonly used word. The values inside the arrays will vary but I want to check the most common words of whatever collection or words I'm using. If theoretically I had an array of the following...
NSArray *stringArray = [NSArray arrayWithObjects:#"Duck", #"Duck", #"Duck", #"Duck", #"Goose"];
How do I iterate through this array to determine the most common string, which would obviously be "Duck"?
Simplest way is probably NSCountedSet:
NSCountedSet* stringSet = [[NSCountedSet alloc] initWithArray:strings];
NSString* mostCommon = nil;
NSUInteger highestCount = 0;
for(NSString* string in stringSet) {
NSUInteger count = [stringSet countForObject:string];
if(count > highestCount) {
highestCount = count;
mostCommon = string;
}
}
You can use the word as a key into a dictionary.
NSMutableDictionary *words = [NSMutableDictionary dictionary];
for (NSString *word in stringArray) {
if (!words[word]) {
[words setValue:[NSDecimalNumber zero] forKey:word];
}
words[word] = [words[word] decimalNumberByAdding:[NSDecimalNumber one]];
}
Now iterate through words and find the key with the highest value.
NSString *mostCommon;
NSDecimalNumber *curMax = [NSDecimalNumber zero];
for (NSString *key in [words allKeys]) {
if ([words[key] compare:curMax] == NSOrderedDescending) {
mostCommon = key;
curMax = word[key];
}
}
NSLog(#"Most Common Word: %#", mostCommon);
EDIT: Rather than looping through the array once then looping separately through the sorted dictionary, I think we can do better and do it all in a single loop.
NSString *mostCommon;
NSDecimalNumber *curMax = [NSDecimalNumber zero];
NSMutableDictionary *words = [NSMutableDictionary dictionary];
for (NSString *word in stringArray) {
if (!words[word]) {
[words setValue:[NSDecimalNumber zero] forKey:word];
}
words[word] = [words[word] decimalNumberByAdding:[NSDecimalNumber one]];
if ([words[word] compare:curMax] == NSOrderedDescending) {
mostCommon = word;
curMax = words[word];
}
}
NSLog(#"Most Common Word: %#", mostCommon);
This should be significantly faster than my answer pre-edit, though I don't know how it compares to using the NSCountedSet answer.
Try using NSPredicate.
NSUInteger count=0;
NSString *mostCommonStr;
for(NSString *strValue in stringArray) {
NSUInteger countStr=[[stringArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"self MATCHES[CD] %#, strValue]]count];
if(countStr > count) {
count=countStr;
mostCommonStr=strValue;
}
}
NSLog(#"The most commonstr is %#",mostCommonStr);

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.

Search in all objects of a NSArray of dictionaries

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 */
}

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

CamelCase to underscores and back in Objective-C

I'm looking for a simple, efficient way to convert strings in CamelCase to underscore notation (i.e., MyClassName -> my_class_name) and back again in Objective C.
My current solution involves lots of rangeOfString, characterAtIndex, and replaceCharactersInRange operations on NSMutableStrings, and is just plain ugly as hell :) It seems that there must be a better solution, but I'm not sure what it is.
I'd rather not import a regex library just for this one use case, though that is an option if all else fails.
Chris's suggestion of RegexKitLite is good. It's an excellent toolkit, but this could be done pretty easily with NSScanner. Use -scanCharactersFromSet:intoString: alternating between +uppercaseLetterCharacterSet and +lowercaseLetterCharacterSet. For going back, you'd use -scanUpToCharactersFromSet: instead, using a character set with just an underscore in it.
How about these:
NSString *MyCamelCaseToUnderscores(NSString *input) {
NSMutableString *output = [NSMutableString string];
NSCharacterSet *uppercase = [NSCharacterSet uppercaseLetterCharacterSet];
for (NSInteger idx = 0; idx < [input length]; idx += 1) {
unichar c = [input characterAtIndex:idx];
if ([uppercase characterIsMember:c]) {
[output appendFormat:#"_%#", [[NSString stringWithCharacters:&c length:1] lowercaseString]];
} else {
[output appendFormat:#"%C", c];
}
}
return output;
}
NSString *MyUnderscoresToCamelCase(NSString *underscores) {
NSMutableString *output = [NSMutableString string];
BOOL makeNextCharacterUpperCase = NO;
for (NSInteger idx = 0; idx < [underscores length]; idx += 1) {
unichar c = [underscores characterAtIndex:idx];
if (c == '_') {
makeNextCharacterUpperCase = YES;
} else if (makeNextCharacterUpperCase) {
[output appendString:[[NSString stringWithCharacters:&c length:1] uppercaseString]];
makeNextCharacterUpperCase = NO;
} else {
[output appendFormat:#"%C", c];
}
}
return output;
}
Some drawbacks are that they do use temporary strings to convert between upper and lower case, and they don't have any logic for acronyms, so myURL will result in my_u_r_l.
Try this magic:
NSString* camelCaseString = #"myBundleVersion";
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])" options:0 error:nil];
NSString *underscoreString = [[regex stringByReplacingMatchesInString:camelCaseString options:0 range:NSMakeRange(0, camelCaseString.length) withTemplate:#"_$1$2"] lowercaseString];
NSLog(#"%#", underscoreString);
Output: my_bundle_version
If your concern is just the visibility of your code, you could make a category for NSString using the methods you've designed already. That way, you only see the ugly mess once. ;)
For instance:
#interface NSString(Conversions) {
- (NSString *)asCamelCase;
- (NSString *)asUnderscored;
}
#implementation NSString(Conversions) {
- (NSString *)asCamelCase {
// whatever you came up with
}
- (NSString *)asUnderscored {
// whatever you came up with
}
}
EDIT: After a quick Google search, I couldn't find any way of doing this, even in plain C. However, I did find a framework that could be useful. It's called RegexKitLite. It uses the built-in ICU library, so it only adds about 20K to the final binary.
Here's my implementation of Rob's answer:
#implementation NSString (CamelCaseConversion)
// Convert a camel case string into a dased word sparated string.
// In case of scanning error, return nil.
// Camel case string must not start with a capital.
- (NSString *)fromCamelCaseToDashed {
NSScanner *scanner = [NSScanner scannerWithString:self];
scanner.caseSensitive = YES;
NSString *builder = [NSString string];
NSString *buffer = nil;
NSUInteger lastScanLocation = 0;
while ([scanner isAtEnd] == NO) {
if ([scanner scanCharactersFromSet:[NSCharacterSet lowercaseLetterCharacterSet] intoString:&buffer]) {
builder = [builder stringByAppendingString:buffer];
if ([scanner scanCharactersFromSet:[NSCharacterSet uppercaseLetterCharacterSet] intoString:&buffer]) {
builder = [builder stringByAppendingString:#"-"];
builder = [builder stringByAppendingString:[buffer lowercaseString]];
}
}
// If the scanner location has not moved, there's a problem somewhere.
if (lastScanLocation == scanner.scanLocation) return nil;
lastScanLocation = scanner.scanLocation;
}
return builder;
}
#end
Here's yet another version based on all the above. This version handles additional forms. In particular, tested with the following:
camelCase => camel_case
camelCaseWord => camel_case_word
camelURL => camel_url
camelURLCase => camel_url_case
CamelCase => camel_case
Here goes
- (NSString *)fromCamelCaseToDashed3 {
NSMutableString *output = [NSMutableString string];
NSCharacterSet *uppercase = [NSCharacterSet uppercaseLetterCharacterSet];
BOOL previousCharacterWasUppercase = FALSE;
BOOL currentCharacterIsUppercase = FALSE;
unichar currentChar = 0;
unichar previousChar = 0;
for (NSInteger idx = 0; idx < [self length]; idx += 1) {
previousChar = currentChar;
currentChar = [self characterAtIndex:idx];
previousCharacterWasUppercase = currentCharacterIsUppercase;
currentCharacterIsUppercase = [uppercase characterIsMember:currentChar];
if (!previousCharacterWasUppercase && currentCharacterIsUppercase && idx > 0) {
// insert an _ between the characters
[output appendString:#"_"];
} else if (previousCharacterWasUppercase && !currentCharacterIsUppercase) {
// insert an _ before the previous character
// insert an _ before the last character in the string
if ([output length] > 1) {
unichar charTwoBack = [output characterAtIndex:[output length]-2];
if (charTwoBack != '_') {
[output insertString:#"_" atIndex:[output length]-1];
}
}
}
// Append the current character lowercase
[output appendString:[[NSString stringWithCharacters:&currentChar length:1] lowercaseString]];
}
return output;
}
If you are concerned with the speed of your code you probably want to write a more performant version of the code:
- (nonnull NSString *)camelCaseToSnakeCaseString {
if ([self length] == 0) {
return #"";
}
NSMutableString *output = [NSMutableString string];
NSCharacterSet *digitSet = [NSCharacterSet decimalDigitCharacterSet];
NSCharacterSet *uppercaseSet = [NSCharacterSet uppercaseLetterCharacterSet];
NSCharacterSet *lowercaseSet = [NSCharacterSet lowercaseLetterCharacterSet];
for (NSInteger idx = 0; idx < [self length]; idx += 1) {
unichar c = [self characterAtIndex:idx];
// if it's the last one then just append lowercase of character
if (idx == [self length] - 1) {
if ([uppercaseSet characterIsMember:c]) {
[output appendFormat:#"%#", [[NSString stringWithCharacters:&c length:1] lowercaseString]];
}
else {
[output appendFormat:#"%C", c];
}
continue;
}
unichar nextC = [self characterAtIndex:(idx+1)];
// this logic finds the boundaries between lowercase/uppercase/digits and lets the string be split accordingly.
if ([lowercaseSet characterIsMember:c] && [uppercaseSet characterIsMember:nextC]) {
[output appendFormat:#"%#_", [[NSString stringWithCharacters:&c length:1] lowercaseString]];
}
else if ([lowercaseSet characterIsMember:c] && [digitSet characterIsMember:nextC]) {
[output appendFormat:#"%#_", [[NSString stringWithCharacters:&c length:1] lowercaseString]];
}
else if ([digitSet characterIsMember:c] && [uppercaseSet characterIsMember:nextC]) {
[output appendFormat:#"%#_", [[NSString stringWithCharacters:&c length:1] lowercaseString]];
}
else {
// Append lowercase of character
if ([uppercaseSet characterIsMember:c]) {
[output appendFormat:#"%#", [[NSString stringWithCharacters:&c length:1] lowercaseString]];
}
else {
[output appendFormat:#"%C", c];
}
}
}
return output;
}
I have combined the answers found here into my refactoring library, es_ios_utils. See NSCategories.h:
#property(nonatomic, readonly) NSString *asCamelCaseFromUnderscores;
#property(nonatomic, readonly) NSString *asUnderscoresFromCamelCase;
Usage:
#"my_string".asCamelCaseFromUnderscores
yields #"myString"
Please push improvements!
I happened upon this question looking for a way to convert Camel Case to a spaced, user displayable string. Here is my solution which worked better than replacing #"_" with #" "
- (NSString *)fromCamelCaseToSpaced:(NSString*)input {
NSCharacterSet* lower = [NSCharacterSet lowercaseLetterCharacterSet];
NSCharacterSet* upper = [NSCharacterSet uppercaseLetterCharacterSet];
for (int i = 1; i < input.length; i++) {
if ([upper characterIsMember:[input characterAtIndex:i]] &&
[lower characterIsMember:[input characterAtIndex:i-1]])
{
NSString* soFar = [input substringToIndex:i];
NSString* left = [input substringFromIndex:i];
return [NSString stringWithFormat:#"%# %#", soFar, [self fromCamelCaseToSpaced:left]];
}
}
return input;
}
OK guys. Here is an all regex answer, which I consider the only true way:
Given:
NSString *MYSTRING = "foo_bar";
NSRegularExpression *_toCamelCase = [NSRegularExpression
regularExpressionWithPattern:#"(_)([a-z])"
options:NSRegularExpressionCaseInsensitive error:&error];
NSString *camelCaseAttribute = [_toCamelCase
stringByReplacingMatchesInString:MYSTRING options:0
range:NSMakeRange(0, attribute.length)
withTemplate:#"\\U$2"];
Yields fooBar.
Conversely:
NSString *MYSTRING = "fooBar";
NSRegularExpression *camelCaseTo_ = [NSRegularExpression
regularExpressionWithPattern:#"([A-Z])"
options:0 error:&error];
NSString *underscoreParsedAttribute = [camelCaseTo_
stringByReplacingMatchesInString:MYSTRING
options:0 range:NSMakeRange(0, attribute.length)
withTemplate:#"_$1"];
underscoreParsedAttribute = [underscoreParsedAttribute lowercaseString];
Yields: foo_bar.
\U$2 replaces second capture group with upper-case version of itself :D
\L$1 however, oddly, does not replace the first capture group with a lower-case version of itself :( Not sure why, it should work. :/