Iterating format % placeholders one by one - ios

I've got an NSAttributedString that looks like
"Somestring bold %# template %f %d blah blah blah"
I want to be able to replace the format template parts just like in [NSString stringWithFormat:...] but keeping the styling, so that the replaced strings match the styling around them (all bold in my example above).
Is there a way to iterate through each format % placeholder one by one so I can use a list of arguments to fill the string?
I don't want to build my own % implementation because I know theres a million and one different formats.
Or is there an easier solution that I'm overlooking?
Edit:
I'll explain a bit of the complete solution I'm solving:
To make it possible for my team to attribute localized strings I've already got a way to write
"key"="test //italic %#// **bold** __underline %d__";
If a specifier is between attributed tags I want that part to be attributed. Currently I can create an attributed string as seen above, the next part is to take care of the specifiers left over.
I'm doing it in the order of Parse attributes -> Apply arguments..., I could easily solve it the other way but I don't want format arguments to mess with the attributes

Big thanks to #Rick for pointing me in the right direction.
His post recommends first going over the arguments and pre-escaping any string or character objects before applying the format strings. This brought me back to another problem I was having earlier, in trying to iterate an argument list of different types (NSString, int etc), like NSLog does. I think I found that it is impossible (or at least really difficult) to do this, and the reason NSLog can is that it knows what types to expect through the format specifiers (%#, %i etc).
I realize I can actually get the same effect not by escaping the arguments, but by escaping the format string itself.
Example:
format: "test //italic %#//"
args: "text // more text"
Steps:
First replace all instances of // with //-TAG-//
Apply the arguments
Determine where styling applies between //-TAG-//
Obviously //-TAG-// can still be written in the arguments to mess up the styling, however depending on what you use as a replacement, the chances of this happening are essentially zero.

I'm doing it in the order of Parse attributes -> Apply arguments..., I
could easily solve it the other way but I don't want format arguments
to mess with the attributes
Why not simply add an escape-character? From what I understand you run the risk of the example you provided getting messed up if the first string contains a double slash?
"key"="test //italic %#// **bold** __underline %d__";
if %# is text // more text that would screw up the formatting.
If so, then simply parse every vararg of the type NSString and char to make sure that they don't contain any of the characters you reserved for your attributes. If they do, add some escape char before which you remove upon parsing the attributes.
The above example would look like this after applying the arguments:
"key"="test //italic text \/\/ more text// **bold** __underline 34__";
After which you parse the attributes, same way as before but you ignore characters preceded by \ and make sure to remove the \.
It's a bit of effort but I bet it's a lot less than implementing your own printf-style parser.

Here is working code:
#import <Foundation/Foundation.h>
#interface NSAttributedString (AttributedFormat)
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ...;
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments;
#end
#implementation NSAttributedString (AttributedFormat)
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ... {
va_list args;
va_start(args, attrFormat);
self = [self initWithFormat:attrFormat arguments:args];
va_end(args);
return self;
}
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments {
NSRegularExpression *regex;
regex = [[NSRegularExpression alloc] initWithPattern: #"(%.*?[#%dDuUxXoOfeEgGccsSpaAF])"
options: 0
error: nil];
NSString *format = attrFormat.string;
format = [regex stringByReplacingMatchesInString: format
options: 0
range: NSMakeRange(0, format.length)
withTemplate: #"\0$1\0"];
NSString *result = [[NSString alloc] initWithFormat:format arguments:arguments];
NSMutableArray *f_comps = [format componentsSeparatedByString:#"\0"].mutableCopy;
NSMutableArray *r_comps = [result componentsSeparatedByString:#"\0"].mutableCopy;
NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init];
__block int consumed_length = 0;
[attrFormat enumerateAttributesInRange:NSMakeRange(0, attrFormat.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
NSMutableString *substr = [NSMutableString string];
while(f_comps.count > 0 && NSMaxRange(range) >= consumed_length + [(NSString *)f_comps[0] length]){
NSString *f_str = f_comps[0];
NSString *r_str = r_comps[0];
[substr appendString:r_str];
[f_comps removeObjectAtIndex:0];
[r_comps removeObjectAtIndex:0];
consumed_length += f_str.length;
}
NSUInteger idx = NSMaxRange(range) - consumed_length;
if(f_comps.count > 0 && idx > 0) {
NSString *f_str = f_comps[0];
NSString *leading = [f_str substringToIndex:idx];
[substr appendString:leading];
NSString *trailing = [f_str substringFromIndex:idx];
[f_comps replaceObjectAtIndex:0 withObject:trailing];
[r_comps replaceObjectAtIndex:0 withObject:trailing];
consumed_length += idx;
}
[output appendAttributedString:[[NSAttributedString alloc] initWithString:substr attributes:attrs]];
}];
return [self initWithAttributedString:output];
}
#end
Usage example:
NSMutableAttributedString *fmt = [[NSMutableAttributedString alloc] initWithString:#"test: "];
[fmt appendAttributedString: [[NSAttributedString alloc] initWithString: #"Some%%string"
attributes: #{
NSFontAttributeName: [UIFont systemFontOfSize:17]
}]];
[fmt appendAttributedString: [[NSAttributedString alloc] initWithString: #"bold %# template %.3f %d"
attributes: #{
NSFontAttributeName: [UIFont boldSystemFontOfSize:20],
NSForegroundColorAttributeName: [UIColor redColor]
}]];
[fmt appendAttributedString: [[NSAttributedString alloc] initWithString: #"%# blah blah blah"
attributes: #{
NSFontAttributeName: [UIFont systemFontOfSize:16],
NSForegroundColorAttributeName: [UIColor blueColor]
}]];
NSAttributedString *result = [[NSAttributedString alloc] initWithFormat:fmt, #"[foo]", 1.23, 56, #"[[bar]]"];
Result:
Maybe this still have some bugs, but it should work in most cases.
(%.*?[#%dDuUxXoOfeEgGccsSpaAF])
This regex matches "Format Specifiers". specifier begin with % and end with listed characters. and may have some modifiers between them. It's not perfect, for example this illegal format "%__s" should be ignored but my regex matches this whole string. but as long as the specifier is legal one, it should works.
My code matches it, and insert delimiters around the specifiers:
I'm %s.
I'm <delimiter>%s<delimiter>.
I use \0 as a delimiter.
I'm \0%s\0.
then interpolate it.
I'm \0rintaro\0.
then split the format and the result with the delimiter:
f_comps: ["I'm ", "%s", "."]
r_comps: ["I'm ", "rintaro", "."]
Here, total string length of f_comps is exact the same as original attributed format. Then, iterate the attributes with enumerateAttributesInRange, we can apply the attributes to the results.
I'm sorry but it's too hard to explain the jobs inside enumerateAttributesInRange, with my poor English skills :)

Related

How to detect emoji and change font size

I have text which contains emoji in it, we are able to display it correctly by doing encoding and decoding the string, what I need to achieve is to increase the font size of only emoji in the text like in image below,
I have got an idea to determine the range of all emoji, and supply in NSAttributedString with increased font size. Now am out of idea how can I detect range of emojis in a given string?
Thanks
I have done the same like
let string = "This is emoji Test"
let attributedEmoji = NSMutableAttributedString(string: " \u{1F600}", attributes: [NSFontAttributeName:UIFont.systemFontOfSize(60)])
let attribString = NSMutableAttributedString.init(string: string)
attribString.appendAttributedString(attributedEmoji)
lblEmoji.attributedText = attribString
You can change the font and font size to scale the emoji.
Put all possible Emoji's(Your application uses) into an array.
Search for emoji into string from array.If found apply attributed Emoji.
Write a method that accept emoji code and return attributed emoji text.
Hope this info will help you in better way.
https://github.com/woxtu/NSString-RemoveEmoji
Find out if Character in String is emoji?
you can use it directly like below or
if ([myString containsString:#"😋"])
{
NSLog(#"one");
//change the font size here.
}
else
{
NSLog(#"fk");
//change the font size here.
}
or you can use
[mystring is isEqualToString:"I believe 😋"];
try those. hope this will help to you.
I have made one demo, You can detect emoji from the string like below,
NSString *str = #"this is 😄 and test 😊";
NSArray *arr = [str componentsSeparatedByString:#" "];
for (int i = 0; i < arr.count; i++) {
NSString *temp = [arr objectAtIndex:i];
if ( ![temp canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSLog(#"%d",i);
NSLog(#"%#",temp); // temp is emoji. You can detect emoji here from your string now you can manage as per your need
}
}
Thanks to all who answered, but none was complete answer though #Raj's suggestion to look NSString-RemoveEmoji helped me to achieve the solution for this, here it is, it works for any kind of emoji
-(NSMutableAttributedString *)getAttributedEmojiString:(NSString *)inputString{
NSMutableArray *__block emojiRange=[[NSMutableArray alloc] init];
[inputString enumerateSubstringsInRange:NSMakeRange(0, [inputString length])
options:NSStringEnumerationByComposedCharacterSequences
usingBlock: ^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop) {
if([substring isEmoji]){
[emojiRange addObject:#{#"startrange":#(substringRange.location),#"endrange":#(enclosingRange.length)}];
}
}];
NSMutableAttributedString *mutString=[[NSMutableAttributedString alloc] initWithString:inputString];
[mutString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16.0] range:NSMakeRange(0, mutString.length)];
[emojiRange enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[mutString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:35.0] range:NSMakeRange([obj[#"startrange"] floatValue], [obj[#"endrange"] floatValue])];
}];
return mutString;
}
Description
First find NSRange of all the emoji in the string by using NSString-RemoveEmoji function isEmoji, and store in array.
Supply the fetched range to apply bigger FONT SIZE to characters in the range.
Finally assign the generated attributed text to the label.
self.label.attributedText=[self getAttributedEmojiString:EmojiDecoded(originalText)];
I use two macros to Encode and Decode Emoji's since I need to save these values to server and read through api, below are the macros.
#define Encoded(val) [[val dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0]
#define Decoded(val) [[NSString alloc] initWithData:[[NSData alloc] initWithBase64EncodedString:val options:0] encoding:NSUTF8StringEncoding]
#define EmojiEncoded(val) [[NSString alloc] initWithData:[val dataUsingEncoding:NSNonLossyASCIIStringEncoding] encoding:NSUTF8StringEncoding]
#define EmojiDecoded(val) [[NSString alloc] initWithData:[val dataUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding]
Hope it helps anyone who is looking for similar solution.
Cheers, and thanks to all.
This is somewhat late but might be useful for other folks who stumble upon this answer. The secret is to ask Core Text and it knows which characters in the NSAttributedString are emoji characters.
// Build the attributed string as needed
let ns = NSAttributedString(string: s)
// Now create a typesetter and render the line
let typesetter = CTTypesetterCreateWithAttributedString(nsa)
let line = CTTypesetterCreateLine(typesetter, CFRangeMake(0, nsa.length))
// Once you have a line you can enumerate the runs
guard let runs = CTLineGetGlyphRuns(line) as? [CTRun] else {
throw NSError(domain: "CoreText", code: -1, userInfo: nil)
}
// Each run will have a font with specific attributes
print("There are \(runs.count) run(s) in \(ns.string)")
print()
for run in runs {
let charRange = CTRunGetStringRange(run)
let x: NSAttributedString = CFAttributedStringCreateWithSubstring(nil, nsa, charRange)
print(" Chars: '\(x.string)'")
let attributes: NSDictionary = CTRunGetAttributes(run)
let font = attributes["NSFont"] as! CTFont
let traits = CTFontGetSymbolicTraits(font)
print(" Emoji == \(traits.contains(.traitColorGlyphs))")
print()
}

How to find and apply two fonts in a label in IOS

I know to apply two fonts in a label for single word with one font and rest with another font using below code..
int lengthofname#"Anand";
UIFont *boldFont = [UIFont fontWithName:#"BodoniStd" size:15];
UIFont *regularFont = [UIFont fontWithName:#"BodoniStd-BoldItalic" size:15];
//UIColor *foregroundColor = [UIColor clearColor];
// Create the attributes
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
boldFont, NSFontAttributeName,nil];
//NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
// boldFont, NSFontAttributeName,
// foregroundColor, NSForegroundColorAttributeName, nil];
NSDictionary *subAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
regularFont, NSFontAttributeName, nil];
const NSRange range = NSMakeRange(0,lengthofname);
// range of " 2012/10/14 ". Ideally this should not be hardcoded const
// Create the attributed string (text + attributes)
NSMutableAttributedString *attributedText =
[[NSMutableAttributedString alloc] initWithString:#"Anand is good programmer"
attributes:attrs];
[attributedText setAttributes:subAttrs range:range];
// Set it in our UILabel and we are done!
[self.descLabel setAttributedText:attributedText];
My requirement is to find some x words in a label and then apply Bold font and rest of text with Regular font..
Please suggest any ideas. Thanks in Advance..!
In this example there highlighted words are not funded with regular expression but I believe there is some list (NSArray: STANDARD, EXPRESS, NEXT DAY, etc) of keywords. And what you have to do is enumerate that array to find the rang in text and if founded apply the different font style, something like that:
for (NSString * keyword in listOfKeywordArray) {
NSRange range = [longTextString rangeOfString:#"keyword"];
if (range.location != NSNotFound) {
//Keyword found, apply different font
// This of course needs to be changed to apply font you want
[attrString addAttribute:NSFontAttributeName value:fontName range:range];
}
}
Greg's answer is pretty close. If you have an array of keywords, you can use for.. in to loop through the array of keywords. Then you'd need to use an inner loop with rangeOfString:options:range to find all occurrences of a given keyword.
That method returns an NSRange. You could use setAttributes:range: on the range to set the text attributes of each occurrence of each keyword to use the font and style you want. (Greg's code using addAttribute only lets you set a single attribute on the text. setAttributes:range: and addAttributes:range: both let you specify a whole dictionary of attributes, like in the code you posted. Your code might look like this:
//Whatever font attributes you want to use
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
boldFont, NSFontAttributeName,nil];
NSArray *keywords = [#"STANDARD", #"EXPRESS", #"NEXT DAY"];
//Loop through each keyword in the array of keywords.
for (NSString aKeyword in keywords)
{
//Set the range to the whole string to start with.
NSRange range = NSMakeRange(0, longTextString.length];
//While there are still more occurrences of this keyword
//Not that the code both assigns the result to range, and evaluates the location.
//This is a bit of C language sleight-of-hand. It works, but is odd-looking code.
while ((range = [longTextString
rangeOfString: aKeyword
options: NSLiteralSearch
range: range]).location != NSNotFound)
{
//Set the attributes on this occurrence of this keyword.
[longTextString setAttributes: attrs range: range];
range.location = range.location+range.length;
range.length = longTextString - range.location;
}
Disclaimer: I typed out the code above in an editor window. It may not compile, much less run. It's intended for illustration purposes only. You will need to clean it up and adapt it to your needs.
Also note that rangeOfString:options:range is not a great choice for this problem, since it will detect word fragments in the middle of longer words. For example, if one of your keywords is "The" then it would detect "The" in the first 3 characters of "Them" and "These". It should really be rewritten to use regular expression string matching that requires a string to be a whole word.
Edit #2. I decided to actually code this up. The rangeOfString:options:range: approach is unsatisfactory because, as mentioned above, it detects word fragments inside larger words. Regular Expressions are a much better solution. Here is code that marks all occurrences of an array of words as bold:
NSMutableAttributedString *viewStyledText = [theTextView.attributedText mutableCopy];
NSString *viewText = viewStyledText.string;
if (viewText.length == 0)
return;
BOOL changed = NO;
NSArray *wordsToBold = #[#"The", #"for", #"to", #"league"];
UIFont *boldFont = [UIFont boldSystemFontOfSize: 15];
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
boldFont, NSFontAttributeName,nil];
//Loop through each keyword in the array of keywords.
for (NSString *wordToBold in wordsToBold)
{
//Set the range to the whole string to start with.
NSRange range = NSMakeRange(0, viewText.length);
/*
Create a regular expression string for the current word.
The "(?i) prefix tells the regular expression to be case-insenstive. Remove it if
you want your search to be case-sensitive.
The "\b" bits (with double backslashes so the output contains a backslash) cause
The regular expression to only match whole words.
*/
NSString *wordRegExString = [NSString stringWithFormat: #"(?i)\\b%#\\b", wordToBold];
//Now create a regular expression object using the current regular expression string.
NSRegularExpression *wordRegEx = [NSRegularExpression
regularExpressionWithPattern: wordRegExString
options: 0
error: nil];
//While there are still more occurrences of this keyword...
//Not that the code both assigns the result to range, and evaluates the location.
//This is a bit of C language sleight-of-hand. It works, but is odd-looking code.
while ((range = [wordRegEx rangeOfFirstMatchInString: viewText
options: 0
range: range]).location != NSNotFound)
{
//Set the attributes on this occurrence of this keyword.
changed = YES;
[viewStyledText setAttributes: attrs range: range];
range.location = range.location+range.length;
range.length = viewStyledText.length - range.location;
}
}
if (changed)
theTextView.attributedText = viewStyledText;

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.

Search for any characters in an NSString

I have the following code to search an NSString:
for (NSDictionary *obj in data) {
NSString *objQuestion = [obj objectForKey:#"Question"];
NSRange dataRange = [objQuestion rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (dataRange.location != NSNotFound) {
[filteredData addObject:obj];
}
}
This works fine, but there is a problem. If objQuestion is: "Green Yellow Red" and I search for "Yellow Green Red", the object will not show up as my search is not in the correct order.
How would I change my code so that no matter what order I search the words in, the object will show?
You should be breaking your search text into words and search each word.
NSArray *wordArray= [searchText componentsSeparatedByString: #" "];
for (NSDictionary *obj in data) {
NSString *objQuestion = [obj objectForKey:#"Question"];
BOOL present = NO;
for (NSString *s in wordArray) {
if (s) {
NSRange dataRange = [objQuestion rangeOfString:s options:NSCaseInsensitiveSearch];
if (dataRange.location != NSNotFound) {
present = YES;
}
}
}
if (present) {
[filteredData addObject:obj];
}
}
So you want to basically do a keyword search? I would recommend doing a regular expression search. where the words can be in any order.
Something like this.
(your|test|data)? *(your|test|data)? *(your|test|data)?
Which you can use in a NSRegularExpressoin
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"(your|test|data)? *(your|test|data)? *(your|test|data)?" options:NSRegularExpressionCaseInsensitive error:&error];
int numMatches = [regex numberOfMatchesInString:searchString options:0 range:NSMakeRange(0, [searchString length])];];
This will match any ordering in an efficient manner.
Not sure if regex is okay for Obj C, because I do not have a mac in front of me right now, but it should be okay.
You might want to consider that the search input string is not always as clean as you expect, and could contain punctuation, brackets, etc.
You'd also want to be lax with accents.
I like to use regular expressions for this sort of problem, and since you are looking for a solution that allows arbitrary ordering of the search terms, we'd need to re-work the search string. We can use regular expressions for that, too - so the pattern is constructed by a regex substitution, just out of principle. You may want to document it thoroughly.
So here is a code snippet that will do these things:
// Use the Posix locale as the lowest common denominator of locales to
// remove accents.
NSLocale *enLoc = [[NSLocale alloc] initWithLocaleIdentifier: #"en_US_POSIX"];
// Mixed bag of genres, but for testing purposes we get all the accents we need
NSString *orgString = #"Beyoncé Motörhead Händel";
// Clean string by removing accents and upper case letters in Posix encoding
NSString *string = [orgString stringByFoldingWithOptions: NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch
locale: enLoc ];
// What the user has typed in, with misplaced umlaut and all
NSString *orgSearchString = #"handel, mötorhead, beyonce";
// Clean the search string, too
NSString *searchString = [orgSearchString stringByFoldingWithOptions: NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch
locale: enLoc ];
// Turn the search string into a regex pattern.
// Create a pattern that looks like: "(?=.*handel)(?=.*motorhead)(?=.*beyonce)"
// This pattern uses positive lookahead to create an AND logic that will
// accept arbitrary ordering of the words in the pattern.
// The \b expression matches a word boundary, so gets rid of punctuation, etc.
// We use a regex to create the regex pattern.
NSString *regexifyPattern = #"(?w)(\\W*)(\\b.+?\\b)(\\W*)";
NSString *pattern = [searchString stringByReplacingOccurrencesOfString: regexifyPattern
withString: #"(?=.*$2)"
options: NSRegularExpressionSearch
range: NSMakeRange(0, searchString.length) ];
NSError *error;
NSRegularExpression *anyOrderRegEx = [NSRegularExpression regularExpressionWithPattern: pattern
options: 0
error: &error];
if ( !anyOrderRegEx ) {
// Regex patterns are tricky, programmatically constructed ones even more.
// So we check if it went well and do something intelligent if it didn't
// ...
}
// Match the constructed pattern with the string
NSUInteger numberOfMatches = [anyOrderRegEx numberOfMatchesInString: string
options: 0
range: NSMakeRange(0, string.length)];
BOOL found = (numberOfMatches > 0);
The use of the Posix locale identifier is discussed in this tech note from Apple.
In theory there is an edge case here if the user enters characters with a special meaning for regexes, but since the first regex removes non-word characters it should be solved that way. A bit of an un-planned positive side effect, so could be worth verifying.
Should you not be interested in a regex-based solution, the code folding may still be useful for "normal" NSString-based searching.

Remove parentheses without regex

I need to turn something like this
NSString *stringWithParentheses = #"This string uses (something special)";
Into this, programmatically.
NSString *normalString = #"This string uses";
The issue is I don't want to use all these weird libraries, regex, etc.
If you change your mind about the regex, here's a short, clean solution:
NSString *foo = #"First part (remove) (me (and ((me)))))) (and me) too))";
NSRegularExpression *expr = [NSRegularExpression regularExpressionWithPattern:#"\\(.*\\)" options:0 error:NULL];
NSString *bar = [expr stringByReplacingMatchesInString:foo options:0 range:NSMakeRange(0, foo.length) withTemplate:#""];
Everything between ( and ) gets removed, including any nested parentheses and unmatched parentheses within parentheses.
Just find the first open parentheses, note its index, find the closing one, note its index, and remove the characters between the indexes (including the indexes themselves).
To find the character use:
[string rangeOfString:#"("];
To remove a range:
[string stringByReplacingCharactersInRange:... withString:#""];
Here is a solution:
NSString* str = #"This string uses (something special)";
NSRange rgMin = [str rangeOfString:#"("];
NSRange rgMax = [str rangeOfString:#")"];
NSRange replaceRange = NSMakeRange(rgMin.location, rgMax.location-rgMin.location+1);
NSString* newString = str;
if (rgMin.location < rgMax.location)
{
newString = [str stringByReplacingCharactersInRange:replaceRange withString:#""];
}
It won't work on nested parentheses. Or multiple parentheses. But it works on your example. This is to be refined to your exact situation.
A way would be to find the position of the first occurrence of the '(' character and the last occurrence of the ')' character, and to build a substring by eliminating all the characters between these ranges. I've made an example:
NSString* str= #"This string uses (something special)";
NSRange r1=[str rangeOfString: #"("];
NSRange r2= [str rangeOfString: #")" options: NSBackwardsSearch];
NSLog(#"%#",[str stringByReplacingCharactersInRange: NSMakeRange(r1.location, r2.location+r2.length-r1.location) withString: #""]);

Resources