Грамматика и синтаксический анализатор арифметических выражений

Недавно я искал подходящую грамматику для арифметических выражений, но нашел только тривиальные, например, игнорируя pow(..., ...). Затем я попробовал это самостоятельно, но иногда это не срабатывало, как ожидалось. Например, я упустил возможность использовать унарный - перед выражениями и исправил это. Возможно, кто-нибудь сможет взглянуть на мой нынешний подход и улучшить его. Более того, я думаю, что другие могут воспользоваться этим, потому что это обычная задача - уметь разбирать арифметические выражения.

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
}

Таким образом, можно оценить следующее:

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

Есть предложения по улучшению? Использую ли я правильную грамматику или это только вопрос времени, пока пользователь не введет допустимое (по отношению к моим предоставленным функциям) арифметическое выражение, которое не может быть проанализировано?
(Что насчет приоритета операторов?)


person Peter Schmitz    schedule 27.04.2011    source источник
comment
Например: userFct, начинающийся с E, выдает ошибку синтаксического анализа, потому что math.E совпал раньше. Как я могу предотвратить это или как правильно комбинировать Parser[Double] с |?   -  person Peter Schmitz    schedule 27.04.2011
comment
Это очень хороший код @Peter Schmitz. Вы должны поместить его в библиотеку на Github, тогда я могу дать вам свои улучшения. Я использую это как отправную точку для проекта, над которым работаю.   -  person Jason    schedule 15.07.2013
comment
@ Джейсон Спасибо. Когда позволит время, я опубликую его на Github, но вы можете сделать это и использовать мой код со своими улучшениями. Я с нетерпением жду улучшений, потому что все еще задаюсь вопросом, правильная ли грамматика.   -  person Peter Schmitz    schedule 16.07.2013
comment
@PeterSchmitz неплохо читал. Я впервые прочитал этот код. и я думаю, что до сих пор нет репозитория github. Некоторая документация могла бы быть полезной. большая часть грамматики довольно ясна, но мне потребовалось время, чтобы выяснить, где находятся ваши терминалы (на самом деле). это особенно актуально для людей, которые не знакомы с dsl парсинга scala.   -  person Alexander Oh    schedule 12.02.2016
comment
@Alex, годы спустя, у меня такие же проблемы. Прости. По-прежнему нет репо, потому что у меня не было времени отполировать, задокументировать и поместить все в репо. Пожалуйста, не стесняйтесь делать это и улучшать! Я до сих пор не уверен, правильная ли это грамматика.   -  person Peter Schmitz    schedule 15.02.2016
comment
@PeterSchmitz Честно говоря, я бы сделал парсинг по-другому. быстрее не иметь AST, но наличие AST делает это намного понятнее. в любом случае чтение было проницательным, так как я только начинаю scala.   -  person Alexander Oh    schedule 15.02.2016
comment
Как вы узнали, что map2Parser будет создавать парсеры, если вы сделали ^^ identity?   -  person Adrian    schedule 13.01.2017
comment
Извините, это было много лет назад, я больше не занимаюсь этим кодом. Возможно, я бы все равно использовал другую библиотеку синтаксического анализа, например fastparse или parboiled2.   -  person Peter Schmitz    schedule 14.01.2017


Ответы (2)


Для лучшей производительности я предлагаю использовать private lazy val вместо private def при определении парсеров. В противном случае всякий раз, когда парсер ссылается, он создается снова.

Хороший код, кстати.

person Stefan Endrullis    schedule 27.04.2011
comment
Вы правы, спасибо. Я пробовал это при кодировании, но он не работал при запуске парсера на нескольких входах. Я предположил, что синтаксический анализатор можно использовать только один раз, поэтому я выбрал def аналогично книге «Программирование в Scala». Теперь работает с lazy val. - person Peter Schmitz; 27.04.2011

Ну, может, добавим в цикл переменные:

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
}

Итак, теперь вам нужно передать карту для оценки, и вы можете:

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))

Как видите, я также добавил параметры для добавления унарных и двоичных функций.

Кстати, спасибо за этот хороший код!

person Didier Villevalois    schedule 05.12.2014