Mathematical expression engine written in Kotlin, running on JVM.
With KFormula, you can parse simple mathematical expression text and get the evaluated result.
You can add variables/constants, or beyond that, you can define your own function and expose the logic to the engine.
This way, you can make your application to be able to accept mathematical expression from user's input, or even from database.
If the calculation should be made, simply change the expression to get the new calculation logic applied to your application without recompiling!
Gradle:
repositories {
jcenter()
mavenCentral()
}
dependencies {
...
implementation 'com.github.vittee.kformula:kformula:1.0.3'
...
}Only base-10 decimal is supported, no fancy bin/oct/hex/scientific notations.
All values will be stored as BigDecimal instances.
KFormula support a special numeric ended with % sign.
| Literal | Value |
|---|---|
100% |
1.0 |
50% |
0.5 |
They can be used in percentage operations
KFormula operates the expressions as numbers, so true is 1, false is 0
Variable name must begin with $ or % sign, followed by any unicode characters and underscores/dots.
Example or valid variable names:
$test |
$2pi |
%discount |
$record.value |
$ตัวแปร |
$変数 |
Variable name that begin with % sign can be used in percentage operations
| Operation | Operator |
|---|---|
| Add | + |
| Subtract | - |
| Multiply | * |
| Divide | / |
| Exponent | ^ |
| Modulo | mod |
| Logical OR | or |
| Logical AND | and |
| Logical NOT | not or ! |
| Equal | = or == |
| Not Equal | != or <> |
| Greater than | > |
| Less than | < |
| Greater than or equal | >= |
| Less than or equal | <= |
Syntax:
<expr> in <begin>...<end>
Returns true if <expr> is within the range starting from <begin> to <end>
It is equivalent to:
(<expr> >= <begin>) and (<expr> <= <end>)
Example:
5 in 5..20
Returns true
20 in 5..20
Returns true
4 in 5..20
Returns false
21 in 5..20
Returns false
Syntax:
not in <begin>...<end>
!in <begin>...<end>
Returns true if <expr> is NOT within the range starting from <begin> to <end>
It is equivalent to:
(<expr> < <begin>) or (<expr> > <end>)
Example:
5 not in 5..20
5 !in 5..20
Returns false
20 not in 5..20
20 !in 5..20
Returns false
4 not in 5..20
4 !in 5..20
Returns true
21 not in 5..20
21 !in 5..20
Returns true
Syntax:
<expr> in [<elements>]
Returns true if <expr> is a member of the set specified by <elements>
Example:
5 in [5,10,15,20]
Returns true
20 in [5,10,15,20]
Returns true
4 in [5,10,15,20]
Returns false
12 in [5,10,15,20]
Returns false
21 in [5,10,15,20]
Returns false
Syntax:
not in [<elements>]
!in [<elements>]
Returns true if <expr> is NOT a member of a set specified by <elements>
Example:
5 not in [5,10,15,20]
5 !in [5,10,15,20]
Returns false
20 not in [5,10,15,20]
20 !in [5,10,15,20]
Returns false
4 not [5,10,15,20]
4 !in [5,10,15,20]
Returns true
12 not [5,10,15,20]
12 !in [5,10,15,20]
Returns true
21 not [5,10,15,20]
21 !in [5,10,15,20]
Returns true
Syntax #1:
if <condition> [then] <true expression> [else <false expression>]
Syntax #2 (function call-like):
IF(<condition>,<true expression>[,<false expression>])
The <true expression> will be evaluated only if the <condition> is true, otherwise <false expression> will be evaluated (if provided).
If the <false expression> is omitted and the <condition> is false it will return 0
Note: Both the <true expression> and <false expression> will be evaluated lazily.
For the syntax #1, then can be omitted, but not recommended as it would reduce readability.
Example
Assuming that $fee1 is 60 and $fee2 is 30
Syntax #1:
if $weight > 200 then $fee1 else if $weight > 100 then $fee2
Syntax #2:
IF($weight > 200, $fee1, IF($weight > 100, $fee2))
Mixed syntax #1:
IF($weight > 200, $fee1, if $weight > 100 then $fee2)
Mixed syntax #2:
if $weight > 200 then $fee1 else IF($weight > 100, $fee2)
Returns 60 if $weight is greater than 200
Returns 30 if $weight is greater than 100
Returns 0 if $weight is less than or equal 100
Any variables with name started with % sign and any numbers that ended with % sign are considered as percentage values and can be used in adding and subtracting percentage from a value.
Only + and - work with right hand side percentage value, e.g:
| Expression | Result |
|---|---|
| 30 + 50% | 45 |
| 400 - 50% | 200 |
| 120 + %fifty | 180 |
| 400 - %discount | 300 |
Assuming that %fifty variable is 0.5 and %discount is 0.25
Note: Any other operations on percentage values will result in normal arithmetic operations and the result is still percentage value.
The simplest way is to use Formula class, it has built-in functions ready for use.
Kotlin:
val code = "1+1"
val fx = Formula()
val program = fx.compile(code)
val result = program.eval()
println("result is $result")Java:
final String code = "1+1";
final Formula fx = new Formula();
final RootExpr program = fx.compile(code);
final BigDecimal result = program.eval();
System.out.println("result is " + result.toPlainString())Kotlin:
val fx = Formula().apply {
addConstant("VALUE", 10)
}Java:
final Formula fx = new Formula();
fx.addConstant("VALUE", 10);Kotlin:
val fx = Formula().apply {
addVariable("\$test", 300)
addVariable("%fifty", 0.5)
}Java:
final Formula fx = new Formula();
fx.addVariable("$test", 300);
fx.addVariable("%fifty", 0.5);It is possible to use a runtime variable in your formula. To do so, you can retreive the runtime variable with a callback function, then use it by "$external" (Kotlin) or "$external" (Java).
Kotlin:
val fx = Formula().apply {
addExternalVariable("\$external") {
getValue().toBigDecimal()
}
}Java (with Lambda):
final Formula fx = new Formula();
fx.addExternalVariable("$external", s -> BigDecimal.valueOf(getValue()));Returns the absolute value.
Syntax:
abs(value)
Example:
abs(-10)
Returns 10
Returns the sum of all values.
Syntax:
sum(...<values>)
Example:
sum(1,2,3,4,5)
Returns 15
Returns the average of all values.
Syntax:
average(...<values>)
Example:
average(1,2,3,4,5)
Returns 3
Returns value rounded down to the nearest integer.
Syntax:
floor(<value>)
Example:
floor(5.321)
Returns 5
Returns value rounded up to the nearest integer.
Syntax:
ceil(<value>)
Example:
ceil(5.321)
Returns 6
Returns value rounded to precision.
Syntax:
round(<value>, [precision=0])
Example:
round(5.321)
Returns 5
round(5.566)
Returns 6
round(5.566, 1)
Returns 5.6
Returns the minimum value.
Syntax:
min(...<values>)
Example:
min(5,1,4,2,3)
Returns 1
Returns the maximum value.
Syntax:
max(...<values>)
Example:
max(5,1,4,2,3)
Returns 5
Clamps the value within the inclusive lower and upper bounds.
Syntax:
clamp(<value>, <lower>, <upper>)
Example:
clamp(5, 10, 20)
Returns 5
clamp(25, 10, 20)
Returns 20
clamp(15, 10, 20)
Returns 15
Returns the square root of a number.
Syntax:
sqrt(<value>)
Example:
sqrt(9)
Returns 3
sqrt(2)
Returns 1.414213562373095
Function can be added to the Formula instance by calling addFunction method.
Kotlin addFunction method definition:
fun addFunction(name: String, vararg signatures: String, handler: FunctionCallHandler)Java addFunction method definition:
public void addFunction(String name, String[] signatures, FunctionCallHandler handler)Kotlin:
val fx = Formula().apply {
addFunction("one") {
1.toBigDecimal()
}
}Java (with Lambda):
final Formula fx = new Formula();
fx.addFunction("one", new String[]{}, args -> {
return BigDecimal.valueOf(1);
});Expression:
one()
Evaluate to 1
one() + one()
Evaluate to 2
To add a function with parameters, just specify a list of parameter names via the addFunction method.
The parameter can then be accessed by name via args parameter in handler.
Kotlin:
val fx = Formula().apply {
addFunction("add", "a", "b") { args ->
args["a"] + args["b"]
}
}Java (with Lambda):
final Formula fx = new Formula();
fx.addFunction("add", new String[]{"a", "b"}, args -> {
BigDecimal a = args.get("a").eval();
BigDecimal b = args.get("b").eval();
return a.add(b)
});Example:
add(1,2)
Returns 3
add(1,add(2,4))
Returns 7
Parameters can have default values by specifying parameter names using <name>=<value> format, e.g. param2=100
Kotlin:
val fx = Formula().apply {
addFunction("add", "a", "b=1") { args ->
args["a"] + args["b"]
}
}Java (with Lambda):
final Formula fx = new Formula();
fx.addFunction("add", new String[]{"a", "b=1"}, args -> {
BigDecimal a = args.get("a").eval();
BigDecimal b = args.get("b").eval();
return a.add(b)
});Example:
add(1)
Returns 2
add(1,2)
Returns 3
Sometimes, the number of parameters is unknown, you can prefix the parameter name with ... to make it variadic. (Also known as Rest parameters)
Kotlin:
val fx = Formula().apply {
addFunction("accumulate", "init", "...all") { args ->
val all = args["all"].rest.eval()
args["init"] + all.reduce { sum, v -> sum.add(v) }
}
}Java (with Lambda):
final Formula fx = new Formula();
fx.addFunction("accumulate", new String[]{"init", "...all"}, args -> {
BigDecimal init = args.get("init").eval();
List<Expr> exprs = args.get("all").getRest();
return exprs.stream().map(Expr::eval).reduce(init, (sum, v) -> sum.add(v));
});TBD