Calculator(M) Sample Overview

The Calculator sample application demonstrates how you can create a calculator with basic mathematical operations.

The following figure illustrates the main screen of the Calculator.

Figure: Calculator screen

Calculator screen

The application opens with the Calculator screen, where you 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.

The application uses a simple MVC (Model-View-Controller) architectural model.

https://developer.tizen.org/dev-guide/2.3.1/org.tizen.web.apireference/html/device_api/mobile/tizen/application.html
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 images used to create the user interface.
index.html This is a starting file from which the application starts loading. It contains the layout of the application screens.
js/ This directory contains the application code.
js/app.js This file contains the code for the main application module used for initialization (Controller layer).
js/model.js This file contains the application model for handling mathematical operations (Model Layer).
js/systeminfo.js This file contains the battery state handling code.
js/ui.js This file contains the implementation code for the user interface (View layer).

Implementation

All JavaScript files are loaded directly from the index.html file. There is also initialization started by calling the init() method of the app module, which acts as an 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 loaded application modules are used for specific actions:

  • app module initializes all other modules:

    /* 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);
    }
    
  • systeminfo module is responsible for checking the battery. If the level.battery status is lower than 4% and the battery is not charging, the application terminates.

    /* js/app.js */
    
    /* Add listener for low battery level */
    listenBatteryLowState: function listenBatteryLowState() 
    {
       'use strict';
       this.systeminfo.addPropertyValueChangeListener('BATTERY', function change(battery) 
          {
             if (!battery.isCharging) 
             {
                tizen.application.getCurrentApplication().exit();
             }
          },
          {
             lowThreshold: 0.04
          });
    },
    
    /* Check low battery state */
    checkBatteryLowState: function checkBatteryLowState() 
    {
       'use strict';
       this.systeminfo.getPropertyValue('BATTERY', function onBatteryRead(battery) 
          {
             if (battery.level < 0.04 && !battery.isCharging) 
             {
                tizen.application.getCurrentApplication().exit();
             }
          },
          null);
    },
    
    /* Initialize the systeminfo module */
    init: function init() 
    {
       'use strict';
       if (typeof tizen === 'object' &&
           typeof tizen.systeminfo === 'object') 
       {
          this.systeminfo = tizen.systeminfo;
          this.checkBatteryLowState();
          this.listenBatteryLowState();
       } 
       else 
       {
          console.warn('tizen.systeminfo not available');
       }
    }
    
  • model module encapsulates the calculator logic (equation state and mathematical operations).

  • ui module is responsible for managing the UI (updating and listening to events):

    1. Register the event listeners:
      /* js/ui.js */
      
      /* Initialize the UI module */
      init: function init() 
      {
         'use strict';
         this.resultElement = document.getElementById('result');
         this.resultValueElement = document.getElementById('resultvalue');
         this.equationElement = document.getElementById('equation');
         this.preloadImages();
         this.bindEvents();
         this.error = false;
         this.result = false;
         /* Disable multitouch */
         document.body.addEventListener('touchstart', this.filterTap, true);
         document.body.addEventListener('touchend', this.filterTap, true);
      },
    2. Load the images and push them to the cache:

      preloadImages: function preloadImages() 
      {
         'use strict';
         var i, image;
         for (i = this.imagesToPreload.length - 1; i >= 0; i -= 1) 
         {
            image = new Image();
            image.src = this.imagesToPreload[i];
            this.imagesCache.push(image);
         }
      },
      
    3. Bound the registered listeners to the calculator numeric keyboard:

      bindEvents: function bindEvents() 
      {
         'use strict';
         var numpad = document.getElementById('numpad'),
             self = this;
      
         numpad.addEventListener('touchstart', function onTouchStart(e) 
         {
            /* Handle event */
         });
         numpad.addEventListener('touchend', function onTouchEnd(e) 
         {
            /* Handle event */
         });
         numpad.addEventListener('touchcancel', function onTouchCancel(e) 
         {
            /* Handle event */
         });
         document.addEventListener('tizenhwkey', function onTizenHwKey(e) 
         {
            if (e.keyName === 'back') 
            {
               tizen.application.getCurrentApplication().exit();
            }
         });
      },
      
    4. Update the equation field with a value obtained from the model module. Now, the application is ready for use and waits for user actions.

Layout Implementation

The Calculator screen displays the basic keyboard and equation:

