How to Get All Nsrange of a Particular Character in a Nsstring

How to get all NSRange of a particular character in a NSString?

I wrote this method for my project - SUITextView with highlight:

- (NSMutableAttributedString*) setColor:(UIColor*)color word:(NSString*)word inText:(NSMutableAttributedString*)mutableAttributedString {

NSUInteger count = 0, length = [mutableAttributedString length];
NSRange range = NSMakeRange(0, length);

while(range.location != NSNotFound)
{
range = [[mutableAttributedString string] rangeOfString:word options:0 range:range];
if(range.location != NSNotFound) {
[mutableAttributedString setTextColor:color range:NSMakeRange(range.location, [word length])];
range = NSMakeRange(range.location + range.length, length - (range.location + range.length));
count++;
}
}

return mutableAttributedString;
}

And in my category of NSMutableAttributedString:

- (void) setTextColor:(UIColor*)color range:(NSRange)range {
// kCTForegroundColorAttributeName
[self removeAttribute:(NSString*)kCTForegroundColorAttributeName range:range]; // Work around for Apple leak
[self addAttribute:(NSString*)kCTForegroundColorAttributeName value:(id)color.CGColor range:range];
}

How to get NSRange(s) for a substring in NSString?

Lots of ways of solving this problem - NSScanner was mentioned; rangeOfString:options:range etc. For completeness' sake, I'll mention NSRegularExpression. This also works:

    NSMutableAttributedString *mutableString = nil;
NSString *sampleText = @"I live in California, blah blah blah California.";
mutableString = [[NSMutableAttributedString alloc] initWithString:sampleText];

NSString *pattern = @"(California)";
NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];

// enumerate matches
NSRange range = NSMakeRange(0,[sampleText length]);
[expression enumerateMatchesInString:sampleText options:0 range:range usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSRange californiaRange = [result rangeAtIndex:0];
[mutableString addAttribute:NSForegroundColorAttributeName value:[NSColor greenColor] range:californiaRange];
}];

How to find Multiple NSRange for a string from full string iOS swift

You say that you want to iterate through NSRange matches in a string so that you can apply a bold attribute to the relevant substrings.

In Swift 5.7 and later, you can use the new Regex:

string.ranges(of: /\d+/)
.map { NSRange($0, in: string) }
.forEach {
attributedString.setAttributes(attributes, range: $0)
}

Or if you find the traditional regular expressions too cryptic, you can import RegexBuilder, and you can use the new regex DSL:

string.ranges(of: Regex { OneOrMore(.digit) })
.map { NSRange($0, in: string) }
.forEach {
attributedString.setAttributes(attributes, range: $0)
}

In Swift versions prior to 5.7, one would use NSRegularExpression. E.g.:

let range = NSRange(location: 0, length: string.count)
try! NSRegularExpression(pattern: "\\d+").enumerateMatches(in: string, range: range) { result, _, _ in
guard let range = result?.range else { return }
attributedString.setAttributes(attributes, range: range)
}

Personally, before Swift 5.7, I found it useful to have a method to return an array of Swift ranges, i.e. [Range<String.Index>]:

extension StringProtocol {
func ranges<T: StringProtocol>(of string: T, options: String.CompareOptions = []) -> [Range<Index>] {
var ranges: [Range<Index>] = []
var start: Index = startIndex

while let range = range(of: string, options: options, range: start ..< endIndex) {
ranges.append(range)

if !range.isEmpty {
start = range.upperBound // if not empty, resume search at upper bound
} else if range.lowerBound < endIndex {
start = index(after: range.lowerBound) // if empty and not at end, resume search at next character
} else {
break // if empty and at end, then quit
}
}

return ranges
}
}

Then you can use it like so:

let string = "Hello world, there are 09 continents and 195 countries."
let ranges = string.ranges(of: "[0-9]+", options: .regularExpression)

And then you can map the Range to NSRange. Going back to the original example, if you wanted to make these numbers bold in some attributed string:

string.ranges(of: "[0-9]+", options: .regularExpression)
.map { NSRange($0, in: string) }
.forEach { attributedString.setAttributes(boldAttributes, range: $0) }

