diff --git a/.github/workflows/runTests.yaml b/.github/workflows/runTests.yaml index 533cebc..aed7538 100644 --- a/.github/workflows/runTests.yaml +++ b/.github/workflows/runTests.yaml @@ -2,7 +2,7 @@ name: Run Tests -on: [push,pull_request] +on: [pull_request] jobs: test: diff --git a/README.md b/README.md index b6d23a6..7149d67 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: diff --git a/src/commonMain/kotlin/com/notkamui/keval/KevalBuilder.kt b/src/commonMain/kotlin/com/notkamui/keval/KevalBuilder.kt index 86a966e..203a6ca 100644 --- a/src/commonMain/kotlin/com/notkamui/keval/KevalBuilder.kt +++ b/src/commonMain/kotlin/com/notkamui/keval/KevalBuilder.kt @@ -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. @@ -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]) }, @@ -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) = + 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. * diff --git a/src/commonMain/kotlin/com/notkamui/keval/Tokenizer.kt b/src/commonMain/kotlin/com/notkamui/keval/Tokenizer.kt index b76a11f..fd25330 100644 --- a/src/commonMain/kotlin/com/notkamui/keval/Tokenizer.kt +++ b/src/commonMain/kotlin/com/notkamui/keval/Tokenizer.kt @@ -38,6 +38,9 @@ private fun Sequence.normalizeTokens(symbols: Map } token == ")" -> TokenType.RPAREN.also { + if (functionAtCount.last() == parenthesesCount) { + functionAtCount.removeLast() + } parenthesesCount -= 1 ret.add(token) } diff --git a/src/commonTest/kotlin/com/notkamui/keval/ASTTest.kt b/src/commonTest/kotlin/com/notkamui/keval/ASTTest.kt index 71c68c0..3bb70e3 100644 --- a/src/commonTest/kotlin/com/notkamui/keval/ASTTest.kt +++ b/src/commonTest/kotlin/com/notkamui/keval/ASTTest.kt @@ -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()) + } } diff --git a/src/commonTest/kotlin/com/notkamui/keval/DSLResourcesTests.kt b/src/commonTest/kotlin/com/notkamui/keval/DSLResourcesTests.kt index 6dd7465..ac90513 100644 --- a/src/commonTest/kotlin/com/notkamui/keval/DSLResourcesTests.kt +++ b/src/commonTest/kotlin/com/notkamui/keval/DSLResourcesTests.kt @@ -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) + } } diff --git a/src/commonTest/kotlin/com/notkamui/keval/TokenizerTest.kt b/src/commonTest/kotlin/com/notkamui/keval/TokenizerTest.kt index 0901b5d..d92c930 100644 --- a/src/commonTest/kotlin/com/notkamui/keval/TokenizerTest.kt +++ b/src/commonTest/kotlin/com/notkamui/keval/TokenizerTest.kt @@ -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 = "")) + } }