<!--index.html-->
<table id=...screen...>
   <tr>
      <td id=...display... valign=...middle...>
         <div id=...overflow_top...></div>
         <div id=...overflow_bottom...></div>
         <div id=...equation...></div>
         <div id=...result... class=...empty...><span id=...resultvalue...></span></div>
      </td>
   </tr>
</table>

<div id=...numpad...>
   <div id=...key_7... class=...key... style=...clear: both;...></div>
   <div id=...key_8... class=...key...></div>
   <div id=...key_9... class=...key...></div>
   <div id=...key_c... class=...key...></div>
   <div id=...key_del... class=...key long-tap-repeat...></div>

   <div id=...key_4... class=...key... style=...clear: both;...></div>
   <div id=...key_5... class=...key...></div>
   <div id=...key_6... class=...key...></div>
   <div id=...key_div... class=...key...></div>
   <div id=...key_mul... class=...key...></div>

   <div id=...key_1... class=...key... style=...clear: both;...></div>
   <div id=...key_2... class=...key...></div>
   <div id=...key_3... class=...key...></div>
   <div id=...key_sub... class=...key...></div>
   <div id=...key_add... class=...key...></div>

   <div id=...key_0... class=...key... style=...clear: both;...></div>
   <div id=...key_dec... class=...key...></div>
   <div id=...key_sign... class=...key...></div>
   <div id=...key_eql... class=...longkey...></div>
</div> 

The ui module manages the layout by listening to touch events on the calculator buttons, updating their press state, and passing control to the app module to run a proper action. The ui module also allows the app module to update the equation and its result, and formats those fields.

/* js/ui.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);
   }
},

show: function show(result) 
{
   'use strict';

   if (result === '') 
   {
      return this.clear();
   }
   this.equationElement.classList.add('top');
   this.resultValueElement.innerHTML = result.replace(/-/g, '&minus;');
},

showResult: function showResult(result) 
{
   'use strict';
   this.show(result);
   this.result = true;
},

Equation Logic Implementation

The model module handles the calculator logic. Its internal implementation of the equation is based on an array that stores each component (such as a number or operator) as a separate string.

/* js/model.js */
formatValue: function formatValue(value) 
{
   'use strict';
   var formatted = '',
       textValue = '',
       dotIndex = 0;

   textValue = value.toString();
   dotIndex = textValue.indexOf('.');
   if (dotIndex >= this.MAX_DIGITS) 
   {
      /* If 2 first digits of the mantissa are higher than 95, round the result */
      /* This is the behavior of the Calculator app in Samsung phones  */
      if (parseInt(textValue.substr(dotIndex + 1, Math.min(textValue.length, 2)), 10) >= 95) 
      {
         value += 1;
      }
   }
   /* Set precision to match 10-digit limit */
   formatted = value.toFixed(this.MAX_DIGITS).toString();
   formatted = formatted.substr(0, 
                                this.MAX_DIGITS + formatted.replace(/\d/g, '').length).replace(/(\.(0*[1-9])*)0+$/, '$1').replace(/\.$/, '');

   /* If the number is too big (exceeds digits limit) */
   /* or is too small (rounds to zero) */
   /* or has scientific notation without decimals (1E23 vs 1.00000E23) */
   /* use properly formatted scientific notation */
   if ((formatted === '0' && value !== 0) ||
       value.toString().match(/[eE]/) ||
       Math.abs(value) >= Math.pow(10, 10)) 
   {
      formatted = value.toExponential(5).toString();
   }
   /* Uppercase 'E', remove optional '+' from exponent */
   formatted = formatted.toUpperCase().replace('E+', 'E');

   return formatted;
},

