Calculator(W) Sample Overview

The Calculator sample application demonstrates how to implement a calculator with basic mathematical operations.

The following figure illustrates the main screen of the Calculator.

Figure: Calculator screen

Calculator screen Calculator screen

The application opens with the Calculator screen, where the user can perform mathematical operations by clicking the applicable buttons.

Source Files

You can create and view the sample application project including the source files in the IDE.

Table: Source files
File name Description
config.xml This file contains the application information for the platform to install and launch the application, including the view mode and the icon to be used in the device menu.
css/style.css This file contains the CSS styling for the application UI.
img/ This directory contains the application images used in the layout.
index.html This is a starting file from which the application starts loading. It contains the layout of the application screen.
js/app.js This file contains the code for the main application module used for initialization.
js/model.js This file contains the application model (handles mathematical operations).
js/systeminfo.js This file contains the battery state handling code.
js/ui.js This file contains the implementation of the user interface.

Implementation

To implement the application:

  1. All application JavaScript files are loaded directly from the index.html file. The file also starts the initialization (by calling the init method of the app module, which acts as the application controller).
    <!--index.html-->
    <script src="js/systeminfo.js"></script>
    <script src="js/app.js"></script>
    <script src="js/ui.js"></script>
    <script src="js/model.js"></script>
    <script>
       app.init();
    </script>
    

    The app module initializes all other modules:

    • systeminfo: responsible for checking the battery level
    • model: encapsulates the calculator logic (equation state and mathematical operations)
    • ui: responsible for managing the UI (updating and listening to events)

    The app module also requests the ui module to update the equation field with a value obtained from the model module. Afterwards, the application is ready for use and waits for user actions.

    /* js/app.js */
    init: function init()
    {
       'use strict';
       systeminfo.init();
       model.init();
       ui.init();
       this.refreshEquation();
    },
    
    refreshEquation: function refreshEquation()
    {
       'use strict';
        ui.showEquation(model.equation);
    }
    
  2. The application layout is defined directly in the index.html file (styled by the CSS file) and managed by the ui module:
    <!--index.html-->
    <div id="screen">
       <div id="display" valign="middle" class="empty-result">
          <div id="equation"></div>
          <div id="result"><span id="resultvalue"></span></div>
       </div>
    </div>
    
    <div id="numpad">
       <div id="key_c" class="key col-4 c darkblue"></div>
       <div id="key_div" class="key col-4 darkblue"></div>
       <div id="key_mul" class="key col-4 darkblue"></div>
       <div id="key_del" class="key long-tap-repeat col-4 darkblue"></div>
    
       <div id="key_7" class="key col-4"></div>
       <div id="key_8" class="key col-4"></div>
       <div id="key_9" class="key col-4"></div>
       <div id="key_sub" class="key col-4 darkblue"></div>
    
       <div id="key_4" class="key col-4"></div>
       <div id="key_5" class="key col-4"></div>
       <div id="key_6" class="key col-4"></div>
       <div id="key_add" class="key col-4 darkblue"></div>
    
       <div id="key_1" class="key col-4"></div>
       <div id="key_2" class="key col-4"></div>
       <div id="key_3" class="key col-4"></div>
       <div id="key_bracket" class="key col-4 darkblue"></div>
    
       <div id="key_dec" class="key col-3 left"></div>
       <div id="key_0" class="key col-3 center"></div>
       <div id="key_sign" class="key col-3 right"></div>
    
       <div id="key_eql" class="key equal col-1"></div>
    </div>
    

    The ui module listens to touch events on the calculator buttons, updates their press state, and passes control to the app module to run proper actions. The ui module also exposes some methods which allow the controller module (app) to update the equation and its result (formatting those fields is also its responsibility).

    /* js/app.js */
    showEquation: function showEquation(equation)
    {
       'use strict';
       var e, element, elementText, span, equationElement, length;
    
       equationElement = document.getElementById('equation');
    
       equationElement.innerHTML = '';
    
       length = equation.length;
       for (e = 0; e < length; e += 1)
       {
          element = equation[e];
          span = document.createElement('span');
          elementText = element;
          if (Object.keys(this.operatorDisplays).indexOf(element) !== -1)
          {
             span.className = 'operator';
             elementText = this.operatorDisplays[element];
          }
          else
          {
             elementText = app.addSeparators(elementText);
          }
          elementText = elementText.replace(/-/g, '&minus;');
          span.innerHTML = elementText;
          equationElement.appendChild(span);
       }
    
       if (equation[0] && equation[0].length >= this.SMALL_FONT_THRESHOLD)
       {
          equationElement.classList.add('medium');
       }
       else
       {
          equationElement.classList.remove('medium');
       }
    
    },
    
    /*
       Shows string in a result element
       @param {string} result
       @param {boolean} error Error flag
       @private
    */
    show: function show(result, error)
    {
       'use strict';
    
       if (result === '')
       {
          return this.clear();
       }
    
       this.equationElement.classList.add('top');
       this.displayElement.classList.remove('empty-result');
    
       if (error === true)
       {
          this.resultElement.classList.add('error');
          if (result.length > this.MAX_DIGITS)
          {
             this.resultElement.classList.add('small');
          }
          else
          {
             this.resultElement.classList.remove('small');
          }
       }
       else
       {
          this.resultElement.classList.remove('error');
          this.resultElement.classList.remove('small');
       }
    
       this.resultValueElement.innerHTML = result.replace(/-/g, '&minus;');
    }
    
    /*
       Shows result in a result element
       @param {string} result
       @param {boolean} error Error flag.
    */
    showResult: function showResult(result, error)
    {
       'use strict';
       error = error || false;
       if (error)
       {
          this.error = true;
       }
       this.show(result, error);
       this.result = true;
    }, 
    
  3. The model module is responsible for the calculator logic. Its internal equation implementation is based on an array, which stores each component (number, operator) as a separate string. The module exposes a set of methods, which allow you to modify the equation and finally compute its value. The module also keeps the equation state valid by refusing to add a wrong component.

    The module's public interface consists of following methods:

    • addDigit(): adds a single digit to the equation.
    • addOperator(): adds an operator (+, -, *, /) to the equation.
    • addDecimal(): adds a decimal point to the equation.
    • deleteLast(): deletes the last digit or operator.
    • resetEquation(): clears the whole equation.
    • changeSign(): changes the sign of the last equation component.
    • isEmpty(): returns true if the equation is empty (does not contain any component).
    • calculate(): calculates the equation value.
    • addBracket(): adds the bracket sign to the equation.
    • isBracket(): checks whether a given value is a bracket sign.
    • findComponent(): searches for a specified component in the equation.
    • countComponent(): searches for a specified component and returns the number of occurrences.

    When the user touches any button (the following example presents the "equal" button flow), the ui module notifies the controller module (app) by calling its processKey() method. The request is dispatched, and the calculate() method of the model module is called. When the result is obtained, the controller module requests the ui module to update the applicable field in the UI (by calling the showResult() method).

    /* js/app.js */
    processKey: function processKey(key)
    {
       'use strict';
       var keys = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
       if (ui.isResultVisible())
       {
          if (Object.keys(this.operatorKeys).indexOf(key) === -1 &&
              key !== 'del' &&
              key !== 'eql' &&
              key !== 'sign')
          {
             model.resetEquation();
          }
       }
       ui.clearResult();
       if (keys.indexOf(key) !== -1)
       {
          this.pushDigits(key);
       }
       else if (Object.keys(this.operatorKeys).indexOf(key) !== -1)
       {
          model.addOperator(this.operatorKeys[key]);
       }
       else if (key === 'dec')
       {
          model.addDecimal();
       }
       else if (key === 'del')
       {
          model.deleteLast();
       }
       else if (key === 'c')
       {
          model.resetEquation();
       }
       else if (key === 'sign')
       {
          model.changeSign();
       }
       else if (key === 'bracket')
       {
          model.addBracket();
       }
    
       if (key === 'eql' && !model.isEmpty())
       {
          this.calculate();
       }
       this.refreshEquation();
    },
    
    calculate: function calculate()
    {
       'use strict';
       var result = '';
       try
       {
          result = model.calculate();
          result = this.addSeparators(result);
          ui.showResult('=&nbsp;' + result);
       }
       catch (e)
       {
          if (e instanceof EquationInvalidFormatError)
          {
             ui.showResult('Wrong format');
          }
          else if (e instanceof CalculationError)
          {
             ui.showResult('Invalid operation');
          }
          else if (e instanceof InfinityError)
          {
             ui.showResult((e.positive ? '' : '&minus;') + '&infin;');
          }
          else
          {
             ui.showError('Unknown error.');
             console.warn(e);
          }
       }
    }
    

    The calculate() method (in the model module) performs some checks to ensure the equation correctness and finally merges all its components into one string and runs it as a JavaScript expression (by the eval() method) to obtain its value. If no error occurs, the equation formatted result is returned.

    /* js/model.js */
    /*
       Calculates equation value
       @return {string}
    */
    calculate: function calculate()
    {
       'use strict';
       var evaluation = '',
          result,
          /*
             Checks whether the matched number is zero
             @param {string} m Whole match including the division operator
             @param {string} p1 Whole number, including sign and parenthesis
             @param {string} number The matched number
             @return {string}
          */
          checkDivisionByZero = function checkDivisionByZero(m, p1, number)
          {
             if (parseFloat(number) === 0)
             {
                throw new DivisionByZeroError();
             }
    
             return '/ ' + number;
          };
    
       if (this.calculated)
       {
          this.replaceLeftOperand(this.lastCalculationResult);
       }
    
       if (!this.isValidEquation())
       {
          throw new EquationInvalidFormatError();
       }
    
       this.calculated = false;
    
       /* Evaluate the equation */
       try
       {
          evaluation = this.equation.join(' ');
          evaluation = evaluation.replace(/\/ *(\(?\-?([0-9\.]+)\)?)/g, checkDivisionByZero);
    
          result = eval('(' + evaluation + ')');
          if (Math.abs(result) < 1.0E-300)
          {
             result = 0;
          }
       }
       catch (e)
       {
          console.error(e);
          throw new CalculationError();
       }
    
       if (isNaN(result))
       {
          throw new CalculationError();
       }
       if (result === Infinity || result === -Infinity)
       {
          throw new InfinityError(result === Infinity);
       }
    
       this.calculated = true;
       /* Format the result value */
       result = this.formatValue(result);
       /* Save the calculated result */
       this.lastCalculationResult = result;
    
       return result;
    }