a>10 && b<c+5 && (a+b)<c*4
находящихся в строке Скалы.
Сам код у меня на Скале, но оценку разных библиотек для этого я делал на Котлине, просто чтобы поиграться с ним. Само выражение я получаю от клиента, но от внутреннего, поэтому мне не надо было заботиться о том, чтобы в выражении мне нe стерли файлы с диска.
Я оценивал разные библиотеки на то 1) могу ли они сделать то, что надо 2) скорость исполнения
Были проверены
- интерполяция строк
- Js Engine
- javaluator
- exp4j
- evalEx
- mxparser
- MathEval
- Groovy
- Jexl
Результаты
Время пробега в мс для 1000 выражений (вернее одно и тоже выражение для 1000 разных набoров 3х переменных):
js | 239 ms |
mxParser | 56713 ms |
evalex | 35 ms |
groovy | 118 ms |
Jexl | 62 ms |
Остальные способы/библиотеки не сработали.
Под катом подробности:
1. Интерполяция строк.
Сначала я думал, нельзя ли приспособить интерполяцию строк Скалы для моих целей.
Я могу в скале написать
s"{a>10}"
но я не смог найти способ превратить обычную строку в строку для интерполяции.В Скале есть 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
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()
}
9. Jexl
val jexl = JexlBuilder().create()
val jexlExp = jexl.createExpression(expr)
fun jexlEvaluate(a: Double, b: Double, c: Double): Boolean {
val jc = MapContext()
jc.set("a", a)
jc.set("b", b)
jc.set("c", c)
return jexlExp.evaluate(jc) as Boolean
}
Код, которым я проверял можно взять на гитхабе
Комментарии (17)
ggo
30.04.2018 20:29+2GroovyShell для других целей.
Вам к GStringTemplateEnginejavax Автор
30.04.2018 21:56+1Сделал, компиляцию выражения таким образом смог вынести и время сильно уменьшилось!
Спасибо
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() }
ElegantBoomerang
01.05.2018 11:18Хочу обратить внимание, что через
ScriptEngineManager
можно запускать и целый Котлин (смотреть про kotlin-scripts-utils). Работает шустро, но долго компилируется перед запуском — для случайных выражений долго, а для более-менее постоянных скриптов может быть и интересно.
dorincea
01.05.2018 11:18А ещё есть jexl, который отлично справляется с задачей:
JexlEngine jexl = new JexlBuilder().create(); String jexlExp = "a>10 && b<c+5 && (a+b)<c*4"; JexlExpression e = jexl.createExpression(jexlExp); JexlContext jc = new MapContext(); jc.set("a", 1); jc.set("b", 2L); jc.set("c", 3D); Object o = e.evaluate(jc); System.out.println("result: " + o);
panchmp
01.05.2018 21:08у меня на проекте используется старый добрый EL 2.0, но можно попробовать и EL 3.0
пример для 3.0 illegalargumentexception.blogspot.ca/2008/04/java-using-el-outside-j2ee.html
Тут есть список альтернатив stackoverflow.com/questions/17026863
Все собираюсь написать для них benchmark
igor_suhorukov
02.05.2018 21:57Есть еще janino который можно как раз использовать как для выражений, так и для java компиляции с помощью ejc. В проекте реализовано много полезного для JSR 199 и т.п.
sshikov
>8. Groovy
>Всё просто и всё работает, но медленно
Насколько я помню, вы взяли не тот API. GroovyShell — это совсем не способ вычислить выражение. Выражение — это groovy.util.Eval, если мне опять же не изменяет память.
Ну и… вы почему-то пропустили языки, которые поддерживают JSR 223, причем все кроме одного (а их было несколько десятков).
javax Автор
Внутри в Eval тот же самый GroovyShell
Вы правы, можно было все имплементации JSR 223 пробывать, но мне показалось, что у evalEx такое преимущество, что уже и смысла нет
sshikov
>Внутри в Eval тот же самый GroovyShell
Вообще-то, скорее наоборот. Шелл — это более высокоуровневая конструкция. Ну т.е. я верю, что разница может в итоге оказаться невелика, но все-таки я бы попробовал.
javax Автор
Вот код из класса Eval:
sshikov
Да, вы правы. Я видимо попутал с org.codehaus.groovy.tools.shell.Main, который тоже в некотором роде шелл, но другой.