Resources:

  • Swift 5.7 and later:
    • WWDC 2022 video Meet Swift Regex
    • WWDC 2022 video Swift Regex: Beyond the basics
    • Hacking With Swift: Regular Expressions
  • Swift before 5.7:
    • Hacking With Swift: How to use regular expressions in Swift
    • NSHipster: Regular Expressions in Swift

Fastest way to get array of NSRange objects for all uppercase letters in an NSString?

The simplest way is probably to use -rangeOfCharacterFromSet:options:range: with [NSCharacterSet uppercaseLetterCharacterSet]. By modifying the range to search over with each call, you can find all of the uppercase letters pretty easily. Something like the following will work to give you an NSArray of all ranges (encoded as NSValues):

- (NSArray *)rangesOfUppercaseLettersInString:(NSString *)str {
NSCharacterSet *cs = [NSCharacterSet uppercaseLetterCharacterSet];
NSMutableArray *results = [NSMutableArray array];
NSRange searchRange = NSMakeRange(0, [str length]);
NSRange range;
while ((range = [str rangeOfCharacterFromSet:cs options:0 range:searchRange]).location != NSNotFound) {
[results addObject:[NSValue valueWithRange:range]];
searchRange = NSMakeRange(NSMaxRange(range), [str length] - NSMaxRange(range));
}
return results;
}

Note, this will not coalesce adjacent ranges into a single range, but that's easy enough to add.

Here's an alternative solution based on NSScanner:

- (NSArray *)rangesOfUppercaseLettersInString:(NSString *)str {
NSCharacterSet *cs = [NSCharacterSet uppercaseLetterCharacterSet];
NSMutableArray *results = [NSMutableArray array];
NSScanner *scanner = [NSScanner scannerWithString:str];
while (![scanner isAtEnd]) {
[scanner scanUpToCharactersFromSet:cs intoString:NULL]; // skip non-uppercase characters
NSString *temp;
NSUInteger location = [scanner scanLocation];
if ([scanner scanCharactersFromSet:cs intoString:&temp]) {
// found one (or more) uppercase characters
NSRange range = NSMakeRange(location, [temp length]);
[results addObject:[NSValue valueWithRange:range]];
}
}
return results;
}

Unlike the last, this one does coalesce adjacent uppercase characters into a single range.

Edit: If you're looking for absolute speed, this one will likely be the fastest of the 3 presented here, while still preserving correct unicode support (note, I have not tried compiling this):

// returns a pointer to an array of NSRanges, and fills in count with the number of ranges
// the buffer is autoreleased
- (NSRange *)rangesOfUppercaseLettersInString:(NSString *)string count:(NSUInteger *)count {
NSMutableData *data = [NSMutableData data];
NSUInteger numRanges = 0;
NSUInteger length = [string length];
unichar *buffer = malloc(sizeof(unichar) * length);
[string getCharacters:buffer range:NSMakeRange(0, length)];
NSCharacterSet *cs = [NSCharacterSet uppercaseLetterCharacterSet];
NSRange range = {NSNotFound, 0};
for (NSUInteger i = 0; i < length; i++) {
if ([cs characterIsMember:buffer[i]]) {
if (range.location == NSNotFound) {
range = (NSRange){i, 0};
}
range.length++;
} else if (range.location != NSNotFound) {
[data appendBytes:&range length:sizeof(range)];
numRanges++;
range = (NSRange){NSNotFound, 0};
}
}
if (range.location != NSNotFound) {
[data appendBytes:&range length:sizeof(range)];
numRanges++;
}
if (count) *count = numRanges;
return [data bytes];
}

How to find out a specific character is present in a NSString or not?

Try this:

NSRange range = [country rangeOfString:searchtext];
if (range.location != NSNotFound)
{
}

You also have the position (location) and length of your match (uninteresting in this case but might be interesting in others) in your range object. Note that searchtext must not be nil. If you are only interested in matching (and not the location) you can even condense this into

if ([country rangeOfString:searchtext].location != NSNotFound)
{
}

How do I bold multiple instances of a character in an NSString?

As others have mentioned, you need to loop through the string to return multiple ranges.
This would work:

