How to parse a string with filter criteria in scal

2019-08-01 13:29发布

Let's say I have an array of objects that contains some String, Integer and Enum values. And also contains arrays of these types and methods that return these types.

For example an array containing the following ExampleObject:

object WeekDay extends Enumeration { type WeekDay = Value; val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value  }

class ExampleObject (val integerValue1 : Integer, val integerValue2 : Integer, val stringValue1 : String, val weekDay: WeekDay.Value, val integerArray : Array[Integer])
{  def intReturningMethod1()= {0}  }  

From the command line I pass in a string with filter criteria to the scala application. For example:

-filter_criteria "((integerValue1 > 100 || integerValue2 < 50) && (stringValue1 == "A" || weekDay != "Mon")) || (integerArray(15) == 1) "

The operators should do what you expect in a normal if statement with these types of values.

How can I parse the filter criteria string and use it to filter ExampleObjects from an array?

Or where should I start reading to find out how to do this?

2条回答
Emotional °昔
2楼-- · 2019-08-01 13:59

You might want to have a look at Twitter's Eval utility library, which you can find here on GitHub. You could just substitute the passed in filtering logic into a String at the point you want to use it and pass it to the eval function.

查看更多
老娘就宠你
3楼-- · 2019-08-01 14:04

If you want to restrict the input to a limited language, you can easily create a parser for that language using only the Scala core library.

I have done this for a stripped down version of your example

  object WeekDay extends Enumeration {
    type WeekDay = Value; val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
  }

  case class ExampleObject(val integerValue1 : Integer, val stringValue1 : String, val weekDay: WeekDay.Value){
    def intReturningMethod1()= {0}
  }

First I use an import and create some helpers:

  type FilterCriterion = ExampleObject => Boolean
  type Extractor[T] = ExampleObject => T

  def compare[T <% Ordered[T]](v1 : T, c : String, v2 : T) : Boolean = c match {
    case "<" => v1 < v2
    case ">" => v1 > v2
    case "==" => v1 == v2
  }

  def compareAny(v1: Any, c : String, v2 : Any) : Boolean = (v1,v2) match {
    case (s1: String, s2:String) => compare(s1,c,s2)
    case (i1: Int, i2 : Int) => compare(i1,c,i2)
    case (w1 : WeekDay.WeekDay, w2 : WeekDay.WeekDay) => compare(w1.id, c, w2.id)
    case _ => throw new IllegalArgumentException(s"Cannot compare ${v1.getClass} with ${v2.getClass}")
  }

Then I create the parser:

  object FilterParser extends JavaTokenParsers {
    def intExtractor : Parser[Extractor[Int]] = wholeNumber ^^ {s => Function.const(s.toInt)_} |
      "intReturningMethod1()" ^^^ {(e : ExampleObject) => e.intReturningMethod1()}  |
      "integerValue1" ^^^ {_.integerValue1}
    def stringExtractor : Parser[Extractor[String]] = stringLiteral ^^ {s => Function.const(s.drop(1).dropRight(1))_} |
      "stringValue1" ^^^ {_.stringValue1}
    def weekDayExtrator : Parser[Extractor[WeekDay.WeekDay]] = stringLiteral ^? {
      case s if WeekDay.values.exists(_.toString == s) => Function.const(WeekDay.withName(s))_
    }
    def extractor : Parser[Extractor[Any]] = intExtractor | stringExtractor | weekDayExtrator

    def compareOp : Parser[FilterCriterion] = (extractor ~ ("<"| "==" | ">") ~ extractor) ^^ {
      case v1 ~ c ~ v2 => (e : ExampleObject) => compareAny(v1(e),c,v2(e))
    }

    def simpleExpression : Parser[FilterCriterion] = "(" ~> expression <~ ")" | compareOp
    def notExpression : Parser[FilterCriterion] = "!" ~> simpleExpression ^^ {(ex) => (e : ExampleObject) => !ex(e)} |
      simpleExpression
    def andExpression : Parser[FilterCriterion] = repsep(notExpression,"&&") ^^ {(exs) => (e : ExampleObject) => exs.foldLeft(true)((b,ex)=> b && ex(e))}
    def orExpression : Parser[FilterCriterion] = repsep(andExpression,"||") ^^ {(exs) => (e : ExampleObject) => exs.foldLeft(false)((b,ex)=> b || ex(e))}
    def expression : Parser[FilterCriterion] = orExpression

    def parseExpressionString(s : String) = parseAll(expression, s)
  }

This parser takes your input string and returns a function that maps an ExampleObject to a boolean value. This test function is constructed once while parsing the input string, using the pre-defined helper functions and the anonymous functions defined in the parser rules. The interpretation of the input string is only done once, while constructing the test function. When you execute the test function, you will run compiled Scale code. So it should run quite fast.

The test function is safe, because it does not allow the user to run arbitrary Scala code. It will just be constructed from the partial function provided in the parser and the pre-defined helpers.

  val parsedCriterion=FilterParser.parseExpressionString("""((integerValue1 > 100 || integerValue1 < 50) && (stringValue1 == "A"))""")

  List(ExampleObject(1,"A", WeekDay.Mon), ExampleObject(2,"B", WeekDay.Sat), ExampleObject(50,"A", WeekDay.Mon)).filter(parsedCriterion.get)

You can easily extend the parser yourself, when you want it to use more functions or more fields in your ExampleObject.

查看更多
登录 后发表回答