The model module contains the following functions to modify the equation and compute its value. It also keeps the equation state valid by refusing to add wrong components.

  • addDigit() adds a single digit to the equation:
    /* js/model.js */
    addDigit: function addDigit(digit) 
    {
       'use strict';
       var last = null;
    
       if (this.calculated) 
       {
          this.resetEquation();
       }
    
       last = this.getLastComponent();
    
       /* If the previous component is not a number */
       /* start a new component */
       /* if there is only a minus before */
       if ((!last || this.isOperator(last)) &&
           (last !== '-' || this.equation.length > 1)) 
       {
          this.addComponent(digit);
    
          return true;
       }
       this.replaceLastComponent(this.checkNegativeFormat(last));
    
       if (this.isNegativeComponent(last) || last === '-') 
       {
          last = '(-' +
                 (RegExp.$2 === '0' ? '' : RegExp.$2) +
                 digit + ')';
       } 
       else if (last === '0') 
       {
          last = digit;
       } 
       else 
       {
          last = last + digit;
       }
       if (last.replace(new RegExp('[^\\d]', 'g'), '').length <= this.MAX_DIGITS) 
       {
          this.replaceLastComponent(last);
    
          return true;
       }
    
       return false;
    },
    
  • addOperator() adds an operator (+, -, *, /) to the equation:
    /* js/model.js */
    addOperator: function addOperator(operator) 
    {
       'use strict';
       var last = null;
    
       if (this.calculated) 
       {
          this.resetEquation();
          this.addComponent(this.lastCalculationResult);
       }
    
       last = this.getLastComponent(true);
    
       /* Operators other than '-' cannot be added to empty equations */
       if (!last && operator !== '-') 
       {
          return;
       }
       /* Cannot replace minus if on first position */
       if (last === '-' && this.equation.length === 1) 
       {
          return;
       }
    
       this.replaceLastComponent(this.checkNegativeFormat(last));
    
       if (this.isOperator(last)) 
       {
          /* Replace the last operator with a new one */
          this.replaceLastComponent(operator);
       } 
       else 
       {
          /* Check for 'E' being the last character of the equation */
          if (last && last.match(/E$/)) 
          {
             /* Add '-' to the number and ignore other operators */
             if (operator === '-') 
             {
                this.replaceLastComponent(last + '-');
             }
          } 
          else 
          {
             /* Add operator */
             this.addComponent(operator);
          }
       }
    },
    
  • addDecimal() adds a decimal point to the equation:
    /* js/model.js */
    addDecimal: function addDecimal() 
    {
       'use strict';
       var last = this.getLastComponent();
    
       if (!last || this.isOperator(last)) 
       {
          this.addComponent('0' + this.DECIMAL);
       } 
       else 
       {
          this.replaceLastComponent(this.checkNegativeFormat(last));
          if (last.indexOf(this.DECIMAL) === -1)
          {
             if (this.isNegativeComponent(last)) 
             {
                last = '(-' + RegExp.$2 + this.DECIMAL + ')';
             } 
             else 
             {
                last += this.DECIMAL;
             }
             this.replaceLastComponent(last);
          }
       }
    },
    
  • deleteLast() Deletes the last digit or operator:
    /* js/model.js */
    removeLastChar: function removeLastChar(str) 
    {
       'use strict';
    
       return str.substring(0, str.length - 1).replace(this.EXPONENTIAL_REGEXP, '');
    },
    
    /* Delete the last element from equation (digit or operator) */
    deleteLast: function deleteLast() 
    {
       'use strict';
       var last = null, lastPositive;
    
       if (this.calculated) 
       {
          this.resetEquation();
          this.addComponent(this.lastCalculationResult);
    
          return;
       }
    
       last = this.getLastComponent();
    
       if (!last) 
       {
          return;
       }
    
       this.replaceLastComponent(this.checkNegativeFormat(last));
    
       if (this.isNegativeComponent(last)) 
       {
          lastPositive = RegExp.$2;
          if (lastPositive.length === 1) 
          {
             this.equation.pop();
          } 
          else 
          {
             this.replaceLastComponent('(-' + this.removeLastChar(lastPositive) + ')');
          }
       }
       else if (last.length === 1 || last.match(/^\-[0-9]$/)) 
       {
          this.equation.pop();
       } 
       else 
       {
          this.replaceLastComponent(this.removeLastChar(last));
       }
    },
    
  • resetEquation() Clears the whole equation:
    /* js/model.js */
    resetEquation: function resetEquation() 
    {
       'use strict';
       this.equation = [];
       this.calculated = false;
    },
    
  • changeSign() changes the sign of the last equation component:
    /* js/model.js */
    isNegativeComponent: function isNegativeComponent(component) 
    {
       'use strict';
    
       return (new RegExp('(\\()\\-(.*?)(\\))')).test(component);
    },
    	
    changeSign: function changeSign() 
    {
       'use strict';
       var last;
    
       if (this.calculated) 
       {
          this.resetEquation();
          this.addComponent(this.lastCalculationResult);
       }
    
       last = this.getLastComponent();
       /* If there is at least one component and last component not an operator or zero */
       if (last && !this.isOperator(last) && last !== '0') 
       {
          if ((/^\-/).test(last)) 
          {
             last = '(' + last + ')';
          }
          if (this.isNegativeComponent(last)) 
          {
             last = RegExp.$2; 
             /* Assign the last matched value */
          } 
          else 
          {
             last = '(-' + last + ')';
          }
          this.replaceLastComponent(last);
    
          return true;
       }
    
       return false;
    },
    
  • isEmpty() Returns true if the equation does not contain any components:
    /* js/model.js */
    isEmpty: function isEmpty() 
    {
       'use strict';
    
       return this.equation.length === 0;
    },
    
  • calculate() calculates the equation value.

    When the user touches a button, the ui module notifies the app module by calling its processKey() function. The request is dispatched and finally the calculate() function of the model module is called. When the result is obtained, the app module requests the ui module to update the applicable field in the UI by calling the showResult() function.

    /* 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();
       }
       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() function checks the equation correctness and finally merges all its components into a single string and runs it as a JavaScript expression to obtain its value. The equation result is returned:

    /* js/model.js */
    calculate: function calculate() 
    {
       'use strict';
       /* jslint evil: true */
       /* jslint unparam: true */
       var evaluation = '',
           result,
           /* Checks whether the matched number is zero */
           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;
    }
    

