All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.barrybecker4.expression.mathexpression.MathExpressionParser.scala Maven / Gradle / Ivy

The newest version!
/* Copyright by Barry G. Becker, 2000-2018. Licensed under MIT License: http://www.opensource.org/licenses/MIT */
package com.barrybecker4.expression.mathexpression

import com.barrybecker4.expression.{LEFT_PAREN, RIGHT_PAREN, _}

import scala.collection.mutable.ListBuffer


/**
  * Parses the text form of an expression (in x) into a tree representation.
  * Create a subclass for parsing a math expression.
  * @author Barry Becker
  */
class MathExpressionParser(opDef: OperatorsDefinition = new MathOperatorsDefinition()) extends ExpressionParser(opDef) {

  /** Recursive method to find all the tree nodes for the terms at the current level.
    * For example, given this expression
    * 2x^3 +  5(x + 3x^2) / (x - 1)
    * the items in []'s represent the array of nodes returned.
    * [2] [*] [x] [^] [3] [+] [5][*][x + 3x^2] [/] [x - 1]
    * The parts that were in ()'s become their own subtrees via recursive calls.
    * Throws an Error if there is a syntax error causing the expression to be invalid
    *
    * @param exp the expression to get the nodes at the current level for
    * @return array of nodes representing terms that the current level.
    */
  override protected def getNodesAtLevel(exp: String): ListBuffer[TreeNode] = {
    var pos = 0
    val nodes = ListBuffer[TreeNode]()
    var token = ""
    var ch = exp.charAt(pos)
    while (pos < exp.length && !(token == "" + RIGHT_PAREN.symbol)) {
      if (ch == ' ') {
        // spaces are ignored
      }
      else if (ch == LEFT_PAREN.symbol) {
        val closingParenPos = findClosingParen(exp, pos + 1)
        // this method will make the recursive call
        token = processSubExpression(exp, pos + 1, token, closingParenPos, nodes)
        pos = closingParenPos + 1
      }
      else if (ch == MINUS.symbol && token.length == 0 && opDef.isLastNodeOperator(nodes)) {
        // a leading minus sign
        token += ch
      }
      else if (isNumericChar(ch)) {
        token += ch
        if (token.contains("x")) {
          throw new Error("Cannot have numbers after x in a term " + token + " within " + exp)
        }
      }
      else if (ch == 'x') token += ch
      else if (opDef.isOperator(ch)) {
        pushNodesForToken(token, nodes)
        token = ""
        nodes.append(new TreeNode(ch, opDef))
      }
      else throw new Error("Unexpected character '" + ch + "' in expression: " + exp)
      pos += 1
      if (pos < exp.length) ch = exp.charAt(pos)
    }
    // add the final node
    pushNodesForToken(token, nodes)
    nodes
  }

  /** Parse a parenthesized sub expression recursively.
    * @return current token. May have been reset to "".
    */
  override protected def processSubExpression(exp: String, pos: Int,
                                              token: String, closingParenPos: Int,
                                              nodes: ListBuffer[TreeNode]): String = {
    val subExp = exp.substring(pos, closingParenPos)
    // recursive call for sub expression
    val subTree = parse(subExp)
    var resultToken = token
    subTree.hasParens = true
    if (token != null && token.length > 0) { // there was some leading token before the parenthesized expression.
      pushNodesForToken(token, nodes)
      resultToken = ""
      nodes.append(new TreeNode(TIMES.symbol, opDef))
    }
    else if (nodes.nonEmpty && nodes.last.hasParens)
      nodes.append(new TreeNode(TIMES.symbol, opDef))
    nodes.append(subTree)
    resultToken
  }

  /**
    * The token may represent several nodes because of implicit multiplication.
    * For example,
    * -4x should become  [-4] [times] [x]
    * -x should become [-1] [times] [x]
    * @param token the token to parse
    * @param nodes array of nodes that the token was parsed into.
    */
  override protected def pushNodesForToken(token: String, nodes: ListBuffer[TreeNode]): Unit = {
    if (token == null || token.length == 0) return
    val len = token.length
    if (token.charAt(len - 1) == 'x') {
      if (len > 1) {
        if (token.charAt(0) == MINUS.symbol) nodes.append(new TreeNode("-1", opDef))
        else nodes.append(getNodeForNumber(token.substring(0, len - 1)))
        nodes.append(new TreeNode(TIMES.symbol, opDef))
      }
      nodes.append(new TreeNode("x", opDef))
    }
    else nodes.append(getNodeForNumber(token))
  }

  /** Converts a list of nodes to a single node by reducing them to
    * subtrees in order of operator precedence.
    */
  override protected def makeTreeFromNodes(theNodes: ListBuffer[TreeNode]): TreeNode = {
    var nodes = theNodes
    for (ops <- opDef.getOperatorPrecedence) {
      //System.out.println("nodes=" + nodes + " ops=" + ops.mkString(", "))
      nodes = reduceNodes(ops, nodes)
    }
    if (nodes.size > 1)
      throw new Error("Expected to have only one node after reducing, but have " + nodes.size + " : " + nodes)
    if (nodes.head.isOperator && nodes.head.children.isEmpty)
      throw new Error("Missing operands")
    nodes.head
  }

  /** Simplify the list of terms by evaluating the terms joined by the specified operators.
    * Reduce the nodes list to a single node and return it.
    * @param ops   list of operators that all have the same precedence.
    * @param nodes the list of nodes to reduce
    * @return same list of nodes, but reduced.
    */
  private def reduceNodes(ops: Array[Operator], nodes: ListBuffer[TreeNode]) = {
    var index = 1
    if (nodes.size == 2) throw new Error("Missing operand : " + nodes)
    while (index < nodes.size) if (isOperator(nodes(index), ops)) {
      nodes(index).children = Seq(nodes(index - 1), nodes(index + 1))
      if (nodes.size < index + 1) throw new Error("Not enough operands for operator in nodes=" + nodes)
      //System.out.println("before splice : " + nodes);
      splice(nodes, index - 1, 3, nodes(index))
      //System.out.println("after splice : " + nodes);
    }
    else index += 2
    nodes
  }

  private def isNumericChar(ch: Char) = (ch >= '0' && ch <= '9') || ch == '.'

  private def getNodeForNumber(token: String) = {
    val num: Double = token.toDouble
    if (num.isNaN) throw new IllegalStateException("Invalid number in expression: " + token)
    new TreeNode("" + num, opDef)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy