kachkaev/humane-math

View on GitHub
src/core/calculator.js

Summary

Maintainability
D
2 days
Test Coverage
import _ from 'underscore';

import {TokenType}    from './analysis/1-lexical/token-type';
import {TreeNodeType} from './analysis/2-syntactic/tree-node-type';
import {Text}         from './text';

/**
 * Calculator can be used for extracting node values.
 * It is also helpful to precompute values for some nodes
 * a tree in order to make further calculations faster. The calculator
 * does not simplify a tree or make any other changes to it except
 * for adding or removing “value” property to nodes.
 * Calculator is a Visitor.
 */
export class Calculator {

  /**
   * This inner use exception is to be thrown anytime a calculation of sub-nodes
   * fails. It is caught by a caller function and NaN is returned. Such approach
   * is made to increase the speed of the calculator and avoid performing useless
   * computations.
   */
  static COULD_NOT_CALCULATE_EXCEPTION = 13;

  /**
   * Recursively calculates values for all nodes starting at “treeNode” where it
   * is possible and stores the result at node.value. This can be useful for
   * increasing the speed of further calculations.
   *
   * @param {MathTreeNode} expressionNode
   *    A MathTreeNode to be calculated. Can be any type of node.
   *
   * @param {Object} variables
   *    Variables passed.
   *
   * @param {Object} userSymbols
   *    Set of user constants and functions. Not in use now.
   *
   * @returns {number}
   *     Value of the expression or NaN if calculation failed.
   */
  calculateExpression(expressionNode, variables, userSymbols) {
    if (expressionNode instanceof Text) {
      expressionNode = expressionNode.tree.root;
    }

    if (!variables) {
      variables = {};
    }

    var result = Number.NaN;
    try {
      result = this.calculateValueOfTheNode(expressionNode, variables, userSymbols);
    } catch (e) {
      if (e != this.COULD_NOT_CALCULATE_EXCEPTION) {
        throw e;
      }
    }

    return result;
  }

  /**
   * Checks whether the given inequality is true. (Not implemented yet)
   * TODO implement
   *
   * @param {Object}
   *    inequalityNode
   * @param {Object}
   *    variables
   * @param {Object}
   *    userSymbols
   */
  //checkInequality(inequalityNode, variables, userSymbols) {
  //}

  /**
   * Recursively calculates values for all nodes starting at “treeNode” where it
   * is possible and stores the result at node.value. This can be useful for
   * increasing the speed of further calculations.
   *
   * @param {MathTreeNode} treeNode
   *    A MathTreeNode to be calculated. Can be any type of node.
   * @param {Object} variables
   *    Variables passed.
   * @param {Object} userSymbols
   *    Set of user constants and functions. Not in use now.
   */
  calculateCachedValues(treeNode, variables, userSymbols) {
    if (treeNode instanceof Text) {
      treeNode = treeNode.tree.root;
    }

    if (!variables) {
      variables = {};
    }

    try {
      this.calculateValueOfTheNode(treeNode, variables, userSymbols, true);
    } catch (e) {
      if (e != this.COULD_NOT_CALCULATE_EXCEPTION) {
        throw e;
      }
    }
  }

  /**
   * Removes all cashed values of the nodes. For instance, if a node contained
   * (pi-3) and its value was equal to 0.14, its value becomes undefined.
   *
   * @param {Object} treeNode
   *    Node to clear values in
   */
  clearCachedValues(treeNode) {
    if (treeNode instanceof Text) {
      treeNode = treeNode.tree.root;
    }

    // Recursively clear child nodes.
    if (treeNode.subNodes) {
      for (let i = treeNode.subNodes.length - 1; i >= 0; i--) {
        this.clearCachedValues(treeNode.subNodes[i]);
      }
    }

    // Delete a value of the current node in case it is not a number.
    if (treeNode.type != TreeNodeType.NUMBER) {
      delete treeNode.value;
    }
  }

