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

com.phasmidsoftware.number.parse.ShuntingYardParser.scala Maven / Gradle / Ivy

package com.phasmidsoftware.number.parse

import com.phasmidsoftware.number.core._
import com.phasmidsoftware.number.mill._
import scala.annotation.tailrec
import scala.util.Try

/**
  * Parser for a Mill.
  * The input to this parser is a string of tokens, each separated by any amount of white space.
  * The input is then converted to an IndexedSeq of Strings which is then reversed and rendered as a single String.
  * The purpose of this strange little dance is to reverse the order of the tokens without reversing the tokens themselves.
  *
  * For detail of the method, please see https://en.wikipedia.org/wiki/Shunting-yard_algorithm
  *
  * CONSIDER using a TokenParser instead of a RegexParser.
  */
object ShuntingYardParser extends BaseMillParser {

  /**
    * Parse the string w as an infix expression.
    * The elements of the input include numbers, and various operators.
    *
    * @param w the String to parse.
    * @return a Mill, wrapped in Try.
    */
  def parseInfix(w: String): Try[Mill] = stringParser(shuntingYard, w).flatMap(_.toMill)

  /**
    * A ShuntingYard consisting of two structures: a queue of values and a stack of operators.
    *
    * NOTE: apparently, this is never used!
    *
    * @param values    a list of values and operators. The former are added directly to values.
    *                  However, operators are only added indirectly, after a closing parenthesis
    *                  (or the end of the string) is reached.
    * @param operators a stack of operators which is temporarily placed here until switch is called.
    */
  case class ShuntingYard(values: Seq[Item], operators: Seq[Item]) {

    /**
      * Method to add a token to this ShuntingYard.
      *
      * @param token the token to add.
      * @return a new ShuntingYard which is the same as this but with token added.
      *         If token is an operator, it is added to the operators;
      *         If token is a number, it is added to the values;
      *         If token is a open or close parenthesis, it is handled specially.
      */
    def :+(token: InfixToken): ShuntingYard = token match {
      case InfixToken(Some(t), _) => t match {
        case Left(operator) => this :+ operator
        case Right(number) => this :+ number
      }
      case InfixToken(None, x) =>
        if (x) this :+ openParenthesis // open parenthesis
        else switch // close parenthesis
    }

    /**
      * Convert this ShuntingYard into a Mill.
      * Any remaining operators must first be switched to the values stack.
      *
      * @return a Try[Mill].
      */
    def toMill: Try[Mill] = switch match {
      case ShuntingYard(values, Nil) => Try(Mill(values: _*))
      case x => scala.util.Failure(MillException(s"toMill: logic error with switch value (usually mis-matched parentheses): $x"))
    }

    private def :+(number: Number) = ShuntingYard(values :+ Expr(Expression(Real(number))), operators)

    @tailrec
    private def :+(operator: String): ShuntingYard = Item(operator) match {
      case o1@Dyadic(_, _) => operators match {
        case Nil => ShuntingYard(values, o1 +: Nil)
        case Open :: xs => ShuntingYard(values, o1 :: Open :: xs)
        case op +: xs => op match {
          case o2@Dyadic(_, _) =>
            if (implicitly[Ordering[Dyadic]].compare(o1, o2) < 0)
              ShuntingYard(values :+ o2, xs) :+ operator
            else
              ShuntingYard(values, o1 +: o2 +: xs)
          case _ => ShuntingYard(values, op +: xs)
        }
      }
      case op => ShuntingYard(values, op +: operators)
    }

    @tailrec
    private def switch: ShuntingYard =
      this match {
        case s@ShuntingYard(_, Nil) => s
        case ShuntingYard(values, Open :: tail) => ShuntingYard(values, tail)
        case ShuntingYard(values, operator :: operators) => ShuntingYard(values :+ operator, operators).switch
        case _ => throw MillException("ShuntingYard.switch: logic error (probably non-matching parentheses)")
      }
  }

  object ShuntingYard {
    /**
      * Create a new, empty, ShuntingYard.
      *
      * @return a new, empty, ShuntingYard.
      */
    def apply(): ShuntingYard = ShuntingYard(Nil, Nil)

    /**
      * Create a new ShuntingYard with the tokens added.
      *
      * @param tokens a list of InfixToken.
      * @return
      */
    def apply(tokens: Seq[InfixToken]): ShuntingYard = tokens.foldLeft(ShuntingYard())(_ :+ _).switch
  }

  /**
    * An infix token which represents either a Token or an open/close parenthesis.
    *
    * @param to    an optional token.
    * @param paren the to is None, then paren is interpreted as Open (for true) and Close (for false).
    */
  case class InfixToken(to: Option[Token], paren: Boolean)

  /**
    * Parser for a shunting yard.
    * It matches on a list of infixTokens separated by white space.
    *
    * @return a Parser[ShuntingYard].
    */
  def shuntingYard: Parser[ShuntingYard] = repSepSp(infixToken) :| "shuntingYard" ^^ (tokens => ShuntingYard(tokens))

  /**
    * Parser for an InfixToken.
    * It matches on a Number, or an operator--including ( and ).
    *
    * @return a Parser[InfixToken].
    */
  private def infixToken: Parser[InfixToken] = (maybeNumber ?| operator) :| "infixToken" ^^ {
    case Right(x) => InfixToken(Some(Right(x)), paren = false)
    case Left("(") => InfixToken(None, paren = true)
    case Left(")") => InfixToken(None, paren = false)
    case Left(w) => InfixToken(Some(Left(w)), paren = false)
  }

  /**
    * Parser for a String.
    * It matches on one of the operators defined in MillParser or an open or close parenthesis.
    *
    * @return Parser[String].
    */
  def operator: Parser[String] = (dyadicOperator | monadicOperator | anadicOperator | neutralOperator2 | openParenthesis | closeParenthesis) :| "operator"

  private val openParenthesis: String = "("

  private val closeParenthesis: String = ")"
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy