Tokenizing an NSString in iPhone

Sometimes we want to split a string into its component parts; this process is called tokenizing. In this blog, we’ll write a very simple expression evaluator which takes a string entered by the user and attempts to evaluate it as a arithmetic expression. Let’s see how it works!

Start Xcode and select “Create a new Xcode project.” Choose the Single View Application template and click Next. Name the project “Parser” and select options as shown:

Click Next, choose a location to save the project, and click Create.

The interface of the application will be very simple: we want a text field for the user to enter an expression, and three labels: two as prompts, and a third to show the result of evaluating the expression. Open ViewController.xib and add these controls as shown:

We’ve left the text of the third label as “Label.” If you wish, change this text to “” so that it will not show unless an expression has been evaluated. Note that we’ve also changed the color of the view’s background.

Now we’ll add outlets and an action method to our view controller. Open ViewController.h, and make changes as shown here:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic, strong) IBOutlet UITextField *expression;
@property (nonatomic, strong) IBOutlet UILabel *evaluation;

(IBAction)expressionEntered:(UITextField *)sender;

@end

Wire these up in interface builder. The expression object should be wired to the text field, the evaluation object to the label under the text field, and the expressionEntered method to the text field’s DidEndOnExit event. The image to the right shows the result of properly wiring up the controls in the File’s Owner property list.

Open ViewController.m and make the changes shown below. Note here that we’ve also made changes to the viewDidLoad: and viewDidUnload: methods.

#import "ViewController.h"

@interface ViewController ()
{
    NSCharacterSet *ops;
    NSCharacterSet *nums;
}
@end

@implementation ViewController

@synthesize expression, evaluation;

(IBAction)expressionEntered:(UITextField *)sender
{
    [self.expression resignFirstResponder];
    float result = 0;
    NSArray *operands = [sender.text componentsSeparatedByCharactersInSet:ops];
    NSArray *tmpOperators = [sender.text componentsSeparatedByCharactersInSet:nums];
    NSMutableArray *operators = [[NSMutableArray alloc] initWithCapacity:5];
    for (NSString *op in tmpOperators) {
        if (![op isEqualToString:@""]) {
            [operators addObject:op];
        }
    }
    if (operands.count == operators.count + 1) {
        NSLog(@"\nOperands:%@", operands);
        NSLog(@"\nOperators:%@", operators);
        result = [[operands objectAtIndex:0] floatValue];
        for (int i = 1; i < operands.count; i++) {
            if ([[operators objectAtIndex:i 1] isEqualToString:@"+"]) {
                result += [[operands objectAtIndex:i] floatValue];
            }
            else if ([[operators objectAtIndex:i 1] isEqualToString:@"-"]) {
                result -= [[operands objectAtIndex:i] floatValue];
            }
            else if ([[operators objectAtIndex:i 1] isEqualToString:@"*"]) {
                result *= [[operands objectAtIndex:i] floatValue];
            }
            else if ([[operators objectAtIndex:i 1] isEqualToString:@"/"]) {
                result /= [[operands objectAtIndex:i] floatValue];
            }
            else {
                ;
            }
        }
        self.evaluation.text = [NSString stringWithFormat:@"%f", result];
    }
}

(void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    ops = [NSCharacterSet characterSetWithCharactersInString:@"+-/*"];
    nums = [NSCharacterSet characterSetWithCharactersInString:@"0123456789."];
}

(void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    self.expression = nil;
    self.evaluation = nil;
}

