NSRange
NSRange
is one of the essential types of Foundation. Passed around and returned in methods throughout the framework, being well-versed in this struct has a range of benefits, which this week’s article will help you locate.
Ranges are data types used to describe a contiguous interval of integers. They are most commonly used with strings, arrays, and similarly-ordered collections.
For Objective-C programs, the Foundation type NSRange
is used. In other languages, ranges are often encoded as a two-element array, containing the start and end indexes. In Foundation, NSRange
instead encodes a range as struct containing the location and length. By command-clicking (⌘-ʘ
) on the NSRange
symbol in Xcode, we can jump directly to its declaration in Foundation/NSRange.h
:
typedef struct _NSRange {
NSUInteger location;
NSUInteger length;
} NSRange;
In practice, this approach helps mitigate common off-by-one errors when working with ranges. For example, compare the equivalent Javascript and Objective-C code for creating a range of characters for a given string:
range.js
var string = "hello, world";
var range = [0, string.length - 1];
Forgetting to subtract 1
for the end index in Javascript would result in an out-of-bounds error later.
range.m
NSString *string = @"hello, world";
NSRange range = NSMakeRange (0, [string length]);
NSRange
’s approach is clearer and less prone to error—especially when it comes to more complex arithmetic operations on ranges.
Usage
Strings
NSString *string = @"lorem ipsum dolor sit amet";
NSRange range = [string rangeOfString :@"ipsum"];
// {.location=6, .length=5}
NSString *substring = [string substringWithRange :range];
// @"ipsum"
NSString
does not have a method like contains
. Instead, range
can be used to check for an NSNot
location value:
NSString *input = ...;
if ([input rangeOfString :@"keyword"].location != NSNotFound ) {
…
}
Arrays
NSArray *array = @[@"a", @"b", @"c", @"d"];
NSArray *subarray = [array subarrayWithRange :NSMakeRange (1, 2)];
// @[@"b", @"c"]
Index Sets
NSIndexSet is a Foundation collection class that is similar to NSRange
, with the notable exception of being able to support non-contiguous series. An NSIndex
can be created from a range using the index
class constructor:
NSRange range = NSMakeRange (0, 10);
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange :range];
Functions
Because NSRange
is not a class, creating and using instances is done through function calls, rather than, say, instance methods.
Many of the NSRange functions are named counter to the modern conventions of Foundation and CoreFoundation wherein the relevant type of the function immediately follows the two-letter namespace. For example,
NSMake
should instead be namedRange NSRange
, following the example ofMake CGRect
andMake CGSize
, et al. Similarly, a better name forMake NSEqual
would beRanges NSRange
, just likeEqual To Range CGPoint
.Equal To Point Although consistency in itself is likely not sufficient reason to go through the trouble of replacing existing usage, this gist shows how one could make their own code base a little more OCD-friendly.
Creating an NSRange
NSMake
: Creates a new NSRange from the specified values.Range
NSArray *array = @[@1, @2, @3];
NSRange range = NSMakeRange (0, [array count]);
// {.location=0, .length=3}
Querying Information
NSEqual
: Returns a Boolean value that indicates whether two given ranges are equal.Ranges
NSRange range1 = NSMakeRange (0, 6);
NSRange range2 = NSMakeRange (2, 7);
BOOL equal = NSEqualRanges (range1, range2); // NO
NSLocation
: Returns a Boolean value that indicates whether a specified position is in a given range.In Range
NSRange range = NSMakeRange (3, 4);
BOOL contained = NSLocationInRange (5, range); // YES
NSMax
: Returns the sum of the location and length of the range.Range
NSRange range = NSMakeRange (3, 4);
NSUInteger max = NSMaxRange (range); // 7
Set Operations
NSIntersection
: Returns the intersection of the specified ranges. If the returned range’s length field isRange 0
, then the two ranges don’t intersect, and the value of the location field is undefined.
NSRange range1 = NSMakeRange (0, 6);
NSRange range2 = NSMakeRange (2, 7);
NSRange intersectionRange = NSIntersectionRange (range1, range2);
// {.location=2, .length=4}
NSUnion
: Returns the union of the specified ranges. A range covering all indices in and between range1 and range2. If one range is completely contained in the other, the returned range is equal to the larger range.Range
NSRange range1 = NSMakeRange (0, 6);
NSRange range2 = NSMakeRange (2, 7);
NSRange unionRange = NSUnionRange (range1, range2);
// {.location=0, .length=9}
Converting Between NSString * & NSRange
NSString
: Returns a string representation of a range.From Range
NSRange range = NSMakeRange (3, 4);
NSString *string = NSStringFromRange (range); // @"{3,4}"
NSRange
: Returns a range from a textual representation.From String
NSString *string = @"{1,5}";
NSRange range = NSRangeFromString (string);
// {.location=1, .length=5}
If the string passed into NSRange
does not represent a valid range, it will return a range with its location and length set to 0
.
NSString *string = @"invalid";
NSRange range = NSRangeFromString (string);
// {.location=0, .length=0}
While one might be tempted to use NSString
to box NSRange
for inclusion within an NSArray
, NSValue +value
is the way to go:
NSRange range = NSMakeRange (0, 3);
NSValue *value = [NSValue valueWithRange :range];
NSRange
is one of the few cases where some of the underlying implementation of its functions are actually exposed and inlined in the public headers:
Foundation/NSRange.h
NS_INLINE NSRange NSMakeRange (NSUInteger loc, NSUInteger len) {
NSRange r;
r.location = loc;
r.length = len;
return r;
}
NS_INLINE NSUInteger NSMaxRange (NSRange range) {
return (range.location + range.length);
}
NS_INLINE BOOL NSLocationInRange (NSUInteger loc, NSRange range) {
return (!(loc < range.location) && (loc - range.location) < range.length) ? YES : NO;
}
NS_INLINE BOOL NSEqualRanges (NSRange range1, NSRange range2) {
return (range1.location == range2.location && range1.length == range2.length);
}
NSRangePointer
One oddity worth mentioning with NSRange
is the existence of NSRange
. “What the what?”, you might exclaim in panicked confusion. Jumping to the source confirms our darkest fears:
Foundation/NSRange.h
typedef NSRange *NSRangePointer ;
So. Without a definitive origin story, one would have to assume that this type was created by a well-meaning framework engineer who noted the confusion around NSRange
being a struct and not a class. NSRange *
is equivalent to NSRange
, though the latter can be found in out parameters for various methods throughout Foundation. NSAttributed
, for instance, has an NSRange
parameter for returning the effective range of an attribute at a particular index (since the attribute likely starts and ends before outside of the specified index):
NSMutableAttributedString *mutableAttributedString = ...;
NSRange range;
if ([mutableAttributedString attribute:NSUnderlineStyleAttributeName
atIndex: 0
effectiveRange: &range])
{
// Make underlined text blue as well
[mutableAttributedString addAttribute :NSForegroundColorAttributeName
value:[UIColor blueColor ]
range:range];
}
CFRange
One final caveat: Core Foundation also defines a CFRange
type, which differs from NSRange
in using CFIndex
types for its members, and having only the function CFRange
:
typedef struct {
CFIndex location;
CFIndex length;
} CFRange;
Anyone working with CoreText or another low-level C API is likely to encounter CFRange
in place of NSRange
.