Objective-C: Find and Replace in String - ios

I have a string, dynamically generated from a website, that I would like to change.
I already know you need to change the string to a NSMutableString, but the part I want to remove is almost never the same. (e.g. [XXXXX] or [XABXX] ... there are thousands of combos).
The only static parts are the square brackets on either end and that there are 5 characters in between.
How would I recognise and remove this string of seven characters ([*****], with the *'s representing a random letter)?

A simple search and you can find it here:
with a simple change:
- (NSString *)stringCleaner:(NSString *)yourString {
NSScanner *theScanner;
NSString *text = nil;
theScanner = [NSScanner scannerWithString:yourString];
while ([theScanner isAtEnd] == NO) {
[theScanner scanUpToString:#"[" intoString:NULL] ;
[theScanner scanUpToString:#"]" intoString:&text] ;
yourString = [yourString stringByReplacingOccurrencesOfString:[NSString stringWithFormat:#"%#]", text] withString:#""];
}
return yourString;
}

Look at using the NSRegularExpression class to find each substring you need to operate on.

Related

Split an NSString at the first capital letter

I have a string like this #"abcdefghijklmnopqrstuvwxyzA". As you can see, A is at the end. How can I find the first capital letter and split the strings:
NSString *lower = #"abcdefghijklmnopqrstuvwxyz";
NSString *upper = #"A";
The string in the beginning is static so the capital letter could be ANYTHING. Will this scanner help?
NSString *String = titleLabelLatestNews.text;
NSScanner *stringScanner = [NSScanner scannerWithString:String];
NSString *content = [[NSString alloc] init];
while ([stringScanner isAtEnd] == NO) {
[stringScanner scanUpToString:#"url=\"" intoString:Nil];
[stringScanner scanUpToString:#"/>" intoString:&content];
}
For another example, #"this is all lower case letters I am awesome"; should become two strings, #"this is all lower case letters"; and #"I am awesome";
Get the idea? Anything before the Capital Letter goes to a string and anything after goes to another string.
An NSScanner will do the trick for you, yes. You just need to create an NSCharacterSet consisting of the capital letters, then use scanUpToCharactersFromSet:intoString:
NSString * s = #"this is all lower case letters I am awesome";
NSScanner * scanner = [NSScanner scannerWithString:s];
NSString * firstPart;
[scanner scanUpToCharactersFromSet:[NSCharacterSet uppercaseLetterCharacterSet]
intoString:&firstPart];
NSString * secondPart = [s substringFromIndex:[scanner scanLocation]];
If you insist on using NSScanner, use scanCharactersFromSet:intoString: where the NSCharacterSet is lowercase characters only.
What I would personally do, if anyone cares, is call rangeOfCharacterFromSet(NSCharacterSet.uppercaseLetterCharacterSet()...) and derive the resulting substrings from there.
A better solution is to use NSString's rangeOfCharacterFromSet
NSString *lowerCaseString=#"";
NSString *upperCaseString=#"";
NSString *stringToSplit = titleLabelLatestNews.text;
NSRange capitalRange=[stringToSplit rangeOfCharacterFromSet:[NSCharacterSet uppercaseLetterCharacterSet]];
if (capitalRange.location == NSNotFound) {
lowerCaseString=stringToSplit;
}
else if (capitalRange.location ==0 ) {
upperCaseString=stringToSplit;
}
else {
lowerCaseString=[stringToSplit substringToIndex:capitalRange.location-1];
upperCaseString=[stringToSplit substringFromIndex:capitalRange.location];
}
NSLog(#"lower case string=%# uppercase=%#",lowerCaseString,upperCaseString);
For completeness, the regular expression solution:
Use NSRegularExpression
The pattern #"([^A-Z]*)([A-Z].*)" will match what you want if you are only interested in A-Z as uppercase characters (see below for unicode change). Broken down this is two group, (...), one for before one for after; first group - anything which is not uppercase, [^A-Z], zero or more times, *; second group - an uppercase letter, [A-Z], followed by anything, .*.
Use firstMatchInString:options:range:; the NSTextCheckingResult will contain the ranges of the two matched groups.
If you wish to allow for Unicode's myriad of uppercase and titlecase letters just change A-Z above to \\p{Lu}\\p{Lt} (make sure you type the double-backslashes, you are passing a backslash to NSRegularExpression). Those two are all the Unicode uppercase letters, \\p{Lu}, and all the title case letters, \\p{Lt}.
HTH
Throwing one more solution into the mix utilizing componentsSeparatedByCharactersInSet: to split the string into multiple arrays (i.e. more than 2 if needed):
// Separate the "sentence" into components separated
// by the characters in the uppercase character set
NSMutableArray *sentenceArray = [[sentence componentsSeparatedByCharactersInSet:[NSCharacterSet uppercaseLetterCharacterSet]] mutableCopy];
// Get the first sentence "segment", i.e. the sentenceArray's
// first object
NSString *segment = [sentenceArray objectAtIndex:0];
// Keep track of the character count with a variable
int characterCount = (int)segment.length;
// Then starting from sentenceArray's index 1, go through
// the rest of sentenceArray's indices
for (int i = 1 ; i < sentenceArray.count ; i ++) {
// Append that "separator" character to the segment at the
// current index by accessing the character before the current segment
segment = [[NSString stringWithFormat:#"%c", [sentence characterAtIndex:characterCount]]stringByAppendingString:[sentenceArray objectAtIndex:i]];
// Replace the object at the current index with this new segment
// string
[sentenceArray replaceObjectAtIndex:i withObject:segment];
// Increment the character count
characterCount += segment.length;
}
NSLog(#"%#", sentenceArray);
// Find index of first capital letter
NSInteger index = ^NSInteger{
for (NSInteger i = 0; i < string.length; ++i) {
unichar c = [string characterAtIndex:i];
if ('A' <= c && c <= 'Z') { return i; }
}
return string.length; // No capital letter, take the entire string
}();
NSLog(#"lower = %#", [string substringToIndex:index]);
NSLog(#"upper = %#", [string substringFromIndex:index]);

Getting just the phone number digits from kABPersonPhoneProperty in iOS [duplicate]

I have an NSString (phone number) with some parenthesis and hyphens as some phone numbers are formatted. How would I remove all characters except numbers from the string?
Old question, but how about:
NSString *newString = [[origString componentsSeparatedByCharactersInSet:
[[NSCharacterSet decimalDigitCharacterSet] invertedSet]]
componentsJoinedByString:#""];
It explodes the source string on the set of non-digits, then reassembles them using an empty string separator. Not as efficient as picking through characters, but much more compact in code.
There's no need to use a regular expressions library as the other answers suggest -- the class you're after is called NSScanner. It's used as follows:
NSString *originalString = #"(123) 123123 abc";
NSMutableString *strippedString = [NSMutableString
stringWithCapacity:originalString.length];
NSScanner *scanner = [NSScanner scannerWithString:originalString];
NSCharacterSet *numbers = [NSCharacterSet
characterSetWithCharactersInString:#"0123456789"];
while ([scanner isAtEnd] == NO) {
NSString *buffer;
if ([scanner scanCharactersFromSet:numbers intoString:&buffer]) {
[strippedString appendString:buffer];
} else {
[scanner setScanLocation:([scanner scanLocation] + 1)];
}
}
NSLog(#"%#", strippedString); // "123123123"
EDIT: I've updated the code because the original was written off the top of my head and I figured it would be enough to point the people in the right direction. It seems that people are after code they can just copy-paste straight into their application.
I also agree that Michael Pelz-Sherman's solution is more appropriate than using NSScanner, so you might want to take a look at that.
The accepted answer is overkill for what is being asked. This is much simpler:
NSString *pureNumbers = [[phoneNumberString componentsSeparatedByCharactersInSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]] componentsJoinedByString:#""];
This is great, but the code does not work for me on the iPhone 3.0 SDK.
If I define strippedString as you show here, I get a BAD ACCESS error when trying to print it after the scanCharactersFromSet:intoString call.
If I do it like so:
NSMutableString *strippedString = [NSMutableString stringWithCapacity:10];
I end up with an empty string, but the code doesn't crash.
I had to resort to good old C instead:
for (int i=0; i<[phoneNumber length]; i++) {
if (isdigit([phoneNumber characterAtIndex:i])) {
[strippedString appendFormat:#"%c",[phoneNumber characterAtIndex:i]];
}
}
Though this is an old question with working answers, I missed international format support. Based on the solution of simonobo, the altered character set includes a plus sign "+". International phone numbers are supported by this amendment as well.
NSString *condensedPhoneNumber = [[phoneNumber componentsSeparatedByCharactersInSet:
[[NSCharacterSet characterSetWithCharactersInString:#"+0123456789"]
invertedSet]]
componentsJoinedByString:#""];
The Swift expressions are
var phoneNumber = " +1 (234) 567-1000 "
var allowedCharactersSet = NSMutableCharacterSet.decimalDigitCharacterSet()
allowedCharactersSet.addCharactersInString("+")
var condensedPhoneNumber = phoneNumber.componentsSeparatedByCharactersInSet(allowedCharactersSet.invertedSet).joinWithSeparator("")
Which yields +12345671000 as a common international phone number format.
Here is the Swift version of this.
import UIKit
import Foundation
var phoneNumber = " 1 (888) 555-5551 "
var strippedPhoneNumber = "".join(phoneNumber.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet))
Swift version of the most popular answer:
var newString = join("", oldString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet))
Edit: Syntax for Swift 2
let newString = oldString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet).joinWithSeparator("")
Edit: Syntax for Swift 3
let newString = oldString.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "")
Thanks for the example. It has only one thing missing the increment of the scanLocation in case one of the characters in originalString is not found inside the numbers CharacterSet object. I have added an else {} statement to fix this.
NSString *originalString = #"(123) 123123 abc";
NSMutableString *strippedString = [NSMutableString
stringWithCapacity:originalString.length];
NSScanner *scanner = [NSScanner scannerWithString:originalString];
NSCharacterSet *numbers = [NSCharacterSet
characterSetWithCharactersInString:#"0123456789"];
while ([scanner isAtEnd] == NO) {
NSString *buffer;
if ([scanner scanCharactersFromSet:numbers intoString:&buffer]) {
[strippedString appendString:buffer];
}
// --------- Add the following to get out of endless loop
else {
[scanner setScanLocation:([scanner scanLocation] + 1)];
}
// --------- End of addition
}
NSLog(#"%#", strippedString); // "123123123"
It Accept only mobile number
NSString * strippedNumber = [mobileNumber stringByReplacingOccurrencesOfString:#"[^0-9]" withString:#"" options:NSRegularExpressionSearch range:NSMakeRange(0, [mobileNumber length])];
It might be worth noting that the accepted componentsSeparatedByCharactersInSet: and componentsJoinedByString:-based answer is not a memory-efficient solution. It allocates memory for the character set, for an array and for a new string. Even if these are only temporary allocations, processing lots of strings this way can quickly fill the memory.
A memory friendlier approach would be to operate on a mutable copy of the string in place. In a category over NSString:
-(NSString *)stringWithNonDigitsRemoved {
static NSCharacterSet *decimalDigits;
if (!decimalDigits) {
decimalDigits = [NSCharacterSet decimalDigitCharacterSet];
}
NSMutableString *stringWithNonDigitsRemoved = [self mutableCopy];
for (CFIndex index = 0; index < stringWithNonDigitsRemoved.length; ++index) {
unichar c = [stringWithNonDigitsRemoved characterAtIndex: index];
if (![decimalDigits characterIsMember: c]) {
[stringWithNonDigitsRemoved deleteCharactersInRange: NSMakeRange(index, 1)];
index -= 1;
}
}
return [stringWithNonDigitsRemoved copy];
}
Profiling the two approaches have shown this using about 2/3 less memory.
You can use regular expression on mutable string:
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:
#"[^\\d]"
options:0
error:nil];
[regex replaceMatchesInString:str
options:0
range:NSMakeRange(0, str.length)
withTemplate:#""];
Built the top solution as a category to help with broader problems:
Interface:
#interface NSString (easyReplace)
- (NSString *)stringByReplacingCharactersNotInSet:(NSCharacterSet *)set
with:(NSString *)string;
#end
Implemenation:
#implementation NSString (easyReplace)
- (NSString *)stringByReplacingCharactersNotInSet:(NSCharacterSet *)set
with:(NSString *)string
{
NSMutableString *strippedString = [NSMutableString
stringWithCapacity:self.length];
NSScanner *scanner = [NSScanner scannerWithString:self];
while ([scanner isAtEnd] == NO) {
NSString *buffer;
if ([scanner scanCharactersFromSet:set intoString:&buffer]) {
[strippedString appendString:buffer];
} else {
[scanner setScanLocation:([scanner scanLocation] + 1)];
[strippedString appendString:string];
}
}
return [NSString stringWithString:strippedString];
}
#end
Usage:
NSString *strippedString =
[originalString stringByReplacingCharactersNotInSet:
[NSCharacterSet setWithCharactersInString:#"01234567890"
with:#""];
Swift 3
let notNumberCharacters = NSCharacterSet.decimalDigits.inverted
let intString = yourString.trimmingCharacters(in: notNumberCharacters)
swift 4.1
var str = "75003 Paris, France"
var stringWithoutDigit = (str.components(separatedBy:CharacterSet.decimalDigits)).joined(separator: "")
print(stringWithoutDigit)
Um. The first answer seems totally wrong to me. NSScanner is really meant for parsing. Unlike regex, it has you parsing the string one tiny chunk at a time. You initialize it with a string, and it maintains an index of how far along the string it's gotten; That index is always its reference point, and any commands you give it are relative to that point. You tell it, "ok, give me the next chunk of characters in this set" or "give me the integer you find in the string", and those start at the current index, and move forward until they find something that doesn't match. If the very first character already doesn't match, then the method returns NO, and the index doesn't increment.
The code in the first example is scanning "(123)456-7890" for decimal characters, which already fails from the very first character, so the call to scanCharactersFromSet:intoString: leaves the passed-in strippedString alone, and returns NO; The code totally ignores checking the return value, leaving the strippedString unassigned. Even if the first character were a digit, that code would fail, since it would only return the digits it finds up until the first dash or paren or whatever.
If you really wanted to use NSScanner, you could put something like that in a loop, and keep checking for a NO return value, and if you get that you can increment the scanLocation and scan again; and you also have to check isAtEnd, and yada yada yada. In short, wrong tool for the job. Michael's solution is better.
For those searching for phone extraction, you can extract the phone numbers from a text using NSDataDetector, for example:
NSString *userBody = #"This is a text with 30612312232 my phone";
if (userBody != nil) {
NSError *error = NULL;
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypePhoneNumber error:&error];
NSArray *matches = [detector matchesInString:userBody options:0 range:NSMakeRange(0, [userBody length])];
if (matches != nil) {
for (NSTextCheckingResult *match in matches) {
if ([match resultType] == NSTextCheckingTypePhoneNumber) {
DbgLog(#"Found phone number %#", [match phoneNumber]);
}
}
}
}
`
I created a category on NSString to simplify this common operation.
NSString+AllowCharactersInSet.h
#interface NSString (AllowCharactersInSet)
- (NSString *)stringByAllowingOnlyCharactersInSet:(NSCharacterSet *)characterSet;
#end
NSString+AllowCharactersInSet.m
#implementation NSString (AllowCharactersInSet)
- (NSString *)stringByAllowingOnlyCharactersInSet:(NSCharacterSet *)characterSet {
NSMutableString *strippedString = [NSMutableString
stringWithCapacity:self.length];
NSScanner *scanner = [NSScanner scannerWithString:self];
while (!scanner.isAtEnd) {
NSString *buffer = nil;
if ([scanner scanCharactersFromSet:characterSet intoString:&buffer]) {
[strippedString appendString:buffer];
} else {
scanner.scanLocation = scanner.scanLocation + 1;
}
}
return strippedString;
}
#end
I think currently best way is:
phoneNumber.replacingOccurrences(of: "\\D",
with: "",
options: String.CompareOptions.regularExpression)
If you're just looking to grab the numbers from the string, you could certainly use regular expressions to parse them out. For doing regex in Objective-C, check out RegexKit. Edit: As #Nathan points out, using NSScanner is a much simpler way to parse all numbers from a string. I totally wasn't aware of that option, so props to him for suggesting it. (I don't even like using regex myself, so I prefer approaches that don't require them.)
If you want to format phone numbers for display, it's worth taking a look at NSNumberFormatter. I suggest you read through this related SO question for tips on doing so. Remember that phone numbers are formatted differently depending on location and/or locale.
Swift 5
let newString = origString.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "")
Based on Jon Vogel's answer here it is as a Swift String extension along with some basic tests.
import Foundation
extension String {
func stringByRemovingNonNumericCharacters() -> String {
return self.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet).joinWithSeparator("")
}
}
And some tests proving at least basic functionality:
import XCTest
class StringExtensionTests: XCTestCase {
func testStringByRemovingNonNumericCharacters() {
let baseString = "123"
var testString = baseString
var newString = testString.stringByRemovingNonNumericCharacters()
XCTAssertTrue(newString == testString)
testString = "a123b"
newString = testString.stringByRemovingNonNumericCharacters()
XCTAssertTrue(newString == baseString)
testString = "a=1-2_3#b"
newString = testString.stringByRemovingNonNumericCharacters()
XCTAssertTrue(newString == baseString)
testString = "(999) 999-9999"
newString = testString.stringByRemovingNonNumericCharacters()
XCTAssertTrue(newString.characters.count == 10)
XCTAssertTrue(newString == "9999999999")
testString = "abc"
newString = testString.stringByRemovingNonNumericCharacters()
XCTAssertTrue(newString == "")
}
}
This answers the OP's question but it could be easily modified to leave in phone number related characters like ",;*#+"
NSString *originalPhoneNumber = #"(123) 123-456 abc";
NSCharacterSet *numbers = [[NSCharacterSet characterSetWithCharactersInString:#"0123456789"] invertedSet];
NSString *trimmedPhoneNumber = [originalPhoneNumber stringByTrimmingCharactersInSet:numbers];
];
Keep it simple!

Getting more than char from string variable

I have an NSString *fileName
This will contain a variable number from 1 to 3 digits. I want to extract all of the digits
I can get the first digit using
//create text for appliance identifier
char obsNumber = [fileName characterAtIndex:3];//get 4 character
NSLog(#"Obs number %c",obsNumber);
//Text label
[cell.titleLabel setText:[NSString stringWithFormat:#"Item No: %c",obsNumber]];
NSLog(#"Label for observation = %#",cell.titleLabel.text);
However if the string contains the number for example 78, or 204 I want to catch all two or three digits.
I tried this
//create text for appliance identifier
char obsNumber1 = [fileName characterAtIndex:3];//get 4 character
char obsNumber2 = [fileName characterAtIndex:4];//get 5 character
char obsNumber3 = [fileName characterAtIndex:5];//get 6 character
NSLog(#"Obs number %c,%c,%c",obsNumber1,obsNumber2,obsNumber3);
//Text label
[cell.titleLabel setText:[NSString stringWithFormat:#"Item No: %c,%c,%c",obsNumber1,obsNumber2,obsNumber3]];
NSLog(#"Label for observation = %#",cell.titleLabel.text);
This gave me 18c 1ce etc
Would this work for you?
NSString *filename = #"obs127observation"; //An example variable with your format
This code could be tidier but you should get the idea:
NSString *filenameNumber = [[filename
stringByReplacingOccurrencesOfString:#"observation"
withString:#""]
stringByReplacingOccurrencesOfString:#"obs"
withString:#""];
you can trim other letters except the decimals.
NSString *onlyNumbers=[yourstring stringByTrimmingCharactersInSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]];
As your comment says , It has predefined set of values, right. Then try like this
NSstring *str = [filename substringFromIndex:11];
// Convert the str to char[]
Then you should try with the NSScanner :
NSString *numberString;
NSScanner *scanner = [NSScanner scannerWithString:filename];
NSCharacterSet *numbers = [NSCharacterSet characterSetWithCharactersInString:#"0123456789"];
// Throw away characters before the first number.
[scanner scanUpToCharactersFromSet:numbers intoString:NULL];
// Collect numbers.
[scanner scanCharactersFromSet:numbers intoString:&numberString];
// Result.
int number = [numberString integerValue];
// you can play around with the set of number
Your approach of separating one character at a time and then combining them back into a string is an awkward, overly complex way of going about this. Kumar's suggestion of using NSScanner is a good option if you have a number in the middle of a string.
However, you make it sound like your string will always contain a number and only a number. Is that true? Or will there be characters you need to ignore?
You need to define the problem clearly and completely before you can select the best solution.
It might be as simple as using the NSString method substringWithRange.

Get the unique characters in an NSString

How can I get the unique characters in an NSString?
What I'm trying to do is get all the illegal characters in an NSString so that I can prompt the user which ones were inputted and therefore need to be removed. I start off by defining an NSCharacterSet of legal characters, separate them with every occurrence of a legal character, and join what's left (only illegal ones) into a new NSString. I'm now planning to get the unique characters of the new NSString (as an array, hopefully), but I couldn't find a reference anywhere.
NSCharacterSet *legalCharacterSet = [NSCharacterSet
characterSetWithCharactersInString:#"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ0123456789-()&+:;,'.# "];
NSString *illegalCharactersInTitle = [[self.titleTextField.text.noWhitespace
componentsSeparatedByCharactersInSet:legalCharacterSet]
componentsJoinedByString:#""];
That should help you. I couldn't find any ready to use function for that.
NSMutableSet *uniqueCharacters = [NSMutableSet set];
NSMutableString *uniqueString = [NSMutableString string];
[illegalCharactersInTitle enumerateSubstringsInRange:NSMakeRange(0, illegalCharactersInTitle.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
if (![uniqueCharacters containsObject:substring]) {
[uniqueCharacters addObject:substring];
[uniqueString appendString:substring];
}
}];
Try with the following adaptation of your code:
// legal set
NSCharacterSet *legalCharacterSet = [NSCharacterSet
characterSetWithCharactersInString:#"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ0123456789-()&+:;,'.# "];
// test strings
NSString *myString = #"LegalStrin()";
//NSString *myString = #"francesco#gmail.com"; illegal string
NSMutableCharacterSet *stringSet = [NSCharacterSet characterSetWithCharactersInString:myString];
// inverts the set
NSCharacterSet *illegalCharacterSet = [legalCharacterSet invertedSet];
// intersection of the string set and the illegal set that modifies the mutable stringset itself
[stringSet formIntersectionWithCharacterSet:illegalCharacterSet];
// prints out the illegal characters with the convenience method
NSLog(#"IllegalStringSet: %#", [self stringForCharacterSet:stringSet]);
I adapted the method to print from another stackoverflow question:
- (NSString*)stringForCharacterSet:(NSCharacterSet*)characterSet
{
NSMutableString *toReturn = [#"" mutableCopy];
unichar unicharBuffer[20];
int index = 0;
for (unichar uc = 0; uc < (0xFFFF); uc ++)
{
if ([characterSet characterIsMember:uc])
{
unicharBuffer[index] = uc;
index ++;
if (index == 20)
{
NSString * characters = [NSString stringWithCharacters:unicharBuffer length:index];
[toReturn appendString:characters];
index = 0;
}
}
}
if (index != 0)
{
NSString * characters = [NSString stringWithCharacters:unicharBuffer length:index];
[toReturn appendString:characters];
}
return toReturn;
}
First of all, you have to be careful about what you consider characters. The API of NSString uses the word characters when talking about what Unicode refers to as UTF-16 code units, but dealing with code units in isolation will not give you what users think of as characters. For example, there are combining characters that compose with the previous character to produce a different glyph. Also, there are surrogate pairs, which only make sense when, um, paired.
As a result, you will actually need to collect substrings which contain what the user thinks of as characters.
I was about to write code very similar to Grzegorz Krukowski's answer. He beat me to it, so I won't but I will add that your code to filter out the legal characters is broken because of the reasons I cite above. For example, if the text contains "é" and it's decomposed as "e" plus a combining acute accent, your code will strip the "e", leaving a dangling combining acute accent. I believe your intent is to treat the "é" as illegal.

NSScanner based on last occurrence of substring "#"

I have a user name system similar to twitters using the # symbol.
I have it set up to message users by typing #username. This is basically identical to mentions in Twitter.
So far I have been able to detect the text following the # symbol and to stop detecting when it hits white space. This has been done perfectly with NSScanner.
My problem is if I want to message multiple #usernames, the NSScanner will only detect the first occurrence. I need it to detect the latest occurrence. I have found this as a useful tool:
NSRange lastSymbol = [text rangeOfString:#"#" options:NSBackwardsSearch];
I can't seem to figure out how to properly use this in conjunction with NSScanner. Here are 2 examples of my code attempts.
Example 1:
NSString *user = nil;
NSString *text = textView.text;
NSRange lastSymbol = [text rangeOfString:#"#" options:NSBackwardsSearch];
if (lastSymbol.location != NSNotFound) {
NSScanner *userScanner = [NSScanner scannerWithString:text];
[userScanner scanUpToString:#"#" intoString:nil];
NSCharacterSet *charset = [NSCharacterSet characterSetWithCharactersInString:#"#"];
if (![userScanner isAtEnd]) {
[userScanner scanUpToCharactersFromSet:charset intoString:nil];
[userScanner scanCharactersFromSet:charset intoString:nil]
[userScanner scanUpToCharactersFromSet:charset intoString:&user];
}
}
if (user.length > 0) {
NSRange whiteSpaceRange = [user rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]];
if (whiteSpaceRange.location != NSNotFound) {
// White space, username has ended
} else {
// Still getting username
NSLog(#"%#", user);
}
}
As you can see, I am just not quite sure how to make these work together to produce the results I am looking for. Any ideas? Thanks!
**************************UPDATE****************************
To be more specific, when a user types the # symbol and starts typing a username, I want to display a list of their friends based on what they type. So naturally, I want to pull the string following the # symbol and as the user keeps typing, keep updating the list of their friends. Once the user types a space, we assume they are done typing the user name (usernames cannot contain spaces) so I then stop displaying the table view.
The above code so far is simply detecting and pulling the string following the # symbol. Here is the output right now:
Textfield Text: Hello #username
This works perfectly, as you can see I can keep checking this text against an array of friends.
Let's say the user wants to message another user in the same text like so:
Hello #username, this is my friend #goodfriend
This time it still displays the first #username, but ignore the second #goodfriend.
I figure the best way to do this is somehow tell the NSScanner to reference the LAST occurrence of the # symbol, because what I have now is only referencing the first # symbol.
This might also have to do with the way I am detecting the white space to know the user is done typing the username, and does not need any more suggestions.
Hope this helps, thanks!
For working with the live input like this, I would recommend using a completely different approach. Instead of looking for the last "#", look at just the last word, and see if it starts with "#".
NSString *user = nil;
NSString *text = textView.text;
// Find the last whitespace character
NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet];
NSRange lastWordRange = [text rangeOfCharacterFromSet:whitespace options:NSBackwardsSearch];
// If found, get the index after it. Otherwise, the entire string is one word.
if(lastWordRange.location != NSNotFound)
lastWordRange.location += lastWordRange.length;
else lastWordRange.location = 0;
// If the index is not at the end, look for a '#' right after it
if(lastWordRange.location < [text length] &&
[text characterAtIndex:lastWordRange.location] == '#') {
// Found a '#', use the rest of the string as the username
user = [text substringFromIndex:lastWordRange.location+1];
}
if(user) NSLog(#"%#", user); // The user is still typing a name
If you really want to use a scanner, you can use the same substringFromIndex: method to get a substring to scan.
NSRange lastSymbol = [text rangeOfString:#"#" options:NSBackwardsSearch];
if (lastSymbol.location != NSNotFound) {
NSString *substring = [text substringFromIndex:lastSymbol.location];
NSScanner *userScanner = [NSScanner scannerWithString:substring];
...
So here is the solution I came up with. #ugjoavgfhw has a method that also works with the limited testing I have done.
Here is my solution:
NSString *user = nil;
NSString *text = textView.text;
NSRange lastSymbol = [text rangeOfString:#"#" options:NSBackwardsSearch];
NSScanner *userScanner = [NSScanner scannerWithString:text];
if (lastSymbol.location < 140) {
[userScanner setScanLocation:lastSymbol.location];
}
[userScanner scanUpToString:#"#" intoString:nil];
NSCharacterSet *charset = [NSCharacterSet characterSetWithCharactersInString:#"#"];
if (![userScanner isAtEnd]) {
if (lastSymbol.location != NSNotFound) {
[userScanner scanUpToCharactersFromSet:charset intoString:nil];
[userScanner scanCharactersFromSet:charset intoString:nil];
[userScanner scanUpToCharactersFromSet:charset intoString:&user];
}
}
if (user.length > 0) {
NSRange whiteSpaceRange = [user rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]];
if (whiteSpaceRange.location != NSNotFound) {
// White space, username has ended
} else {
// Still getting username
NSLog(#"%#", user);
}
}
Just to be clear, this code is called in the delegate method:
- (void)textViewDidChange:(UITextView *)textView
Every time the text updates, we perform a backward search looking for the # symbol. This is assigned to an NSRange.
Then the NSScanner is created. Afterwards we check the lastSymbol.location to make sure it is somewhere below 140 characters (the format is that same as twitter with limited characters).
If it is, then we assign the scanning location to the NSScanner.
The rest works as expected. Hope this helps someone else!

Resources