NSString *a = @"* This is a text String \n* Followed by another text String \n* Followed by a third";
NSMutableAttributedString *att = [[NSMutableAttributedString alloc] initWithString:a];
NSRange foundRange = [a rangeOfString:@"*"];

while (foundRange.location != NSNotFound)
{
[att addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:20.0f] range:foundRange];

NSRange rangeToSearch;
rangeToSearch.location = foundRange.location + foundRange.length;
rangeToSearch.length = a.length - rangeToSearch.location;
foundRange = [a rangeOfString:@"*" options:0 range:rangeToSearch];
}

[[self textView] setAttributedText:att];

Finding occurrences of String as NSRange from an NSString using Swift, result NSRange to be used in NSAttributedString

Found the correct way to convert a Range to NSRange, thanks to MartinR for this answer

I was using the wrong way to convert Range to NSRange, here is the working code snippet with proper way to convert from Range to NSRange:

let findStrings = ["#is","#tag","#siØve","#search"]
let findInString = "This #is a #tag #tag inten#siØve#search"
let result = NSMutableAttributedString(string: findInString)
let utf16 = findInString.utf16
for (index, stringToFind) in findStrings.enumerated() {

var nextStartIndex = findInString.startIndex

while let range = findInString.range(of: stringToFind, options: [.literal, .caseInsensitive], range: nextStartIndex..<findInString.endIndex) {

// PROPER WAY TO CONVERT TO NSRange
let from = range.lowerBound.samePosition(in: utf16)
let start = utf16.distance(from: utf16.startIndex, to: from)
let length = utf16.distance(from: from, to: range.upperBound.samePosition(in: utf16))

result.addAttribute(NSLinkAttributeName, value: "\(index):", range: NSMakeRange(start, length))

nextStartIndex = range.upperBound
}

}

Shortcut to generate an NSRange for entire length of NSString?

Function? Category method?

- (NSRange)fullRange
{
return (NSRange){0, [self length]};
}

[myString replaceOccurrencesOfString:@"replace_me"
withString:replacementString
options:NSCaseInsensitiveSearch
range:[myString fullRange]];

NSRange from Swift Range?

Swift String ranges and NSString ranges are not "compatible".
For example, an emoji like counts as one Swift character, but as two NSString
characters (a so-called UTF-16 surrogate pair).

Therefore your suggested solution will produce unexpected results if the string
contains such characters. Example:

let text = "Long paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(text.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
}
})
println(attributedString)

Output:


Long paragra{
}ph say{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}

As you see, "ph say" has been marked with the attribute, not "saying".

Since NS(Mutable)AttributedString ultimately requires an NSString and an NSRange, it is actually
better to convert the given string to NSString first. Then the substringRange
is an NSRange and you don't have to convert the ranges anymore:

let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)

nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
println(attributedString)

Output:


Long paragraph {
}saying{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}!{
}

Update for Swift 2:

let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
(substring, substringRange, _, _) in

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
print(attributedString)

Update for Swift 3:

let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
(substring, substringRange, _, _) in

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
}
})
print(attributedString)

Update for Swift 4:

As of Swift 4 (Xcode 9), the Swift standard library
provides method to convert between Range<String.Index> and NSRange.
Converting to NSString is no longer necessary:

let text = "Long paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
(substring, substringRange, _, _) in
if substring == "saying" {
attributedString.addAttribute(.foregroundColor, value: NSColor.red,
range: NSRange(substringRange, in: text))
}
}
print(attributedString)

Here substringRange is a Range<String.Index>, and that is converted to the
corresponding NSRange with

NSRange(substringRange, in: text)

Find all locations of substring in NSString (not just first)

You can use rangeOfString:options:range: and set the third argument to be beyond the range of the first occurrence. For example, you can do something like this:

NSRange searchRange = NSMakeRange(0,string.length);
NSRange foundRange;
while (searchRange.location < string.length) {
searchRange.length = string.length-searchRange.location;
foundRange = [string rangeOfString:substring options:0 range:searchRange];
if (foundRange.location != NSNotFound) {
// found an occurrence of the substring! do stuff here
searchRange.location = foundRange.location+foundRange.length;
} else {
// no more substring to find
break;
}
}


Related Topics



Leave a reply



Submit