-1

Now I can read data from a database and can map the data to a case class using a ORM the case class is like following

case class Example(a: Double, b: Double)

And I have a text file and this file contains lines of criteria(s) like "a > 3.7 and b < 67.2".

Now you want to read the criteria(s) from the file and apply them to the data I tied to the database to see if there are any data conforms any criteria. Is there any scala library to do this sort of things?

I searched the web and looked some libs like criteria(s) (https://index.scala-lang.org/eff3ct0/criteria4s) but still cannot solved this problem.

Can Anyone provide some solutions?

1

1 Answer 1

4

One possibility is for you to write a small parser that translates those snippets into an expression that you can evaluate at runtime.

First you would need to define the AST.

sealed trait AST
final case class Identifier(name: String) extends AST
final case class Number(value: Double) extends AST
final case class Bool(value: Boolean) extends AST
final case class Comparison(left: AST, op: String, right: AST) extends AST
final case class Logical(left: AST, op: String, right: AST) extends AST

Then you can use fastparse to define the parsing logic

def number[$: P]: P[Number] = P(CharsWhileIn("0-9.").!).map(s => Number(s.toDouble))
def boolean[$: P]: P[Bool] = P("true" | "false").!.map(s => Bool(s.toBoolean))
def identifier[$: P]: P[Identifier] = P(CharsWhileIn("a-zA-Z_").!).map(Identifier.apply)
def factor[$: P]: P[AST] = P(number | boolean | identifier)

def comparisonOp[$: P]: P[String] = P(">" | "<" | ">=" | "<=" | "==" | "!=").!
def comparison[$: P]: P[Comparison] =
  P(factor ~ " ".rep(1) ~ comparisonOp ~ " ".rep(1) ~ factor).map {
    case (left, op, right) => Comparison(left, op, right)
  }

def logicalOp[$: P]: P[String] = P("and" | "or").!
def logicalExpr[$: P]: P[AST] =
  P(comparison ~ (" ".rep(1) ~ logicalOp ~ " ".rep(1) ~ logicalExpr).?).map {
    case (left, None)              => left
    case (left, Some((op, right))) => Logical(left, op, right)
  }

def parseExpr(input: String): Parsed[AST] = {
  parse(input, logicalExpr(_))
}

You can use the output of the parser to evaluate the expression, passing your Example as the environment in which it operates.

def eval(ast: AST, context: Example): AST = ast match {
  case Identifier(name) =>
    name match {
      case "a" => Number(context.a)
      case "b" => Number(context.b)
      case _ => throw new IllegalArgumentException(s"Undefined variable: $name")
    }
  case Comparison(left, op, right) =>
    lazy val lhs = eval(left, context).asInstanceOf[Number].value
    lazy val rhs = eval(right, context).asInstanceOf[Number].value
    val result = op match {
      case ">"  => lhs > rhs
      case "<"  => lhs < rhs
      case ">=" => lhs >= rhs
      case "<=" => lhs <= rhs
      case "==" => lhs == rhs
      case "!=" => lhs != rhs
      case _    => throw new IllegalArgumentException(s"Unknown operator: $op")
    }
    Bool(result)
  case Logical(left, op, right) =>
    lazy val lhs = eval(left, context).asInstanceOf[Bool].value
    lazy val rhs = eval(right, context).asInstanceOf[Bool].value
    val result = op match {
      case "and"  => lhs && rhs
      case "or"  => lhs || rhs
      case _    => throw new IllegalArgumentException(s"Unknown operator: $op")
    }
    Bool(result)
  case literal => literal // literal, doesn't need further evaluation
}

Finally, you can put everything together by parsing the expression and giving it to the evaluation function:

parseExpr("a > 3.7 and b < 67.2") match {
  case Parsed.Success(ast, _) =>
    try {
      assert(eval(ast, Example(a = 5.0, b = 60.0)) == Bool(true))
      assert(eval(ast, Example(a = 5.0, b = 68.0)) == Bool(false))
    } catch {
      case e =>
        println(s"Evaluation error: ${e.getMessage}")
    }

  case f: Parsed.Failure =>
    println("Parsing failed.")
    println(f.longMsg)
}

This is a very barebones implementation which skips some important bits, like properly handling runtime errors (such as type errors, which with this toy implementation will pop up as ClassCastExceptions). Furthermore there's plenty of improvements that can be made to the overall implementation, this is mostly meant as a starting point.

You can play around with this code here on Scastie.

For more info on fastparse, here are the docs.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.