Keval
A Kotlin Multiplatform mini library for string evaluation

(You may need to watch out using it:
having to evaluate a string into a number is more often than not a code smell)
Import
You can import Keval directly with the jar files, or using your favorite dependency manager with the Maven Central
repository:
Maven
<dependencies>
<dependency>
<groupId>com.notkamui.libs</groupId>
<artifactId>keval</artifactId>
<version>2.0.0</version>
</>
Gradle (here with KotlinDSL)
repositories {
mavenCentral()
}
dependencies {
implementation("com.notkamui.libs:keval:2.0.0")
}
(In case you're using it with another language than Kotlin -- i.e. Java --, make sure you include kotlin stdlib too)
Usage
Keval can evaluate a mathematical expression as a String into a Double value. It is customizable in the sense that
one can add new binary and unary operators, functions and constants.
The base settings of Keval already include sensible defaults for the most common mathematical operations.
Keval has support for all classic binary operators:
- Subtraction
-
- Addition
+
- Multiplication
*
- Division
/
- Exponent
^
- Remainder (mod)
%
Keval has support for all classic unary operators:
- Negation/Opposition
- (prefix)
- Identity
+ (prefix) (basically does nothing)
- Factorial
! (postfix)
Keval has support for functions of variable arity:
Keval has support for constants, it has two built-in constant:
- π
PI
- e
e (Euler's number)
You can optionally add as many operators, functions or constants to Keval, as long as you define every field
properly, with a DSL (Domain Specific Language):
Keval will use the built-in operators, function and constants if you choose not to define any new resource ; but if you
choose to do so, you need to include them manually. You may also choose to use Keval as an extension function.
Please note that adding a new resource with a name that already exists will overwrite the previous one, except in the
case of operators, where one symbol can represent both a binary and a unary operator. For example, it is possible to
define a binary operator - and a unary operator - at the same time.
You can use it in several ways:
The advantage of using Keval.create is that you may keep an instance of it in a variable so that you can call as
many eval as you need.
In concordance with creating a Keval instance, you can also add resources like this:
This can be combined with creating an instance with a DSL (i.e. Keval.create).
This is an especially useful syntax for Java users, since DSLs generally don't translate well over it.
Creating a resource with a name that already exists will overwrite the previous one.
Keval assumes products/multiplications, and as such, the * symbol/name cannot be overwritten, and is the only
operator to
always be present in the resource set of a Keval instance:
"(2+3)(6+4)".keval() == "(2+3)*(6+4)".keval()
In addition, the symbols (,),, are reserved and trying to create operator using one of those symbols will result with an exception.
Generic number types
Keval is generic over the numeric result type via KevalNumber. The default is Double on all platforms (JVM, JS, Native, Android via commonMain). Use KevalNumbers.real as the primary name for the built-in Double implementation.
Keval.create(myNumber) {
includeDefault()
function {
name = "twice"
arity = 1
implementation = { args -> args[0] + args[0] }
}
}.eval("twice(21)")
"1 + 2".evalWith(KevalNumbers.real)
val compiled = "x * 2".compileWith(KevalNumbers.real)
Function implementations take List<N> instead of DoubleArray. The String.keval() extension and Keval.eval(String) companion remain Double-only shortcuts.
Compile once, evaluate many times
Parsing is the expensive part. Use compile() or String.compileWith() to produce a CompiledExpression<N> that can be evaluated repeatedly:
val keval = Keval.create(KevalNumbers.real) { includeDefault() }
val expr = keval.compile("2 + rate * hours")
expr.eval(mapOf("rate" to 25.0, "hours" to 8.0))
expr.variables
Variables
Identifiers that are not operators, functions, or constants are treated as variables. Constants and functions take precedence over variable names with the same spelling.
val keval = Keval.create(KevalNumbers.real) { includeDefault() }
keval.eval("x + y", mapOf("x" to 3.0, "y" to 7.0))
keval.compile("x(y + 1)").eval(mapOf("x" to 2.0, "y" to 2.0))
Unresolved variables throw KevalUnresolvedVariableException.
Non-throwing evaluation
evalOrNull and evalResult catch only (not arbitrary throwables). The same variants exist on and as / for .
BigDecimal (JVM only)
On the JVM artifact, KevalNumberBigDecimal provides a reduced default set (arithmetic, comparison, aggregates, rounding — no trig/log/random). Decimal comparison operators (eq, ne, gt, …) use numeric equality via compareTo, not scale equality. Other targets continue to use Double through commonMain.
"0.1 + 0.2".kevalBigDecimal()
Keval.create(KevalNumbers.BigDecimal) {
includeDefault()
}.eval("sum(1, 2, 3)")
val lowPrecision = KevalNumberBigDecimal.withContext(MathContext(4))
Keval.create(lowPrecision) { includeDefault() }.eval("1 / 3")
BigDecimal on Android
The JVM jvmMain artifact (including KevalNumberBigDecimal) is not on the Android classpath. Android apps can still use Double via KevalNumbers.real from . For on Android, implement locally — copy or adapt the defaults from using .
Migrating from v1.x
String.keval() and Keval.eval(expr) are unchanged for Double.
Error Handling
In case of an error, Keval will throw one of several KevalExceptions:
KevalZeroDivisionException, KevalInvalidArgumentException, and KevalUnresolvedVariableException are instantiable so that you can throw them when implementing a custom operator/function.
Use evalOrNull / evalResult (or String.kevalOrNull() / String.kevalResult() for Double) when you prefer not to catch exceptions manually.