  /**
   * Calculates the value of a node and depending on the mode returns it or saves
   * as the node “value” property.
   *
   * @param {MathTreeNode} treeNode
   *    A MathTreeNode to be calculated. Can be any type of node.
   * @param {Object} variables
   *    Variables passed.
   * @param {Object} symbols
   *    Set of constants and functions.
   * @param {boolean} calculateCacheMode
   *    If true, the result of the function will be saved as the node “value” property.
   * @returns {number|NaN}
   *      xxx
   */
  calculateValueOfTheNode(treeNode, variables, symbols, calculateCacheMode) {

    //FIXME revise
    //// Do nothing if the value is already calculated or this node is a number (it has already got a value).
    // if ((treeNode.value !== null && !calculateCacheMode) || treeNode.type == TreeNodeType.NUMBER)

    // Do nothing if a value is already known, just return it.
    if (treeNode.value !== undefined) {
      return treeNode.value;
    }

    // The result is meaningless by default.
    var result = null;

    // All values of sub-nodes must be kept in a separate array to be then passed
    // to an executor function or processed.
    var subNodesValues = new Array();

    // Detects if the function must return NaN if any of sub-nodes contains NaN.
    var failOnSubNodeNaN = true;

    // If is a special case. No need to return NaN if any of the arguments is NaN.
    if (treeNode.type == TreeNodeType.FUNCTION
        && symbols.functions[treeNode.id]
        && symbols.functions[treeNode.id].calculateForNaNs) {
      failOnSubNodeNaN = false;
    }

    // Calculate values for all children sub-nodes if there are any.
    if (treeNode.subNodes) {
      for (let i = treeNode.subNodes.length - 1; i >= 0; i--) {
        // Try to get the value of a sub-node and saving it.
        if (treeNode.subNodes[i].value === undefined) {
          // If failOnSubNodeNaN is false, calculations are made in
          // try/catch block to avoid lazy finish in case of a NaN.
          if (!failOnSubNodeNaN) {
            try {
              subNodesValues[i] = this.calculateValueOfTheNode(
                  treeNode.subNodes[i], variables,
                  symbols, calculateCacheMode
                );
            } catch (e) {
              if (e != this.COULD_NOT_CALCULATE_EXCEPTION) {
                throw e;
              }
            }
          } else {
            subNodesValues[i] = this.calculateValueOfTheNode(
                treeNode.subNodes[i], variables, symbols,
                calculateCacheMode);
          }
        } else {
          subNodesValues[i] = treeNode.subNodes[i].value;
        }

        // If any of sub-nodes has a non-numeric value and failOnSubNodeNaN
        // is not false, return.
        if (failOnSubNodeNaN
            && (subNodesValues[i] === undefined || subNodesValues[i] === null)) {
          return NaN;
        }
      }
    }

    // If the function cannot be simplified and now in calculate cache mode, return.
    if (calculateCacheMode
        && treeNode.type == TreeNodeType.FUNCTION
        && typeof (symbols.functions[treeNode.id]) != 'undefined'
        && symbols.functions[treeNode.id].nonSimplifiable) {
      return NaN;
    }

    var subNodesValuesCount = subNodesValues.length;

    /////////////////////////////////////
    // Calculate a value of the current node depending on its type.
    /////////////////////////////////////

    switch (treeNode.type) {

    /////////////////////////////////////
    // Node is a symbol
    case TreeNodeType.SYMBOL:
      // A symbol is a standard constant.
      if (treeNode.subType == TreeNodeType.STANDARD_CONSTANT) {
        if (typeof (symbols.constants[treeNode.id]) == 'undefined') {
          return NaN;
        }
        result = symbols.constants[treeNode.id].value;
        break;
      }

      // A symbol is a variable passed to the calculator.
      if (variables[treeNode.id] !== undefined) {
        result = variables[treeNode.id];
        break;
      }
      //FIXME check this
      break;

    /////////////////////////////////////
    // Node is a function
    case TreeNodeType.FUNCTION:

      // A function is standard.
      if (treeNode.subType == TreeNodeType.STANDARD_FUNCTION) {
        if (typeof (symbols.functions[treeNode.id]) != undefined) {
          return NaN;
        }
        result = symbols.functions[treeNode.id].executor(subNodesValues);
        break;
      }
      break;

    /////////////////////////////////////
    // Node is an expression
    case TreeNodeType.EXPRESSION:
      result = subNodesValues[0];
      for (let i = 1; i < subNodesValuesCount; i++) {
        if (treeNode.subActions[i - 1].type == TokenType.ADD) {
          result += subNodesValues[i];
        } else {
          result -= subNodesValues[i];
        }
      }
      break;

    /////////////////////////////////////
    // Node is a term
    case TreeNodeType.TERM:
      result = subNodesValues[0];
      for (let i = 1; i < subNodesValuesCount; i++) {
        if (treeNode.subActions[i - 1].type == TokenType.MULTIPLY) {
          result *= subNodesValues[i];
        } else {
          result /= subNodesValues[i];
        }
      }
      break;

    /////////////////////////////////////
    // Node is a power
    case TreeNodeType.POWER:
      result = subNodesValues[0];
      for (let i = 1; i < subNodesValuesCount; i++) {
        result = Math.pow(result, subNodesValues[i]);
      }
      break;
    }

    // Do something with an obtained result

    if (isNaN(result) && !calculateCacheMode) {
      throw this.COULD_NOT_CALCULATE_EXCEPTION;
    }

    // Saving node result if it we are in a “calculate cache” mode
    if (calculateCacheMode && (_.isNumber(result) && !isNaN(result))) {
      treeNode.value = result;
    }
    return result;
  }
}