The following example shows the support functions for the model module:

/* js/model.js */

/* Returns the last component of equation*/
getLastComponent: function getLastComponent(correct) 
{
   'use strict';
   var last = this.equation[this.equation.length - 1] || null;
   if (correct && last && last.slice(-1) === this.DECIMAL) 
   {
      last = last.slice(0, -1);
      last.replace('.)', ')');
      this.equation[this.equation.length - 1] = last;
   }

   return last;
},

/* Replaces the last equation component with specified value */
replaceLastComponent: function replaceLastComponent(value) 
{
   'use strict';
   var length = this.equation.length;

   if (length > 0) 
   {
      this.equation[length - 1] = value;
      this.calculated = false;
   }
},

/* Adds a new component to the equation */
addComponent: function addComponent(value) 
{
   'use strict';
   this.equation.push(value);
   this.calculated = false;
},

/* Returns true if the specified value is an operator */
isOperator: function isOperator(value) 
{
   'use strict';

   return this.OPERATORS.indexOf(value) !== -1;
},

/* Returns true if the equation can be calculated */
isValidEquation: function isValidEquation() 
{
   'use strict';
   var last = this.getLastComponent(true);

   return (!this.isOperator(last) && !last.match(/E-?$/));
},

/* Replaces the left operand with specified value*/
replaceLeftOperand: function replaceLeftOperand(value) 
{
   'use strict';
   var length = this.equation.length,
       leftOperandSize = 0;

   if (length === 0) 
   {
      return;
   }
   if (length === 1) 
   {
      leftOperandSize = 0;
   } 
   else if (length === 2) 
   {
      leftOperandSize = 1;
   } 
   else 
   {
      leftOperandSize = length - 3;
   }

   this.equation.splice(0, leftOperandSize);
   this.equation[0] = value;
   this.calculated = false;
}

/* Checks whether the component represents a negative digit */
isNegativeComponent: function isNegativeComponent(component) 
{
   'use strict';

   return (new RegExp('(\\()\\-(.*?)(\\))')).test(component);
},

/* Checks whether the component is negative and fixes its format */
checkNegativeFormat: function checkNegativeFormat(component) 
{
   'use strict';
   if (component && component.match(/^\-d+/)) 
   {
      component = '(' + component + ')';
   }

   return component;
},

/* Changes the sign of the last component (if applicable) */
/* Returns true if sign was changed */
changeSign: function changeSign() 
{
   'use strict';
   var last;

   if (this.calculated) 
   {
      this.resetEquation();
      this.addComponent(this.lastCalculationResult);
   }

   last = this.getLastComponent();
   /* If there is at least one component and last component not an operator or zero */
   if (last && !this.isOperator(last) && last !== '0') 
   {
      if ((/^\-/).test(last)) 
      {
         last = '(' + last + ')';
      }
      if (this.isNegativeComponent(last)) 
      {
         last = RegExp.$2; 
         /* Assign the last matched value */
      } 
      else 
      {
         last = '(-' + last + ')';
      }
      this.replaceLastComponent(last);

      return true;
   }

   return false;
},