Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/runTests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

name: Run Tests

on: [push,pull_request]
on: [pull_request]

jobs:
test:
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ Keval has support for all classic unary operators:
Keval has support for functions of variable arity:

- Negate/Oppose `neg(expr)` (where 'expr' is an expression)
- Expression sign `sign(expr)` (where 'expr' is an expression)
- Absolute `abs(expr)` (where 'expr' is an expression)
- Square root `sqrt(expr)` (where 'expr' is an expression)
- Cube root `cbrt(expr)` (where 'expr' is an expression)
- Root of nth power `nthrt(expr_a, expr_pow)` (where 'expr_a' is an expression and 'expr_pow' is target power)
- Exponential `exp(expr)` (where 'expr' is an expression)
- Natural logarithm `ln(expr)` (where 'expr' is an expression)
- Base 10 logarithm `log10(expr)` (where 'expr' is an expression)
Expand All @@ -79,6 +81,28 @@ Keval has support for functions of variable arity:
- Ceiling `ceil(expr)` (where 'expr' is an expression)
- Floor `floor(expr)` (where 'expr' is an expression)
- Round `round(expr)` (where 'expr' is an expression)
- Truncate decimal part `trunc(expr)` (where 'expr' is an expression)
- Minimal/maximal/average/median value `min(expr...)`/`max(expr...)`/`avg(expr...)`/`median(expr...)` (where 'expr...' is any number of expressions to get results from)
- Percentile `percentile(expr_perc, expr_a)` (where 'expr_a' is the expression to get percentile from, and 'expr_perc' is the percent value)
- Random number generator/selector `rand(expr...)` (where 'expr...' is any number of expressions; either generates a random double (zero arguments), or generates a random integer up to expr (one argument), or selects a random value of given arguments (two or more arguments)
- Random number between 'start' and 'end' with 'step' `randRange(expr_start, expr_end, expr_step)`
- Built-in boolean operations (0.0 is equivalent to false, anything else is equivalent to true):
- Convert to "boolean" form (0/1) `bool(expr)` (where 'expr' is an expression)
- Invert `not(bool)` (where 'bool' is a boolean)
- And `and(bool...)` (where 'bool...' is any number of booleans)
- Not And `nand(bool...)` (where 'bool...' is any number of booleans)
- Or `or(bool...)` (where 'bool...' is any number of booleans)
- Not Or `nor(bool...)` (where 'bool...' is any number of booleans)
- Exclusive Or `xor(bool...)` (where 'bool...' is any number of booleans)
- Exlusive Not Or `xnor(bool...)` (where 'bool...' is any number of booleans)
- Implies `imply(bool_a, bool_b)` (where 'bool_a' and 'bool_b' are booleans)
- Does Not Imply `nimply(bool_a, bool_b)` (where 'bool_a' and 'bool_b' are booleans)
- Equals `eq(expr...)` (where 'expr...' is any number of expressions; returns a boolean)
- Does Not Equal `ne(expr...)` (where 'expr...' is any number of expressions; returns a boolean)
- Greater Than `gt(expr_a, expr_b)` (where 'expr_a' and 'expr_b' are expressions; returns a boolean)
- Greater Or Equal To `ge(expr_a, expr_b)` (where 'expr_a' and 'expr_b' are expressions; returns a boolean)
- Less Than `lt(expr_a, expr_b)` (where 'expr_a' and 'expr_b' are expressions; returns a boolean)
- Less Or Equal To `le(expr_a, expr_b)` (where 'expr_a' and 'expr_b' are expressions; returns a boolean)

Keval has support for constants, it has two built-in constant:

Expand Down
66 changes: 66 additions & 0 deletions src/commonMain/kotlin/com/notkamui/keval/KevalBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.notkamui.keval

import kotlin.math.*
import kotlin.random.Random

/**
* This class is used to build a Keval instance with custom operators, functions, and constants.
Expand Down Expand Up @@ -156,9 +157,11 @@ class KevalBuilder internal constructor(

// functions
"neg" to KevalFunction(1) { -it[0] },
"sign" to KevalFunction(1) { if (it[0] < 0) -1.0 else if (it[0] > 0) 1.0 else 0.0 },
"abs" to KevalFunction(1) { it[0].absoluteValue },
"sqrt" to KevalFunction(1) { sqrt(it[0]) },
"cbrt" to KevalFunction(1) { cbrt(it[0]) },
"nthrt" to KevalFunction(2) { it[1].pow(1 / it[0]) },
"exp" to KevalFunction(1) { exp(it[0]) },
"ln" to KevalFunction(1) { ln(it[0]) },
"log10" to KevalFunction(1) { log10(it[0]) },
Expand All @@ -172,12 +175,75 @@ class KevalBuilder internal constructor(
"ceil" to KevalFunction(1) { ceil(it[0]) },
"floor" to KevalFunction(1) { floor(it[0]) },
"round" to KevalFunction(1) { round(it[0]) },
"trunc" to KevalFunction(1) { it[0].toInt().toDouble() },
"min" to KevalFunction(null) { it.min() },
"max" to KevalFunction(null) { it.max() },
"sum" to KevalFunction(null) { it.sum() },
"avg" to KevalFunction(null) { it.average() },
"median" to KevalFunction(null) { it.sorted()[it.size / 2] },
"percentile" to KevalFunction(null) {
if (it.size <= 1) throw KevalInvalidArgumentException("percentile requires at least 2 values")
val perc = it[0]
if (perc !in 0.0..100.0) throw KevalInvalidArgumentException("percentile must be between 0 and 100")
val sorted = it.sorted()
val index = ((perc / 100) * sorted.size).toInt()
sorted[index]
},
"rand" to KevalFunction(null) {
when (it.size) {
0 -> Random.Default.nextDouble()
1 -> (0..it[0].toInt()).random().toDouble()
else -> it.random()
}
},
"randRange" to KevalFunction(3) {
val start = it[0]
val end = it[1]
val step = it[2]

if (step > 0) throw KevalInvalidArgumentException("step must be greater than 0")
val numberOfSteps = ((end - start) / step).toInt()
val randomStepIndex = Random.nextInt(0, numberOfSteps + 1)
start + randomStepIndex * step
},

// logical functions
"bool" to KevalFunction(1) { booleanToDouble(it[0] != 0.0) },
"not" to KevalFunction(1) { booleanToDouble(!doubleToBoolean(it[0])) },
"and" to KevalFunction(null) { it.reduceBoolean { a, b -> a && b } },
"nand" to KevalFunction(null) { it.reduceBoolean(true) { a, b -> a && b } },
"or" to KevalFunction(null) { it.reduceBoolean { a, b -> a || b } },
"nor" to KevalFunction(null) { it.reduceBoolean(true) { a, b -> a || b } },
"xor" to KevalFunction(null) { it.reduceBoolean { a, b -> a xor b } },
"xnor" to KevalFunction(null) { it.reduceBoolean(true) { a, b -> a xor b } },
"imply" to KevalFunction(2) { booleanOperation(it) { a, b -> !a || b } },
"nimply" to KevalFunction(2) { booleanOperation(it) { a, b -> a && !b } },
"eq" to KevalFunction(null) { booleanToDouble(it.all { e -> e == it[0] }) },
"ne" to KevalFunction(null) { booleanToDouble(it.distinct().size == it.size) },
"gt" to KevalFunction(2) { booleanToDouble(it[0] > it[1]) },
"lt" to KevalFunction(2) { booleanToDouble(it[0] < it[1]) },
"ge" to KevalFunction(2) { booleanToDouble(it[0] >= it[1]) },
"le" to KevalFunction(2) { booleanToDouble(it[0] <= it[1]) },

// constants
"PI" to KevalConstant(PI),
"e" to KevalConstant(E)
)

private fun doubleToBoolean(value: Double) = value != 0.0
private fun booleanToDouble(value: Boolean) = if (value) 1.0 else 0.0
private fun booleanOperation(array: DoubleArray, operation: (Boolean, Boolean) -> Boolean) =
booleanToDouble(operation(doubleToBoolean(array[0]), doubleToBoolean(array[1])))
private fun DoubleArray.viaBoolean(operation: List<Boolean>.() -> Boolean) =
booleanToDouble(operation(map(::doubleToBoolean)))
private fun DoubleArray.reduceBoolean(invert: Boolean = false, operation: (Boolean, Boolean) -> Boolean) =
viaBoolean {
reduce(operation).let {
if (invert) !it
else it
}
}

/**
* Builder representation of a binary operator.
*
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/com/notkamui/keval/Tokenizer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ private fun Sequence<String>.normalizeTokens(symbols: Map<String, KevalOperator>
}

token == ")" -> TokenType.RPAREN.also {
if (functionAtCount.last() == parenthesesCount) {
functionAtCount.removeLast()
}
parenthesesCount -= 1
ret.add(token)
}
Expand Down
8 changes: 8 additions & 0 deletions src/commonTest/kotlin/com/notkamui/keval/ASTTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,12 @@ class ASTTest {
val node = ValueNode(9.0)
assertEquals(9.0, node.eval())
}

@Test
fun testNestedFunctionNodes() {
val node1 = FunctionNode({ a -> a.sum() }, listOf(ValueNode(3.0), ValueNode(4.0)))
val node2 = FunctionNode({ a -> a.sum() }, listOf(node1, ValueNode(5.0)))
val mainNode = FunctionNode({ a -> a.sum() }, listOf(node2, node1))
assertEquals(19.0, mainNode.eval())
}
}
42 changes: 42 additions & 0 deletions src/commonTest/kotlin/com/notkamui/keval/DSLResourcesTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,46 @@ class DLSTest {
}
assertEquals(3.0, k.eval("1+2"), "1+2")
}

// this test fails due to wrong handling of nested calls
@Test
fun checkLogicalOperations() {
val k = Keval.create {
includeDefault()
function {
name = "isPositive"
arity = 1
implementation = { args -> if (args[0] > 0.0) 1.0 else 0.0 }
}
binaryOperator {
symbol = '#'
implementation = { a, b -> if (a > b) 1.0 else 0.0 }
precedence = 3
isLeftAssociative = true
}
}

val expr = """
and(
not(lt(5, 3)),
or(
gt(4,2),
xor(
eq(1, 1, 1),
ne(1, 2, 3)
)
),
nand(
ge(5, 5),
not(le(3, 4))
),
nor(
imply(1, 0),
nimply(1, 1)
),
xnor(1, 1)
)
""".trimIndent()
assertEquals(1.0, k.eval(expr), expr)
}
}
24 changes: 24 additions & 0 deletions src/commonTest/kotlin/com/notkamui/keval/TokenizerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,28 @@ class TokenizerTest {
val nodes = "f(((1)))".tokenize(k.resourcesView())
assertEquals("f(((1)))", nodes.joinToString(separator = ""))
}

@Test
fun checkNestedFunctions() {
val k = Keval.create {
includeDefault()
function {
name = "f"
arity = 1
implementation = { args -> args[0] }
}
function {
name = "s"
implementation = { args -> args.sum() }
}
function {
name = "a"
arity = 2
implementation = { args -> args[0] + args[1] }
}
}

val nodes = "f(s(a(1,2),3))".tokenize(k.resourcesView())
assertEquals("f(s(a(1,2),3))", nodes.joinToString(separator = ""))
}
}
Loading