(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

@end

First, we notice that there are entries in the @interface section of this implementation file. The @interface section of an .m file is a good place to put instance variables that will not be used outside the object. In this case, we declare two NSCharacterSet iVars that will be used as we parse through the entered expression. These are initialized in viewDidLoad:

ops = [NSCharacterSet characterSetWithCharactersInString:@"+-/*"];
    nums = [NSCharacterSet characterSetWithCharactersInString:@"0123456789."];

ops is a character set containing only the characters corresponding to the operations we want to deal with in this implementation. The nums character set contains the ten decimal digits as well as the decimal point character (a period).

After synthesizing the properties, we implement the expressionEntered: method. First, we resignFirstResponder on the sender, which will dismiss the keyboard from the view. Next, initialize a float to hold the expression result.

The heart of the application is really in the next few lines:

NSArray *operands = [sender.text componentsSeparatedByCharactersInSet:ops];
    NSArray *tmpOperators = [sender.text componentsSeparatedByCharactersInSet:nums];
    NSMutableArray *operators = [[NSMutableArray alloc] initWithCapacity:5];
    for (NSString *op in tmpOperators) {
        if (![op isEqualToString:@""]) {
            [operators addObject:op];
        }
    }

The operands array is an array of strings generated here by splitting the sender’s text into tokens separated by any character in the ops array: this will give an array of string representations of the operands (numbers) in the expression. Similarly, the tmpOperators array is an array of strings containing the operations to be performed on the numbers in the operands array.

tmpOperators may also contain empty strings, so we want to make sure to purge those from the array. This is done in the for loop.

At this point, we should have one more operand than we have operators. If this is true, we proceed to evaluate the expression.

if (operands.count == operators.count + 1) {
        NSLog(@"\nOperands:%@", operands);
        NSLog(@"\nOperators:%@", operators);
        result = [[operands objectAtIndex:0] floatValue];
        for (int i = 1; i < operands.count; i++) {
            if ([[operators objectAtIndex:i 1] isEqualToString:@"+"]) {
                result += [[operands objectAtIndex:i] floatValue];
            }
            else if ([[operators objectAtIndex:i 1] isEqualToString:@"-"]) {
                result -= [[operands objectAtIndex:i] floatValue];
            }
            else if ([[operators objectAtIndex:i 1] isEqualToString:@"*"]) {
                result *= [[operands objectAtIndex:i] floatValue];
            }
            else if ([[operators objectAtIndex:i 1] isEqualToString:@"/"]) {
                result /= [[operands objectAtIndex:i] floatValue];
            }
            else {
                ;
            }
        }
        self.evaluation.text = [NSString stringWithFormat:@"%f", result];

Logging out the operands and operators allows us to verify that we’re getting good results. Of course, we would remove these log statements in production code.

We set result to the floatValue of the first operand, then enter a for loop that walks through the remaining operands, looking at the corresponding operator in the operators array. Depending on the operator found, we then perform the corresponding operation. In the final else clause we do nothing; this might be a good place to catch invalid operations and throw an exception in a production program.
Finally, we set the evaluation object’s text field to a string representation of result.

In this implementation, we’ve ignored operator precedence: the expression will be evaluated from left to right regardless of the operands. For example, the expression 3+4*2 will be evaluated as 7*2=14 rather than 3+8=11. In order to implement operator precedence, we could reorder both the operands and operator arrays according to operator precedence (placing * and / before + and -) before entering the for loop in the expressionEntered: method. This is left to you to implement: the point of this blog is tokenizing strings.

Another thing you might try is implementing parentheses in expressions. This would allow expressions of the type (3+4)*2. The effect of parentheses is to override precedence rules: anything in parens should be evaluated first.

Run the application and observe the result of entering an expression in the text field:

Remember that the rules of precedence don’t exist: the correct mathematic evaluation of 2+8/2*3 is 2+((8/2)*3) = 14. Here, the expression is evaluated from right to left without regard to precedence rules: 2+8 = 10, 10/2 = 5, 5*3 = 15. If we want the correct evaluation, we must reorder our expression:

Have fun implementing operator precedence and parentheses in this application. You will also want to acquaint yourself with the NSCharacterSet class by reading the class reference in the iOS documentation, either in Xcode or online at the link below:

https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nscharacterset_Class/Reference/Reference.html

Leave a Reply

Your email address will not be published. Required fields are marked *