Recently I was looking for a decent grammar for arithmetic expressions but found only trivial ones, ignoring pow(..., ...)
for example. Then I tried it on my own, but sometimes it didn´t worked as one expects. For example, I missed to allow a unary -
in front of expressions and fixed it. Perhaps someone can take a look at my current approach and improve it. Furthermore I think others can take advantage because it´s a common task to be able to parse arithmetic expressions.
import scala.math._
import scala.util.parsing.combinator._
import scala.util.Random
class FormulaParser(val constants: Map[String,Double] = Map(), val userFcts: Map[String,String => Double] = Map(), random: Random = new Random) extends JavaTokenParsers {
require(constants.keySet.intersect(userFcts.keySet).isEmpty)
private val allConstants = constants ++ Map("E" -> E, "PI" -> Pi, "Pi" -> Pi) // shouldn´t be empty
private val unaryOps: Map[String,Double => Double] = Map(
"sqrt" -> (sqrt(_)), "abs" -> (abs(_)), "floor" -> (floor(_)), "ceil" -> (ceil(_)), "ln" -> (math.log(_)), "round" -> (round(_)), "signum" -> (signum(_))
)
private val binaryOps1: Map[String,(Double,Double) => Double] = Map(
"+" -> (_+_), "-" -> (_-_), "*" -> (_*_), "/" -> (_/_), "^" -> (pow(_,_))
)
private val binaryOps2: Map[String,(Double,Double) => Double] = Map(
"max" -> (max(_,_)), "min" -> (min(_,_))
)
private def fold(d: Double, l: List[~[String,Double]]) = l.foldLeft(d){ case (d1,op~d2) => binaryOps1(op)(d1,d2) }
private implicit def map2Parser[V](m: Map[String,V]) = m.keys.map(_ ^^ (identity)).reduceLeft(_ | _)
private def expression: Parser[Double] = sign~term~rep(("+"|"-")~term) ^^ { case s~t~l => fold(s * t,l) }
private def sign: Parser[Double] = opt("+" | "-") ^^ { case None => 1; case Some("+") => 1; case Some("-") => -1 }
private def term: Parser[Double] = longFactor~rep(("*"|"/")~longFactor) ^^ { case d~l => fold(d,l) }
private def longFactor: Parser[Double] = shortFactor~rep("^"~shortFactor) ^^ { case d~l => fold(d,l) }
private def shortFactor: Parser[Double] = fpn | sign~(constant | rnd | unaryFct | binaryFct | userFct | "("~>expression<~")") ^^ { case s~x => s * x }
private def constant: Parser[Double] = allConstants ^^ (allConstants(_))
private def rnd: Parser[Double] = "rnd"~>"("~>fpn~","~fpn<~")" ^^ { case x~_~y => require(y > x); x + (y-x) * random.nextDouble } | "rnd" ^^ { _ => random.nextDouble }
private def fpn: Parser[Double] = floatingPointNumber ^^ (_.toDouble)
private def unaryFct: Parser[Double] = unaryOps~"("~expression~")" ^^ { case op~_~d~_ => unaryOps(op)(d) }
private def binaryFct: Parser[Double] = binaryOps2~"("~expression~","~expression~")" ^^ { case op~_~d1~_~d2~_ => binaryOps2(op)(d1,d2) }
private def userFct: Parser[Double] = userFcts~"("~(expression ^^ (_.toString) | ident)<~")" ^^ { case fct~_~x => userFcts(fct)(x) }
def evaluate(formula: String) = parseAll(expression,formula).get
}
So one can evaluate the following:
val formulaParser = new FormulaParser(
constants = Map("radius" -> 8D,
"height" -> 10D,
"c" -> 299792458, // m/s
"v" -> 130 * 1000 / 60 / 60, // 130 km/h in m/s
"m" -> 80),
userFcts = Map("perimeter" -> { _.toDouble * 2 * Pi } ))
println(formulaParser.evaluate("2+3*5")) // 17.0
println(formulaParser.evaluate("height*perimeter(radius)")) // 502.6548245743669
println(formulaParser.evaluate("m/sqrt(1-v^2/c^2)")) // 80.00000000003415
Any improvement suggestions? Do I use the right grammar or is it only a question of time until a user types in a valid (with respect to my provided functions) arithmetic expression that can´t be parsed?
(What´s about operator precedence?)
In computer science, a parsing expression grammar (PEG) is a type of analytic formal grammar, i.e. it describes a formal language in terms of a set of rules for recognizing strings in the language.
An arithmetic expression is an expression built up using numbers, arithmetic operators (such as +, , -, / and ) and parentheses, "(" and ")". Arithmetic expressions may also make use of exponents, for example, writing 23as an abreviation for ((2 2) 2).
Parser GeneratorsThey take in a grammar as input and produce Java code to parse input. And they can handle more grammars than a recursive descent parser can. There is much more to building parsers than we can cover in this course.
There are three kinds of expressions: An arithmetic expression evaluates to a single arithmetic value. A character expression evaluates to a single value of type character. A logical or relational expression evaluates to a single logical value.
For a better performance I suggest to use private lazy val
instead of private def
when defining parsers. Otherwise whenever a parser is references it is created again.
Nice code BTW.
Well maybe add variables in the loop :
import scala.math._
import scala.util.parsing.combinator._
import scala.util.Random
class FormulaParser(val variables: Set[String] = Set(),
val constants: Map[String, Double] = Map(),
val unary: Map[String, Double => Double] = Map(),
val binary: Map[String, (Double, Double) => Double] = Map(),
val userFcts: Map[String, String => Double] = Map(),
random: Random = new Random) extends JavaTokenParsers {
require(constants.keySet.intersect(userFcts.keySet).isEmpty)
private val allConstants = constants ++ Map("E" -> E, "PI" -> Pi, "Pi" -> Pi)
// shouldn´t be empty
private val unaryOps = Map[String, Double => Double](
"sqrt" -> (sqrt(_)), "abs" -> (abs(_)), "floor" -> (floor(_)), "ceil" -> (ceil(_)), "ln" -> (math.log(_)), "round" -> (round(_).toDouble), "signum" -> (signum(_))
) ++ unary
private val binaryOps1 = Map[String, (Double, Double) => Double](
"+" -> (_ + _), "-" -> (_ - _), "*" -> (_ * _), "/" -> (_ / _), "^" -> (pow(_, _))
)
private val binaryOps2 = Map[String, (Double, Double) => Double](
"max" -> (max(_, _)), "min" -> (min(_, _))
) ++ binary
type Argument = Map[String, Double]
type Formula = Argument => Double
private def fold(d: Formula, l: List[~[String, Formula]]) = l.foldLeft(d) { case (d1, op ~ d2) => arg => binaryOps1(op)(d1(arg), d2(arg))}
private implicit def set2Parser[V](s: Set[String]) = s.map(_ ^^ identity).reduceLeft(_ | _)
private implicit def map2Parser[V](m: Map[String, V]) = m.keys.map(_ ^^ identity).reduceLeft(_ | _)
private def expression: Parser[Formula] = sign ~ term ~ rep(("+" | "-") ~ term) ^^ { case s ~ t ~ l => fold(arg => s * t(arg), l)}
private def sign: Parser[Double] = opt("+" | "-") ^^ { case None => 1; case Some("+") => 1; case Some("-") => -1}
private def term: Parser[Formula] = longFactor ~ rep(("*" | "/") ~ longFactor) ^^ { case d ~ l => fold(d, l)}
private def longFactor: Parser[Formula] = shortFactor ~ rep("^" ~ shortFactor) ^^ { case d ~ l => fold(d, l)}
private def shortFactor: Parser[Formula] = fpn | sign ~ (constant | variable | rnd | unaryFct | binaryFct | userFct | "(" ~> expression <~ ")") ^^ { case s ~ x => arg => s * x(arg)}
private def constant: Parser[Formula] = allConstants ^^ (name => arg => allConstants(name))
private def variable: Parser[Formula] = variables ^^ (name => arg => arg(name))
private def rnd: Parser[Formula] = "rnd" ~> "(" ~> fpn ~ "," ~ fpn <~ ")" ^^ { case x ~ _ ~ y => (arg: Argument) => require(y(arg) > x(arg)); x(arg) + (y(arg) - x(arg)) * random.nextDouble} | "rnd" ^^ { _ => arg => random.nextDouble}
private def fpn: Parser[Formula] = floatingPointNumber ^^ (value => arg => value.toDouble)
private def unaryFct: Parser[Formula] = unaryOps ~ "(" ~ expression ~ ")" ^^ { case op ~ _ ~ d ~ _ => arg => unaryOps(op)(d(arg))}
private def binaryFct: Parser[Formula] = binaryOps2 ~ "(" ~ expression ~ "," ~ expression ~ ")" ^^ { case op ~ _ ~ d1 ~ _ ~ d2 ~ _ => arg => binaryOps2(op)(d1(arg), d2(arg))}
private def userFct: Parser[Formula] = userFcts ~ "(" ~ (expression ^^ (_.toString) | ident) <~ ")" ^^ { case fct ~ _ ~ x => arg => userFcts(fct)(x)}
def evaluate(formula: String) = parseAll(expression, formula).get
}
So now you have to pass a map to evaluate and you can do :
val formulaParser = new FormulaParser(Set("x"), unary = Map(
"sin" -> (math.sin(_)), "cos" -> (math.cos(_)), "tan" -> (math.tan(_))
))
val formula = formulaParser.evaluate("sin(x)^x")
val function: Double => Double = x => formula(Map("x" -> x))
println(function(5.5))
As you can see, I also added parameters to add unary and binary functions.
Thanks for that good code by the way!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With