Thursday, September 13, 2012

Shunting-yard algorithm and RPN computer

The father of non-cross-platform Java(read: J++) is so moving to JavaScript


"Java used to be cross-platform but is no longer cross-platform; the new cross-platform language in town is JavaScript" -- Anders Hejlsberg


It would be ironic if he would introduce another variant of JavaScript. They could be making IE's JavaScript uberfast, or they could make a transpiler(portmanteau of translator and compiler) for C# to JavaScript. They could even introduce another MVC framework for JavaScript, I'm pining for this.


If you wanted to see a glimpse of future JavaScript programming, you can check AngularJS. It's an MVC framework for JavaScript that allows Web Developers to do JavaScript programming sans the DOM/HTML concerns(for Web Designers). As with any MVC framework, AngularJS allows your Controller to be free from View concerns, View just receive values from Model; lastly, Controller doesn't directly manipulate the View, it just feed the View with Model. Proper separation of concerns at its finest.


An MVC example, expression evaluator: http://jsfiddle.net/7jh9f/2/


The expression evaluator utilizes Shunting-yard algorithm and RPN evaluator. The jsFiddle example let you visualize how those two algorithms works. With MVC, your algorithm(at the Controller) doesn't need to manipulate the HTML(View) in order to make visualization for the algorithm's steps; it's the other way around, the View automagically react to the values of your Model(which is the only thing your algorithm(at the Controller) directly manipulates), your View is able to reflect how your algorithm works.

Model and Controller:

function Controller($scope) {
    
    // $scope is similar to *this* keyword of OOP, it disambiguates local variable
    // against the class-level variable
    
    // ====Model====
    
    // result: 11
    // $scope.expression = '1 + 2 * 3 - 4 + 5 * 6 / 2 - 2 * 3 ^ 2 + ( 2 + 5 ) * 2';
    $scope.expression = '2 + 3 * 4 - 5 + 7 * 6 / 3 - 2 * 3 ^ 2 + ( 5 - 2 ) * 2';
    
    // result: 16
    // $scope.expression = '1 + 2 * 3 - 4 + 5 * 6 / 2 - 2';

    
    $scope.yieldIntermediateResults = true;
    $scope.normal = null;
    
    $scope.rpn = [];  
    $scope.opStack = [];    
    
   
    $scope.values = [];
    
    
    // ====Controller's Actions====
    
    
    $scope.structure = function() {
        $scope.normal = $scope.expression.split(' ');          
    };
    
    $scope.convertToRpn = function() {
        while ((elem = $scope.normal.shift()) != null)  {
           
            
            if (elem == '(' || elem == ')') {
                if (elem == '(')
                    $scope.opStack.push(elem);
                else if (elem == ')') {
                    while((op = $scope.opStack.pop()) != '(') {
                        $scope.rpn.push(op);
                    }
                }                    
            }
            else if (!elem.isOperator()) {                
                $scope.rpn.push(elem);
            }
            else {
                
                var peek = $scope.opStack[$scope.opStack.length - 1];

                
                while ($scope.opStack.length > 0 &&
                    (
                        (elem.isLeftAssociative() && elem.operatorPrecedence() <=  peek.operatorPrecedence())
                        ||
                        elem.operatorPrecedence() < peek.operatorPrecedence()
                    )                        
                       
                      ) { 
                      
                     $scope.rpn.push($scope.opStack.pop());
                          
                     if ($scope.opStack.length > 0)
                         peek = $scope.opStack[$scope.opStack.length - 1];                          
                          
                }//while  

               
                
                $scope.opStack.push(elem);                
                
                
                              
            }// if
                
            if ($scope.yieldIntermediateResults)            
                return;
        } // while
        
        while($scope.opStack.length > 0) {
            $scope.rpn.push($scope.opStack.pop());
            
            if ($scope.yieldIntermediateResults)            
                return;            
        }
        
       
        
        
    };
    
    
    $scope.solveRpn = function() {
        while ((elem = $scope.rpn.shift()) != null)  {
            
            if (elem.isOperator()) {
                var opB = parseInt($scope.values.pop());
                var opA = parseInt($scope.values.pop());
                
                var result = 0;
                
                if (elem == '^') {
                    result = Math.pow(opA, opB);
                } else if (elem == '*') {
                    result = opA * opB;
                } else if (elem == '/') {
                    result = opA / opB;
                } else if (elem == '+') {
                    result = opA + opB;                    
                } else if (elem == '-') {
                    result = opA - opB;
                } 
                
                $scope.values.push(result);
                
            } else {
                $scope.values.push(elem);
            }
            
            if ($scope.yieldIntermediateResults)            
                return;
            
        }//while
        

        // $scope.tellAnswer = 'Answer is ' + $scope.values.pop();
    };
    
}

String.prototype.isOperator = function() {
    return this == '*' || this == '/' || this == '+' || this == '-' || this == '^';
};

String.prototype.isRightAssociative = function() {
    return this == '^';
};

String.prototype.isLeftAssociative = function() {
    return !this.isRightAssociative();
};

String.prototype.operatorPrecedence = function() {
    
    if (this == '^') return 3;
    else if (this == '*' || this == '/') return 2;
    else if (this == '+' || this == '-') return 1;
    return 0;
};​

View:

<div ng-app ng-controller="Controller">

    
    <div ng-show='normal != null'>{{expression}}</div>
    <br/>
    
    
    <div ng-show='normal == null'>
    <input type='text' style='width: 300px' ng-model='expression' />
        <br/><br/>    
        <input type='checkbox' ng-model='yieldIntermediateResults '/> Yield Intermediate Results
        <br/></br>
    
    <input type='button' value='Structure' ng-click='structure()' />
    </div>
    
    
    <div ng-show='normal.length > 0 || opStack.length > 0'>
    <input type='button' value='Convert to Reversed Polish Notation' ng-click='convertToRpn()' />
        <br/><br/>
    </div>    
    
    <div ng-show='normal.length > 0'>
        Normal: <span class='box' ng-repeat='x in normal'>{{x}}</span>
        <br/><br/>
    </div>
    
    
    
    <div ng-show='normal.length > 0 || rpn.length > 0'>
        RPN: <span class='box' ng-repeat='x in rpn'>{{x}}</span>
        <hr/>
    </div>
    
    <div ng-show='opStack.length > 0'>
        <div class='box' style='width: 10px' ng-repeat="op in opStack.slice(0).reverse()" >{{op}}</div>
    </div>
        
    <div ng-show='yieldIntermediateResults && ((normal != null && normal.length > 0) || opStack.length > 0)'>
        __OP STACK__
        <hr/>
    </div>    


  

    

    <div ng-show="(normal.length == 00 && rpn.length > 0 && opStack.length == 0)" >
    <input type='button' value='Solve RPN' ng-click='solveRpn()'/>
        <br/>
    </div>
    
    
    <div ng-show="yieldIntermediateResults && (normal.length == 00 && rpn.length > 0 && opStack.length == 0)" >
        <div class='box' style='width: 15px' ng-repeat="v in values.slice(0).reverse()" >{{v}}</div>

        __VALUES STACK__
        <hr/>
    </div>    
    
    
    <div ng-show='rpn.length == 0 && values.length == 1'>Answer is {{values[0]}}</div>
    
</div>​

CSS:
.box {
    border-width: 1px;
    border-style: solid;
    margin: 2px;
    padding: 3px;
    border-color: red;
}​

No comments:

Post a Comment