Ad

Our DNA is written in Swift
Jump

Free Range

You will often find yourself working with NSRange parameters and variables, especially when dealing with strings. I stumbled into a problem that I think is an SDK bug, that prompted me to look at the header and find out what kind of functions are provided to us for comfortably dealing with NSRange.

Don’t be fooled by the NS to think that this is an object. Similar to NSInteger this is just a scalar value, basically shorthand for the compiler to know that there are two components – location and length – with 4 bytes each. So if you access range.length you are interested in the second 4 bytes, for range.location you access the first 4.

This also means that you don’t have any instance methods available, all manipulation of NSRange has to be via C-style functions. So let’s have a look at the NSRange.h header contained in the Foundation.framework to see what interesting functions await for us to be unearthed there.

In fact, let me print the entire header and let’s read it together.

typedef struct _NSRange {
    NSUInteger location;
    NSUInteger length;
} NSRange;

This is how the NSRange is defined as a type. Types are what you need to define variables. You can see that NSRange consists of two unsigned integers.

typedef NSRange *NSRangePointer;

In some instances you might not deal with an NSRange directly, but with a pointer to 8 bytes of memory containing an NSRange. Instead of writing NSRange *every time, Apple defines an NSRangePointer for us to use. Since these are a couple more characters to type then the only reason I can think of is safety. Using NSRangePointer avoids the risk of forgetting the asterisk.

NS_INLINE NSRange NSMakeRange(NSUInteger loc, NSUInteger len) {
    NSRange r;
    r.location = loc;
    r.length = len;
    return r;
}

This is the most convenient way to fill an NSRange with values. It is defined as inline which means that this actual code is inserted into yours everywhere you use it as opposed to being execute as a true function call, which would involve copying stuff onto the stack, jumping to code, copying the result back and jumping again. Inlining happens transparently to you, but gives you the better performance of avoiding the function call. This usually makes sense for very small functions like this one.

NS_INLINE NSUInteger NSMaxRange(NSRange range) {
    return (range.location + range.length);
}

This is neat, using this inline function we don’t have to do the addition ourselves to find the maximum index contained in the range.

NS_INLINE BOOL NSLocationInRange(NSUInteger loc, NSRange range) {
    return (loc - range.location < range.length);
}

Another neat inline function lets us check if an index is contained in the range.

NS_INLINE BOOL NSEqualRanges(NSRange range1, NSRange range2) {
    return (range1.location == range2.location && range1.length == range2.length);
}

This saves us even more work if we wanted to see if two ranges are identical.

FOUNDATION_EXPORT NSRange NSUnionRange(NSRange range1, NSRange range2);

This creates a union of two ranges. The code for this is actually contained in one binary of the Foundation framework. The resulting union will go from the smallest location to include the maximum index. So if you create a union of two non-intersecting ranges the result also covers the space between the ranges.

FOUNDATION_EXPORT NSRange NSIntersectionRange(NSRange range1, NSRange range2);

This makes an intersection. This works only if the ranges are overlapping. If they don’t then {0,0} is returned. Knowing this we can easily check if one range is contained within another by creating an intersection and then checking if length is non-zero.

FOUNDATION_EXPORT NSString *NSStringFromRange(NSRange range);

If we needed a string representation of a range, then this function would quickly provide it for you. Use in NSLog for example.

FOUNDATION_EXPORT NSRange NSRangeFromString(NSString *aString);

In some even rarer cases you might want to do the reverse, move from NSString to NSRange. This is the function to achieve that.

@interface NSValue (NSValueRangeExtensions)
 
+ (NSValue *)valueWithRange:(NSRange)range;
- (NSRange)rangeValue;
 
@end

And the final block defines a category for NSValue to package NSRanges into objects for storing for whenever you need to have an Objective-C object, like if you want to store multiple ranges in an array.

Now, about the bug that I suspect to have found. NSAttributedString has a method to enumerate over all the attribute dictionaries and you get an NSRange for where this attributes are valid. enumerateAttributesInRange:options:usingBlock:

This usually works without problems unless you run into a low-memory condition. Apparently there is a situation when this method returns a range outside of the string. So this is how I worked around it to avoid problems.

NSRange validRange = NSMakeRange(0, [attributedString length]);
 
[attributedString enumerateAttributesInRange:range options:0 usingBlock:
         ^(NSDictionary *attrs, NSRange range, BOOL *stop)
{
         if (NSIntersectionRange(range, validRange).length)
         {
                  // work with attributes
         }
         else
         {
             NSLog(@"Invalid Range returned by attribute enumeration: %@", NSStringFromRange(range));
         }
}
];

So if the value returned from the enumeration is outside of the valid range then I ignore it.

Knowing of these functions allows you to more efficiently deal with NSRanges. So you might want to commit these to memory or make a mental or actual bookmark of this article.


Categories: Recipes

3 Comments »

  1. An extremely minor nitpick:

    > Similar to NSInteger this is just a scalar value, basically shorthand for the compiler to know that there are two components – location and length – with 4 bytes each.

    Technically, NSRange is a “structure type” which is composed of two scalar values. I realize this is a pretty pedantic nitpick, but there is a slightly more useful nitpick in the quoted text: “with 4 bytes each”. Really they are of `sizeof(NSUInteger)` bytes each, which will typically be 4 bytes on a 32-bit target, and 8 bytes on a 64-bit target.

    This is why I tend to hate doing technical writing- sometimes these kinds of “simplifications” are appropriate for your target audience and sometimes they aren’t, and finding a good balance is always hard. And finding an elegant, compact way of expressing something technical where you are able to communicate all the pedantic nuances without resorting to “simplifications” is orders of magnitude harder- I’ve spent hours just word-smithing a paragraph trying to find that sweet spot. 🙁

    And something that is both relevant and actually useful: You can toll-free bridge NSString and CFStrings, and both have a *Range type. While the Foundation side of things uses NSRange, which has NSUInteger values, the Core Foundation side of things uses this:

    typedef struct {
    CFIndex location;
    CFIndex length;
    } CFRange;

    The important bit is “CFIndex”, which is defined as:

    typedef signed long CFIndex;

    In other words, Foundation uses an *unsigned* type, whereas Core Foundation uses a *signed* type. This can, and does, create the occasional odd corner case if you’re using toll free bridging.

  2. NS_INLINE BOOL NSLocationInRange(NSUInteger loc, NSRange range) {
    return (loc – range.location < range.length);
    }

    uses a slightly disgusting hack. Since loc and range.location are both NSUIntegers, this code dispenses with the check that range.location <= loc by assuming that loc – range.location would return a wrapped-around huge NSUInteger if that is not the case.

    So don't use huge ranges. And don't depend on unsigned integer arithmetic!

    And imagine how this interacts with the above-mentioned CFIndex punning.

  3. I just got bit by NSLocationInRange’s use of subtraction. I was using a “length” of NSUIntegerMax to indicate that it was unbounded on the upper end. This, of course, resulted in *everything* being considered within that range.

    I think I will file a radar on this– at the very least, they should mention something in the documentation.