Parsing Strings with NSScanner
I've decided to try out a new post format where I present some actual production code I've written. It's almost definitely not brilliant (and it might not even be correct) but it's something that will have struck me as worth discussing or maybe just underrepresented online.
So let's get real...
I often forget about NSScanner. Maybe because I've spent so much time working in languages where there's no analog. Today I figured I'd share a function I wrote recently that uses NSScanner to parse KML coordinate sets.
From the docs:
An NSScanner object interprets and converts the characters of an NSString object into number and string values. You assign the scanner’s string on creating it, and the scanner progresses through the characters of that string from beginning to end as you request items.
So now that that's cleared up, this is how a polygon is represented in KML:
<coordinates>-122.2093391418457,37.467092345296194,0.0 -122.2005844116211,37.46177847961746,0.0 -122.19423294067383,37.46968101483232,0.0 -122.20487594604492,37.470498470798724,0.0 -122.2093391418457,37.467092345296194,0.0</coordinates>
Each point is represented as a longitude, latitude, elevation
coordinate tuple. Each tuple is separated by a space. If this were Ruby or Python I'd probably split the string by spaces and then each part by the comma to get at each value. But Objective-C isn't Ruby or Python and NSScanner has helpful built-in methods for parsing numbers from strings.
Here's the function I came up with:
NSArray *parseCoordinateString(NSString *string) {
NSScanner *scanner = [NSScanner scannerWithString:string];
NSMutableArray *all = [NSMutableArray array];
NSMutableArray *set = [NSMutableArray array];
BOOL setComplete = NO;
while (![scanner isAtEnd]) {
if (setComplete) {
if ([set count] != 3) {
return nil;
}
[all addObject:[set copy]];
[set removeAllObjects];
setComplete = NO;
}
else {
double doubleVal;
if (![scanner scanDouble:&doubleVal]) {
return nil;
}
[set addObject:@(doubleVal)];
if (![scanner scanString:@"," intoString:NULL]) {
setComplete = YES;
}
}
}
[all addObject:[set copy]];
return [all copy];
}
First I initialize the NSScanner instance with the string and set a flag to allow me to keep track of when I've scanned a full set of coordinates. From here I start a loop (breaking when I've scanned the entire string) where on each iteration I:
- Look for a completed set, storing the array and resetting the flag if one is found.
- Scan the next double value and store it as an NSNumber in the set array.
- Look for a comma (or absence thereof) after the double which indicates that there are more values within this set to scan.
Most of NSScanner's methods return a BOOL and take an address where the matching value (if any) should be stored. You can use the return values to control flow while at the same time parsing object or scalar values and storing them into variables for later use. If you just want to know if there's a match you can pass NULL to the method.
NSScanner is a powerful class and one I could make better use of.