Вычисление логических выражений в строке внутри Java/Scala/Kotlin кода
Мне нужно было в рантайме вычислять истинность выражений типа
a>10 && b<c+5 && (a+b)<c*4
находящихся в строке Скалы.
Само выражение я получаю от клиента, но от внутреннего, поэтому мне не надо было заботиться о том, чтобы в выражении мне нe стерли файлы с диска. Сам код у меня на Скале, но оценку разных библиотек для этого я делал на Котлине, просто чтобы поиграться с ним.
Я оценивал разные библиотеки на то 1) могу ли они сделать то, что надо 2) скорость исполнения
Были проверены
- интерполяция строк
- Js Engine
- javaluator
- exp4j
- evalEx
- mxparser
- MathEval
- Groovy
Результаты
Время пробега в мс для 1000 выражений (вернее одно и тоже выражение для 1000 разных набoров 3х переменных):
js
239 ms
mxParser
56713 ms
evalex
35 ms
groovy
9910 ms
Остальные способы/библиотеки не сработали.
Под катом подробности:
1. Интерполяция строк.
Сначала я думал, нельзя ли приспособить интерполяцию строк Скалы для моих целей.
Я могу в скале написать
s""
но я не смог найти способ превратить обычную строку в строку для интерполяции.
В Скале есть StringContext, который чередует обычные строки с переменными:
s"You are ${age / 10} decades old, $name!"
это на самом деле
StringContext ("You are ", " decades old, ", "!").s (age / 10, name)
и превращается в
"You are " + (age / 10) + " decades old, " + (name) + "!"
но не хотелось возиться с парсингом строки и разделением ее на части
2. Использовать JavaScript Engine внутри Java.
работало без проблем
val e = ScriptEngineManager().getEngineByName("js")
fun jsEvaluate(a: Double, b: Double, c: Double): Boolean { e.context.setAttribute("a", a, ScriptContext.ENGINE_SCOPE) e.context.setAttribute("b", b, ScriptContext.ENGINE_SCOPE) e.context.setAttribute("c", c, ScriptContext.ENGINE_SCOPE) return e.eval(expr) as Boolean
}
3. Библиотека Javaluator
Без того, чтобы писать свои расширения, поддерживает только выражения с плавающей точкой.
С легкостью можно расширить для булевских выражений, и, наверное, можно расширить и для выражений типа "(a+b)>5"
4. Библиотека mxParser
Всё делает, но медленее чем JavaScript примерно в 1000 раз.
Результат всегда возвращает как Double.
val mxExpr = org.mariuszgromada.math.mxparser.Expression(expr) fun mxParserEvaluate(a: Double, b: Double, c: Double): Boolean { val v1 = Argument("a = $a") val v2 = Argument("b = $b") val v3 = Argument("c = $c") mxExpr.addArguments(v1, v2, v3) return mxExpr.calculate() == 1.0 }
5. Библиотека evalEx
Всё делает, самая быстрая, результат возвращает как BigDecimal
val evalExpression = com.udojava.evalex.Expression(expr) fun evalexEvaluate(a: Double, b: Double, c: Double): Boolean { val eval = evalExpression.with("a", BigDecimal.valueOf(a)).and("b", BigDecimal.valueOf(b)).and("c", BigDecimal.valueOf(c)).eval() return eval == BigDecimal.ONE }
6. Библиотека exp4j
Нет поддержки для логических выражений, только выражения с плавающей точкой, но может быть расширена.
7. Библиотека MathEval
Нет поддержки для логических выражений, только выражения с плавающей точкой
8. Groovy
Всё просто и всё работает, но медленно
fun groovyEvaluate(a: Double, b: Double, c: Double): Boolean { val binding = Binding() binding.setVariable("a", a) binding.setVariable("b", b) binding.setVariable("c", c) val sh = GroovyShell(binding) return sh.evaluate(expr) as Boolean }
Код, которым я проверял можно взять на гитхабе
S. P.
По совету в комментах изменил код с груви, время уменьшилось до 100 мс:
val template = groovy.text.GStringTemplateEngine().createTemplate(expr) fun groovyEvaluate(a: Double, b: Double, c: Double): Boolean { val binding = HashMap<String, Double>() binding.put("a", a) binding.put("b", b) binding.put("c", c) val template = template.make(binding) return template.toString().toBoolean() }