In Objective-C, ranges are portable things. NSRange consists of a simple integer position and length and can be used with any object that supports integer-based indexing.
typedef struct _NSRange { NSUInteger location; NSUInteger length; } NSRange;
Swift introduces Index types that offer a different take on ranges. Consider the following:
// Fetch a range let string = "My String" let range = string.rangeOfString("Str")! let string2 = "Another String" let result2 = string2.substringWithRange(range) // can seem to work // It fails with a string that uses different backing traits let string3 = "????????????????" let result3 = string3.substringWithRange(range) // bzzt, returns "�?"
You cannot apply the range returned from the initial string to string3 even though both items are typed String and their ranges are typed String.Index. The backing character stores do not match.
Don’t assume that the range from “My String” works with “Another String”. Even if strings are both ASCII, they may use different internal encodings. The range may appear to work with another string with the same basic properties but this is not reliable and it may just as easily misbehave.
Follow the golden rule of Swift String indices: “Never use String.Index with a string other than the string it belongs to.” This generalizes to a don’t-share-needles-or-indices rule for every index type.
Instead, create range portability by abstracting ranges, as in the following struct.
struct AbstractRange : Printable { let start : Int let end : Int // note: advance() and distance() are O(1) for RandomAccessIndexType and otherwise O(n) init<T : CollectionType where T.Index : ForwardIndexType>(range: Range<T.Index>, reference: T) { start = numericCast(distance(reference.startIndex, range.startIndex)) end = numericCast(distance(reference.startIndex, range.endIndex)) } func rangeForItem<T : CollectionType where T.Index : ForwardIndexType>(item: T) -> Range<T.Index> { return advance(item.startIndex, numericCast(start))..<advance(item.startIndex, numericCast(end)) } var description : String {return "\(start)..<\(end)"} }
Index types enable you to move forward (for forward index types) and back (for bidirectional types) through a collection.
The intermediate integer representations enable you to target ranges for specific strings:
let aRange = AbstractRange(range: range, reference: string) let newResult2 = string2.substringWithRange(aRange.rangeForItem(string2)) // correct let newResult3 = string3.substringWithRange(aRange.rangeForItem(string3)) // correct
The results are now fixed and the indexing ranges are defined with respect to their target collections.
Let me finish with a quick warning. Using the right character stride length does not immunize you from errors. When you address indices outside string bounds, a string-adjusted range crashes just as nastily as a natively created range does.
let aRange2 = AbstractRange(range: string3.rangeOfString("??")!, reference: string3) // 8..<10 let r2 = string.substringWithRange(aRange2.rangeForItem(string)) // boom
In the end, while AbstractRange is interesting to think about and helped me understand how string-specific ranges work, there’s little reason to need this solution in practice.
Thanks, once again, to Lily Ballard and Mike Ash for insights.
One Comment
I don’t understand why you think this is specific to Swift. The exact same thing happens if you do, in ObjC:
NSString *string = @"My String";
NSRange range = [string rangeOfString:@"Str"];
NSString *string2 = @"Another String";
NSString *result2 = [string2 substringWithRange:range];
NSString *string3 = @"";
NSString *result3 = [string3 substringWithRange:range];
The only difference is that in Swift you have the advance() function, which is just the equivalent of doing something similar to
NSUInteger advance(NSString *str, NSUInteger startIndex, NSUInteger count) {
NSUInteger ndx = startIndex;
while (count--) {
NSRange r = [str rangeOfComposedCharacterSequenceAtIndex:ndx];
ndx = r.location + r.length;
}
return ndx;
}
so then you can do
NSRange mapRange(NSString *target, NSRange chRange) {
NSUInteger start = advance(target, 0, chRange.location);
NSUInteger end = advance(target, start, chRange.length);
return NSMakeRange(start, end - start);
}
and
NSString *newResult3 = [string3 substringWithRange:mapRange(string3, range)];
which gives