我们如何评估 Kotlin 中由字符串表示的布尔表达式?

How can we evaluate a boolean expression represented by a String in Kotlin?

val exp = "( 0 == 1 && 10 > 11 ) || ( 10 < 9 ) && ( 10 == 10)"
val result: Boolean = evaluate(exp) //result = true/false

如何在 Android (Kotlin) 中编写一个简单的程序来评估上述字符串并获得布尔结果?我不想使用像 JEL or JEval, Js Eval or any other library 这样的完整求值器,因为它们对于这个特定要求来说太大了。

Preconditions :
Operators supported : < > == && || 
Works only on digits

也不想用ScriptEngineManager()

注意:javax.script 软件包在 Android 上不可用。

Android 上没有内置选项。您必须使用一些第 3 方解释器或编写自己的解释器。

我最终使用了 JEXL:它非常小(约 400KB jar)并且易于使用。

Gradle 依赖关系:

compile 'org.apache.commons:commons-jexl3:3.1'

用法(我创建的包装器):

class ExpressionEvaluator {
    private val jexlEngine = JexlBuilder().create()
    private val jexlContext = MapContext()

    fun evaluate(expression: String): Any? = try {
        val jexlExpression = jexlEngine.createExpression(expression)
        jexlExpression.evaluate(jexlContext)
    } catch (e: JexlException) {
        Timber.w("Could not evaluate expression '$expression'") //just logs
        null
    }

    fun evaluateAsBoolean(expression: String): Boolean? {
        val boolean = evaluate(expression) as? Boolean
        if (boolean == null) {
            Timber.w("Could not evaluate expression '$expression' as Boolean") //just logs
        }
        return boolean
    }
}

以下解析器是 100% 独立的,不需要任何库。当然它可以通过 1000 种方式和一种方式进行改进,但它显示了原理(并且写起来很有趣)。

sealed class Token
data class BooleanComposition(val c: Char) : Token()
data class NumberComparison(val c: Char) : Token()
data class Number(val value: Int) : Token()
object ParenLeft : Token()
object ParenRight : Token()
object EOF : Token()

sealed class Expression
data class BooleanTerm(val numExpr: NumberExpression) : Expression()
data class BooleanExpression(val b1: Expression, val cmp: (Boolean, Boolean) -> Boolean, val b2: Expression) : Expression()
data class NumberExpression(val n1: Int, val cmp: (Int, Int) -> Boolean, val n2: Int) : Expression()

fun and(b1: Boolean, b2: Boolean) = b1 && b2
fun or(b1: Boolean, b2: Boolean) = b1 || b2
fun lessThan(n1: Int, n2: Int): Boolean = n1 < n2
fun greaterThan(n1: Int, n2: Int): Boolean = n1 > n2
fun equal(n1: Int, n2: Int): Boolean = n1 == n2

fun scan(inputString: String): List<Token> = sequence {
    var index = 0
    while (index < inputString.length) {
        val c = inputString[index]
        if (c == '&' || c == '|' || c == '=') {
            check(inputString[++index] == c)
        }
        when (c) {
            in '0'..'9' -> {
                var end = index
                while (end < inputString.length && inputString[end].isDigit()) {
                    end += 1
                }
                val numToken = Number(inputString.substring(index, end).toInt())
                index = end - 1
                yield(numToken)
            }
            '&', '|' -> yield(BooleanComposition(c))
            '=', '<', '>' -> yield(NumberComparison(c))
            '(' -> yield(ParenLeft)
            ')' -> yield(ParenRight)
            else -> check(c.isWhitespace())
        }
        index += 1
    }
    yield(EOF)
}.toList()

fun parse(inputTokens: List<Token>): Expression {
    var index = 0

    fun expression(): Expression {
        val lastExpression = when (val firstToken = inputTokens[index++]) {
            is ParenLeft -> {
                val nestedExpression = expression()
                val closingToken = inputTokens[index++]
                check(closingToken is ParenRight) { "Found $closingToken" }
                nestedExpression
            }
            is Number -> {
                val opToken = inputTokens[index++]
                check(opToken is NumberComparison)
                val op = when (opToken.c) {
                    '<' -> ::lessThan
                    '>' -> ::greaterThan
                    '=' -> ::equal
                    else -> error("Bad op $opToken")
                }
                val secondToken = inputTokens[index++]
                check(secondToken is Number)
                NumberExpression(firstToken.value, op, secondToken.value)
            }
            else -> error("Parse error on $firstToken")
        }

        return when (val lookAhead = inputTokens[index]) {
            is EOF, is ParenRight -> lastExpression // pushback
            is BooleanComposition -> {
                // use lookAhead
                index += 1
                val op = when (lookAhead.c) {
                    '&' -> ::and
                    '|' -> ::or
                    else -> error("Bad op $lookAhead")
                }
                val secondExpression = expression()
                BooleanExpression(lastExpression, op, secondExpression)
            }
            else -> error("Parse error on $lookAhead")
        }
    }

    return expression()
}

fun evaluate(expr: Expression): Boolean = when (expr) {
    is BooleanTerm -> evaluate(expr.numExpr)
    is BooleanExpression -> expr.cmp.invoke(evaluate(expr.b1), evaluate(expr.b2))
    is NumberExpression -> expr.cmp.invoke(expr.n1, expr.n2)
}

fun main() {
    // scan . parse . evaluate ("( 0 == 1 && 10 > 11 ) || ( 10 < 9 ) && ( 10 == 10)")
    val soExample = evaluate(parse(scan("( 0 == 1 && 10 > 11 ) || ( 10 < 9 ) && ( 10 == 10)")))
    println(soExample)
}