├── .idea ├── .name ├── codeStyles │ └── codeStyleConfig.xml ├── vcs.xml ├── kotlinc.xml ├── encodings.xml ├── modules.xml ├── runConfigurations.xml ├── misc.xml ├── jarRepositories.xml ├── inspectionProfiles │ └── Project_Default.xml └── compiler.xml ├── src ├── main │ └── kotlin │ │ └── com │ │ └── github │ │ └── keelar │ │ └── exprk │ │ ├── internal │ │ ├── Function.kt │ │ ├── Token.kt │ │ ├── TokenType.kt │ │ ├── Expr.kt │ │ ├── Scanner.kt │ │ ├── Evaluator.kt │ │ └── Parser.kt │ │ └── Expressions.kt └── test │ └── kotlin │ └── com │ └── github │ └── keelar │ └── exprk │ └── TestExpressions.kt ├── LICENSE ├── .gitignore ├── ExprK.iml ├── pom.xml └── README.md /.idea/.name: -------------------------------------------------------------------------------- 1 | ExprK -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/Function.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | import java.math.BigDecimal 4 | 5 | abstract class Function { 6 | 7 | abstract fun call(arguments: List): BigDecimal 8 | 9 | } -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/Token.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | internal class Token(val type: TokenType, 4 | val lexeme: String, 5 | val literal: Any?) { 6 | 7 | override fun toString(): String { 8 | return type.toString() + " " + lexeme + " " + literal 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/TokenType.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | internal enum class TokenType { 4 | 5 | // Basic operators 6 | PLUS, 7 | MINUS, 8 | STAR, 9 | SLASH, 10 | MODULO, 11 | EXPONENT, 12 | SQUARE_ROOT, 13 | ASSIGN, 14 | 15 | // Logical operators 16 | EQUAL_EQUAL, 17 | NOT_EQUAL, 18 | GREATER, 19 | GREATER_EQUAL, 20 | LESS, 21 | LESS_EQUAL, 22 | BAR_BAR, 23 | AMP_AMP, 24 | 25 | // Other 26 | COMMA, 27 | 28 | // Parentheses 29 | LEFT_PAREN, 30 | RIGHT_PAREN, 31 | 32 | // Literals 33 | NUMBER, 34 | IDENTIFIER, 35 | 36 | EOF 37 | 38 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Keelar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff: 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/dictionaries 8 | 9 | # Sensitive or high-churn files: 10 | .idea/**/dataSources/ 11 | .idea/**/dataSources.ids 12 | .idea/**/dataSources.xml 13 | .idea/**/dataSources.local.xml 14 | .idea/**/sqlDataSources.xml 15 | .idea/**/dynamic.xml 16 | .idea/**/uiDesigner.xml 17 | 18 | # Gradle: 19 | .idea/**/gradle.xml 20 | .idea/**/libraries 21 | 22 | # CMake 23 | cmake-build-debug/ 24 | cmake-build-release/ 25 | 26 | # Mongo Explorer plugin: 27 | .idea/**/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | 40 | # JIRA plugin 41 | atlassian-ide-plugin.xml 42 | 43 | # Cursive Clojure plugin 44 | .idea/replstate.xml 45 | 46 | # Crashlytics plugin (for Android Studio and IntelliJ) 47 | com_crashlytics_export_strings.xml 48 | crashlytics.properties 49 | crashlytics-build.properties 50 | fabric.properties 51 | 52 | /target -------------------------------------------------------------------------------- /ExprK.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/keelar/exprk/TestExpressions.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk 2 | 3 | import org.junit.Test 4 | import java.math.BigDecimal 5 | import kotlin.test.assertEquals 6 | 7 | class TestExpressions { 8 | 9 | @Test 10 | fun `test that scientific notation BigDecimals are parsed and equivalent to the plain representation`() { 11 | val expr = Expressions() 12 | val scival = BigDecimal("1E+7") 13 | expr.define("SCIVAL", scival) 14 | assertEquals(scival.toPlainString(), expr.eval("SCIVAL").toPlainString()) 15 | } 16 | 17 | @Test 18 | fun `test Scanner will scan scientific form correctly`() { 19 | val expr = Expressions() 20 | assertEquals(BigDecimal("1e+7").toPlainString(), expr.eval("1E+7").toPlainString()) 21 | assertEquals(BigDecimal("1e-7").toPlainString(), expr.eval("1E-7").toPlainString()) 22 | assertEquals(BigDecimal(".101e+2").toPlainString(), expr.eval(".101e+2").toPlainString()) 23 | assertEquals(BigDecimal(".123e2").toPlainString(), expr.eval(".123e2").toPlainString()) 24 | assertEquals(BigDecimal("3212.123e-2").toPlainString(), expr.eval("3212.123e-2").toPlainString()) 25 | } 26 | 27 | @Test 28 | fun `test normal expression`() { 29 | val expr = Expressions() 30 | assertEquals(BigDecimal(".123e2").add(BigDecimal("3212.123e-2")).toPlainString(), 31 | expr.eval(".123e2+3212.123e-2").toPlainString()) 32 | assertEquals(BigDecimal("1e+7").minus(BigDecimal("52132e-2")).toPlainString(), 33 | expr.eval("1E+7-52132e-2").toPlainString()) 34 | } 35 | 36 | @Test 37 | fun `test is functions are ignore case`() { 38 | val expr = Expressions() 39 | assertEquals(listOf(BigDecimal.ONE.negate(), BigDecimal.ZERO, BigDecimal.ONE).minOrNull(), 40 | expr.eval("mIN(-1,0,1)")) 41 | 42 | assertEquals(listOf(BigDecimal.ONE.negate(), BigDecimal.ZERO, BigDecimal.ONE).maxOrNull(), 43 | expr.eval("MaX(-1,0,1)")) 44 | } 45 | 46 | @Test 47 | fun `test is variables are ignore case`() { 48 | val expr = Expressions() 49 | assertEquals(BigDecimal(Math.PI.toString()), expr.eval("pI")) 50 | assertEquals(BigDecimal(Math.E.toString()), expr.eval("E")) 51 | } 52 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | com.github.keelar.exprk 8 | ExprK 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | com.github.keelar.exprk ExprK 13 | 14 | 15 | UTF-8 16 | 1.6.0 17 | 4.13.1 18 | 19 | 20 | 21 | 22 | org.jetbrains.kotlin 23 | kotlin-stdlib 24 | ${kotlin.version} 25 | 26 | 27 | org.jetbrains.kotlin 28 | kotlin-test-junit 29 | ${kotlin.version} 30 | test 31 | 32 | 33 | junit 34 | junit 35 | ${junit.version} 36 | test 37 | 38 | 39 | 40 | 41 | src/main/kotlin 42 | src/test/kotlin 43 | 44 | 45 | 46 | org.jetbrains.kotlin 47 | kotlin-maven-plugin 48 | ${kotlin.version} 49 | 50 | 51 | compile 52 | compile 53 | 54 | compile 55 | 56 | 57 | 58 | test-compile 59 | test-compile 60 | 61 | test-compile 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/Expr.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | import java.math.BigDecimal 4 | 5 | internal sealed class Expr { 6 | 7 | abstract fun accept(visitor: ExprVisitor): R 8 | 9 | } 10 | 11 | internal class AssignExpr(val name: Token, 12 | val value: Expr) : Expr() { 13 | 14 | override fun accept(visitor: ExprVisitor): R { 15 | return visitor.visitAssignExpr(this) 16 | } 17 | 18 | } 19 | 20 | internal class LogicalExpr(val left: Expr, 21 | val operator: Token, 22 | val right: Expr) : Expr() { 23 | 24 | override fun accept(visitor: ExprVisitor): R { 25 | return visitor.visitLogicalExpr(this) 26 | } 27 | 28 | } 29 | 30 | internal class BinaryExpr(val left: Expr, 31 | val operator: Token, 32 | val right: Expr) : Expr() { 33 | 34 | override fun accept(visitor: ExprVisitor): R { 35 | return visitor.visitBinaryExpr(this) 36 | } 37 | 38 | } 39 | 40 | internal class UnaryExpr(val operator: Token, 41 | val right: Expr) : Expr() { 42 | 43 | override fun accept(visitor: ExprVisitor): R { 44 | return visitor.visitUnaryExpr(this) 45 | } 46 | 47 | } 48 | 49 | internal class CallExpr(val name: String, 50 | val arguments: List) : Expr() { 51 | 52 | override fun accept(visitor: ExprVisitor): R { 53 | return visitor.visitCallExpr(this) 54 | } 55 | 56 | } 57 | 58 | internal class LiteralExpr(val value: BigDecimal) : Expr() { 59 | 60 | override fun accept(visitor: ExprVisitor): R { 61 | return visitor.visitLiteralExpr(this) 62 | } 63 | 64 | } 65 | 66 | internal class VariableExpr(val name: Token) : Expr() { 67 | 68 | override fun accept(visitor: ExprVisitor): R { 69 | return visitor.visitVariableExpr(this) 70 | } 71 | 72 | } 73 | 74 | internal class GroupingExpr(val expression: Expr) : Expr() { 75 | 76 | override fun accept(visitor: ExprVisitor): R { 77 | return visitor.visitGroupingExpr(this) 78 | } 79 | 80 | } 81 | 82 | internal interface ExprVisitor { 83 | 84 | fun visitAssignExpr(expr: AssignExpr): R 85 | 86 | fun visitLogicalExpr(expr: LogicalExpr): R 87 | 88 | fun visitBinaryExpr(expr: BinaryExpr): R 89 | 90 | fun visitUnaryExpr(expr: UnaryExpr): R 91 | 92 | fun visitCallExpr(expr: CallExpr): R 93 | 94 | fun visitLiteralExpr(expr: LiteralExpr): R 95 | 96 | fun visitVariableExpr(expr: VariableExpr): R 97 | 98 | fun visitGroupingExpr(expr: GroupingExpr): R 99 | 100 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExprK 2 | A simple mathematical expression evaluator for Kotlin and Java, written in Kotlin. 3 | 4 | ### Features: 5 | * Uses BigDecimal for calculations and results 6 | * Allows you to define variables using values or expressions 7 | * Variable definition expressions can reference previously defined variables 8 | * Configurable precision and rounding mode 9 | * Functions and the ability to define new ones 10 | 11 | ### Supported operators 12 | #### Arithmetic operators 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
NameOperator
Plus+
Minus-
Multiply*
Divide/
Modulus%
Exponent^
Square root
47 | 48 | #### Logical operators 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
NameOperator
And&&
Or||
63 | 64 | ### Pre-defined variables 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
VariableValue
pi3.141592653589793
e2.718281828459045
79 | 80 | ### Pre-defined functions 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
FunctionDescription
abs(expression)Returns the absolute value of the expression
sum(expression, ...)Returns the sum of all arguments
floor(expression)Rounds the value of the expression down to the nearest integer
ceil(expression)Rounds the value of the expression up to the nearest integer
round(expression)Rounds the value of the expression to the nearest integer in the direction decided by the configured rounding mode
min(expression, ...)Returns the value of the smallest argument
max(expression, ...)Returns the value of the largest argument
if(condition, trueValue, falseValue)Returns trueValue if condition is true(condition != 0), otherwise it returns falseValue
119 | 120 | ### Examples: 121 | ````Kotlin 122 | val result = Expressions() 123 | .eval("(5+5)*10") // returns 100 124 | ```` 125 | You can define variables with the `define` method. 126 | ````Kotlin 127 | val result = Expressions() 128 | .define("x", 5) 129 | .eval("x*10") // returns 50 130 | ```` 131 | The define method returns the expression instance to allow chaining definition method calls together. 132 | ````Kotlin 133 | val result = Expressions() 134 | .define("x", 5) 135 | .define("y", "5*2") 136 | .eval("x*y") // returns 50 137 | ```` 138 | Variable definition expressions can reference previously defined variables. 139 | ````Kotlin 140 | val result = Expressions() 141 | .define("x", 5) 142 | .define("y", "x^2") 143 | .eval("y*x") // returns 125 144 | ```` 145 | You can add new functions with the `addFunction` method. 146 | ````kotlin 147 | val result = Expressions() 148 | .addFunction("min") { arguments -> 149 | if (arguments.isEmpty()) throw ExpressionException( 150 | "min requires at least one argument") 151 | 152 | arguments.min()!! 153 | } 154 | .eval("min(4, 8, 16)") // returns 4 155 | ```` 156 | You can set the precision and rounding mode with `setPrecision` and `setRoundingMode`. 157 | ````Kotlin 158 | val result = Expressions() 159 | .setPrecision(128) 160 | .setRoundingMode(RoundingMode.UP) 161 | .eval("222^3/5.5") 162 | ```` 163 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/Scanner.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | import java.math.MathContext 4 | import com.github.keelar.exprk.ExpressionException 5 | import com.github.keelar.exprk.internal.TokenType.* 6 | 7 | private fun invalidToken(c: Char) { 8 | throw ExpressionException("Invalid token '$c'") 9 | } 10 | 11 | internal class Scanner(private val source: String, 12 | private val mathContext: MathContext) { 13 | 14 | private val tokens: MutableList = mutableListOf() 15 | private var start = 0 16 | private var current = 0 17 | 18 | fun scanTokens(): List { 19 | while (!isAtEnd()) { 20 | scanToken() 21 | } 22 | 23 | tokens.add(Token(EOF, "", null)) 24 | return tokens 25 | } 26 | 27 | private fun isAtEnd(): Boolean { 28 | return current >= source.length 29 | } 30 | 31 | private fun scanToken() { 32 | start = current 33 | val c = advance() 34 | 35 | when (c) { 36 | ' ', 37 | '\r', 38 | '\t' -> { 39 | // Ignore whitespace. 40 | } 41 | '+' -> addToken(PLUS) 42 | '-' -> addToken(MINUS) 43 | '*' -> addToken(STAR) 44 | '/' -> addToken(SLASH) 45 | '%' -> addToken(MODULO) 46 | '^' -> addToken(EXPONENT) 47 | '√' -> addToken(SQUARE_ROOT) 48 | '=' -> if (match('=')) addToken(EQUAL_EQUAL) else addToken(ASSIGN) 49 | '!' -> if (match('=')) addToken(NOT_EQUAL) else invalidToken(c) 50 | '>' -> if (match('=')) addToken(GREATER_EQUAL) else addToken(GREATER) 51 | '<' -> if (match('=')) addToken(LESS_EQUAL) else addToken(LESS) 52 | '|' -> if (match('|')) addToken(BAR_BAR) else invalidToken(c) 53 | '&' -> if (match('&')) addToken(AMP_AMP) else invalidToken(c) 54 | ',' -> addToken(COMMA) 55 | '(' -> addToken(LEFT_PAREN) 56 | ')' -> addToken(RIGHT_PAREN) 57 | else -> { 58 | when { 59 | c.isDigit() -> number() 60 | c.isAlpha() -> identifier() 61 | else -> invalidToken(c) 62 | } 63 | } 64 | } 65 | } 66 | 67 | private fun isDigit(char: Char, 68 | previousChar: Char = '\u0000', 69 | nextChar: Char = '\u0000'): Boolean { 70 | return char.isDigit() || when (char) { 71 | '.' -> true 72 | 'e', 'E' -> previousChar.isDigit() && (nextChar.isDigit() || nextChar == '+' || nextChar == '-') 73 | '+', '-' -> (previousChar == 'e' || previousChar == 'E') && nextChar.isDigit() 74 | else -> false 75 | } 76 | } 77 | 78 | private fun number() { 79 | while (peek().isDigit()) advance() 80 | 81 | if (isDigit(peek(), peekPrevious(), peekNext())) { 82 | advance() 83 | while (isDigit(peek(), peekPrevious(), peekNext())) advance() 84 | } 85 | 86 | val value = source 87 | .substring(start, current) 88 | .toBigDecimal(mathContext) 89 | 90 | addToken(NUMBER, value) 91 | } 92 | 93 | private fun identifier() { 94 | while (peek().isAlphaNumeric()) advance() 95 | 96 | addToken(IDENTIFIER) 97 | } 98 | 99 | private fun advance() = source[current++] 100 | 101 | private fun peek(): Char { 102 | return if (isAtEnd()) { 103 | '\u0000' 104 | } else { 105 | source[current] 106 | } 107 | } 108 | 109 | private fun peekPrevious(): Char = if (current > 0) source[current - 1] else '\u0000' 110 | 111 | private fun peekNext(): Char { 112 | return if (current + 1 >= source.length) { 113 | '\u0000' 114 | } else { 115 | source[current + 1] 116 | } 117 | } 118 | 119 | private fun match(expected: Char): Boolean { 120 | if (isAtEnd()) return false 121 | if (source[current] != expected) return false 122 | 123 | current++ 124 | return true 125 | } 126 | 127 | private fun addToken(type: TokenType) = addToken(type, null) 128 | 129 | private fun addToken(type: TokenType, literal: Any?) { 130 | val text = source.substring(start, current) 131 | tokens.add(Token(type, text, literal)) 132 | } 133 | 134 | private fun Char.isAlphaNumeric() = isAlpha() || isDigit() 135 | 136 | private fun Char.isAlpha() = this in 'a'..'z' 137 | || this in 'A'..'Z' 138 | || this == '_' 139 | 140 | private fun Char.isDigit() = this == '.' || this in '0'..'9' 141 | 142 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/Evaluator.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | import com.github.keelar.exprk.ExpressionException 4 | import com.github.keelar.exprk.internal.TokenType.* 5 | import java.math.BigDecimal 6 | import java.math.MathContext 7 | import java.math.RoundingMode 8 | 9 | internal class Evaluator() : ExprVisitor { 10 | internal var mathContext: MathContext = MathContext.DECIMAL64 11 | 12 | private val variables: LinkedHashMap = linkedMapOf() 13 | private val functions: MutableMap = mutableMapOf() 14 | 15 | private fun define(name: String, value: BigDecimal) { 16 | variables += name to value 17 | } 18 | 19 | fun define(name: String, expr: Expr): Evaluator { 20 | define(name.toLowerCase(), eval(expr)) 21 | 22 | return this 23 | } 24 | 25 | fun addFunction(name: String, function: Function): Evaluator { 26 | functions += name.toLowerCase() to function 27 | 28 | return this 29 | } 30 | 31 | fun eval(expr: Expr): BigDecimal { 32 | return expr.accept(this) 33 | } 34 | 35 | override fun visitAssignExpr(expr: AssignExpr): BigDecimal { 36 | val value = eval(expr.value) 37 | 38 | define(expr.name.lexeme, value) 39 | 40 | return value 41 | } 42 | 43 | override fun visitLogicalExpr(expr: LogicalExpr): BigDecimal { 44 | val left = expr.left 45 | val right = expr.right 46 | 47 | return when (expr.operator.type) { 48 | BAR_BAR -> left or right 49 | AMP_AMP -> left and right 50 | else -> throw ExpressionException( 51 | "Invalid logical operator '${expr.operator.lexeme}'") 52 | } 53 | } 54 | 55 | override fun visitBinaryExpr(expr: BinaryExpr): BigDecimal { 56 | val left = eval(expr.left) 57 | val right = eval(expr.right) 58 | 59 | return when (expr.operator.type) { 60 | PLUS -> left + right 61 | MINUS -> left - right 62 | STAR -> left * right 63 | SLASH -> left.divide(right, mathContext) 64 | MODULO -> left.remainder(right, mathContext) 65 | EXPONENT -> left pow right 66 | EQUAL_EQUAL -> (left == right).toBigDecimal() 67 | NOT_EQUAL -> (left != right).toBigDecimal() 68 | GREATER -> (left > right).toBigDecimal() 69 | GREATER_EQUAL -> (left >= right).toBigDecimal() 70 | LESS -> (left < right).toBigDecimal() 71 | LESS_EQUAL -> (left <= right).toBigDecimal() 72 | else -> throw ExpressionException( 73 | "Invalid binary operator '${expr.operator.lexeme}'") 74 | } 75 | } 76 | 77 | override fun visitUnaryExpr(expr: UnaryExpr): BigDecimal { 78 | val right = eval(expr.right) 79 | 80 | return when (expr.operator.type) { 81 | MINUS -> { 82 | right.negate() 83 | } 84 | SQUARE_ROOT -> { 85 | right.pow(BigDecimal(0.5)) 86 | } 87 | else -> throw ExpressionException("Invalid unary operator") 88 | } 89 | } 90 | 91 | override fun visitCallExpr(expr: CallExpr): BigDecimal { 92 | val name = expr.name 93 | val function = functions[name.toLowerCase()] ?: 94 | throw ExpressionException("Undefined function '$name'") 95 | 96 | return function.call(expr.arguments.map { eval(it) }) 97 | } 98 | 99 | override fun visitLiteralExpr(expr: LiteralExpr): BigDecimal { 100 | return expr.value 101 | } 102 | 103 | override fun visitVariableExpr(expr: VariableExpr): BigDecimal { 104 | val name = expr.name.lexeme 105 | 106 | return variables[name.toLowerCase()] ?: 107 | throw ExpressionException("Undefined variable '$name'") 108 | } 109 | 110 | override fun visitGroupingExpr(expr: GroupingExpr): BigDecimal { 111 | return eval(expr.expression) 112 | } 113 | 114 | private infix fun Expr.or(right: Expr): BigDecimal { 115 | val left = eval(this) 116 | 117 | // short-circuit if left is truthy 118 | if (left.isTruthy()) return BigDecimal.ONE 119 | 120 | return eval(right).isTruthy().toBigDecimal() 121 | } 122 | 123 | private infix fun Expr.and(right: Expr): BigDecimal { 124 | val left = eval(this) 125 | 126 | // short-circuit if left is falsey 127 | if (!left.isTruthy()) return BigDecimal.ZERO 128 | 129 | return eval(right).isTruthy().toBigDecimal() 130 | } 131 | 132 | private fun BigDecimal.isTruthy(): Boolean { 133 | return this != BigDecimal.ZERO 134 | } 135 | 136 | private fun Boolean.toBigDecimal(): BigDecimal { 137 | return if (this) BigDecimal.ONE else BigDecimal.ZERO 138 | } 139 | 140 | private infix fun BigDecimal.pow(n: BigDecimal): BigDecimal { 141 | var right = n 142 | val signOfRight = right.signum() 143 | right = right.multiply(signOfRight.toBigDecimal()) 144 | val remainderOfRight = right.remainder(BigDecimal.ONE) 145 | val n2IntPart = right.subtract(remainderOfRight) 146 | val intPow = pow(n2IntPart.intValueExact(), mathContext) 147 | val doublePow = BigDecimal(Math 148 | .pow(toDouble(), remainderOfRight.toDouble())) 149 | 150 | var result = intPow.multiply(doublePow, mathContext) 151 | if (signOfRight == -1) result = BigDecimal 152 | .ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP) 153 | 154 | return result 155 | } 156 | 157 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/internal/Parser.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk.internal 2 | 3 | import com.github.keelar.exprk.ExpressionException 4 | import com.github.keelar.exprk.internal.TokenType.* 5 | import java.math.BigDecimal 6 | 7 | internal class Parser(private val tokens: List) { 8 | 9 | private var current = 0 10 | 11 | fun parse(): Expr { 12 | val expr = expression() 13 | 14 | if (!isAtEnd()) { 15 | throw ExpressionException("Expected end of expression, found '${peek().lexeme}'") 16 | } 17 | 18 | return expr 19 | } 20 | 21 | private fun expression(): Expr { 22 | return assignment() 23 | } 24 | 25 | private fun assignment(): Expr { 26 | val expr = or() 27 | 28 | if (match(ASSIGN)) { 29 | val value = assignment() 30 | 31 | if (expr is VariableExpr) { 32 | val name = expr.name 33 | 34 | return AssignExpr(name, value) 35 | } else { 36 | throw ExpressionException("Invalid assignment target") 37 | } 38 | } 39 | 40 | return expr 41 | } 42 | 43 | private fun or(): Expr { 44 | var expr = and() 45 | 46 | while (match(BAR_BAR)) { 47 | val operator = previous() 48 | val right = and() 49 | 50 | expr = LogicalExpr(expr, operator, right) 51 | } 52 | 53 | return expr 54 | } 55 | 56 | private fun and(): Expr { 57 | var expr = equality() 58 | 59 | while (match(AMP_AMP)) { 60 | val operator = previous() 61 | val right = equality() 62 | 63 | expr = LogicalExpr(expr, operator, right) 64 | } 65 | 66 | return expr 67 | } 68 | 69 | private fun equality(): Expr { 70 | var left = comparison() 71 | 72 | while (match(EQUAL_EQUAL, NOT_EQUAL)) { 73 | val operator = previous() 74 | val right = comparison() 75 | 76 | left = BinaryExpr(left, operator, right) 77 | } 78 | 79 | return left 80 | } 81 | 82 | private fun comparison(): Expr { 83 | var left = addition() 84 | 85 | while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) { 86 | val operator = previous() 87 | val right = addition() 88 | 89 | left = BinaryExpr(left, operator, right) 90 | } 91 | 92 | return left 93 | } 94 | 95 | private fun addition(): Expr { 96 | var left = multiplication() 97 | 98 | while (match(PLUS, MINUS)) { 99 | val operator = previous() 100 | val right = multiplication() 101 | 102 | left = BinaryExpr(left, operator, right) 103 | } 104 | 105 | return left 106 | } 107 | 108 | private fun multiplication(): Expr { 109 | var left = unary() 110 | 111 | while (match(STAR, SLASH, MODULO)) { 112 | val operator = previous() 113 | val right = unary() 114 | 115 | left = BinaryExpr(left, operator, right) 116 | } 117 | 118 | return left 119 | } 120 | 121 | private fun unary(): Expr { 122 | if (match(MINUS)) { 123 | val operator = previous() 124 | val right = unary() 125 | 126 | return UnaryExpr(operator, right) 127 | } 128 | 129 | return sqrt() 130 | } 131 | 132 | private fun sqrt(): Expr { 133 | if (match(SQUARE_ROOT)) { 134 | val operator = previous() 135 | val right = unary() 136 | 137 | return UnaryExpr(operator, right) 138 | } 139 | 140 | return exponent() 141 | } 142 | 143 | private fun exponent(): Expr { 144 | var left = call() 145 | 146 | if (match(EXPONENT)) { 147 | val operator = previous() 148 | val right = unary() 149 | 150 | left = BinaryExpr(left, operator, right) 151 | } 152 | 153 | return left 154 | } 155 | 156 | private fun call(): Expr { 157 | if (matchTwo(IDENTIFIER, LEFT_PAREN)) { 158 | val (name, _) = previousTwo() 159 | 160 | val arguments = mutableListOf() 161 | 162 | if (!check(RIGHT_PAREN)) { 163 | do { 164 | arguments += expression() 165 | } while (match(COMMA)) 166 | } 167 | 168 | consume(RIGHT_PAREN, "Expected ')' after function arguments") 169 | 170 | return CallExpr(name.lexeme, arguments) 171 | } 172 | 173 | return primary() 174 | } 175 | 176 | private fun primary(): Expr { 177 | if (match(NUMBER)) { 178 | return LiteralExpr(previous().literal as BigDecimal) 179 | } 180 | 181 | if (match(IDENTIFIER)) { 182 | return VariableExpr(previous()) 183 | } 184 | 185 | if (match(LEFT_PAREN)) { 186 | val expr = expression() 187 | 188 | consume(RIGHT_PAREN, "Expected ')' after '${previous().lexeme}'.") 189 | 190 | return GroupingExpr(expr) 191 | } 192 | 193 | throw ExpressionException("Expected expression after '${previous().lexeme}'.") 194 | } 195 | 196 | private fun match(vararg types: TokenType): Boolean { 197 | for (type in types) { 198 | if (check(type)) { 199 | advance() 200 | 201 | return true 202 | } 203 | } 204 | 205 | return false 206 | } 207 | 208 | private fun matchTwo(first: TokenType, second: TokenType): Boolean { 209 | val start = current 210 | 211 | if (match(first) && match(second)) { 212 | return true 213 | } 214 | 215 | current = start 216 | return false 217 | } 218 | 219 | private fun check(tokenType: TokenType): Boolean { 220 | return if (isAtEnd()) { 221 | false 222 | } else { 223 | peek().type === tokenType 224 | } 225 | } 226 | 227 | private fun consume(type: TokenType, message: String): Token { 228 | if (check(type)) return advance() 229 | 230 | throw ExpressionException(message) 231 | } 232 | 233 | private fun advance(): Token { 234 | if (!isAtEnd()) current++ 235 | 236 | return previous() 237 | } 238 | 239 | private fun isAtEnd() = peek().type == EOF 240 | 241 | private fun peek() = tokens[current] 242 | 243 | private fun previous() = tokens[current - 1] 244 | 245 | private fun previousTwo() = Pair(tokens[current - 2], tokens[current - 1]) 246 | 247 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/keelar/exprk/Expressions.kt: -------------------------------------------------------------------------------- 1 | package com.github.keelar.exprk 2 | 3 | import com.github.keelar.exprk.internal.* 4 | import com.github.keelar.exprk.internal.Function 5 | import java.math.BigDecimal 6 | import java.math.MathContext 7 | import java.math.RoundingMode 8 | 9 | class ExpressionException(message: String) 10 | : RuntimeException(message) 11 | 12 | @Suppress("unused") 13 | class Expressions { 14 | private val evaluator = Evaluator() 15 | 16 | init { 17 | define("pi", Math.PI) 18 | define("e", Math.E) 19 | 20 | evaluator.addFunction("abs", object : Function() { 21 | override fun call(arguments: List): BigDecimal { 22 | if (arguments.size != 1) throw ExpressionException( 23 | "abs requires one argument") 24 | 25 | return arguments.first().abs() 26 | } 27 | }) 28 | 29 | evaluator.addFunction("sum", object : Function() { 30 | override fun call(arguments: List): BigDecimal { 31 | if (arguments.isEmpty()) throw ExpressionException( 32 | "sum requires at least one argument") 33 | 34 | return arguments.reduce { sum, bigDecimal -> 35 | sum.add(bigDecimal) 36 | } 37 | } 38 | }) 39 | 40 | evaluator.addFunction("floor", object : Function() { 41 | override fun call(arguments: List): BigDecimal { 42 | if (arguments.size != 1) throw ExpressionException( 43 | "floor requires one argument") 44 | 45 | return arguments.first().setScale(0, RoundingMode.FLOOR) 46 | } 47 | }) 48 | 49 | evaluator.addFunction("ceil", object : Function() { 50 | override fun call(arguments: List): BigDecimal { 51 | if (arguments.size != 1) throw ExpressionException( 52 | "ceil requires one argument") 53 | 54 | return arguments.first().setScale(0, RoundingMode.CEILING) 55 | } 56 | }) 57 | 58 | evaluator.addFunction("round", object : Function() { 59 | override fun call(arguments: List): BigDecimal { 60 | if (arguments.size !in listOf(1, 2)) throw ExpressionException( 61 | "round requires either one or two arguments") 62 | 63 | val value = arguments.first() 64 | val scale = if (arguments.size == 2) arguments.last().toInt() else 0 65 | 66 | return value.setScale(scale, roundingMode) 67 | } 68 | }) 69 | 70 | evaluator.addFunction("min", object : Function() { 71 | override fun call(arguments: List): BigDecimal { 72 | if (arguments.isEmpty()) throw ExpressionException( 73 | "min requires at least one argument") 74 | 75 | return arguments.minOrNull()!! 76 | } 77 | }) 78 | 79 | evaluator.addFunction("max", object : Function() { 80 | override fun call(arguments: List): BigDecimal { 81 | if (arguments.isEmpty()) throw ExpressionException( 82 | "max requires at least one argument") 83 | 84 | return arguments.maxOrNull()!! 85 | } 86 | }) 87 | 88 | evaluator.addFunction("if", object : Function() { 89 | override fun call(arguments: List): BigDecimal { 90 | val condition = arguments[0] 91 | val thenValue = arguments[1] 92 | val elseValue = arguments[2] 93 | 94 | return if (condition != BigDecimal.ZERO) { 95 | thenValue 96 | } else { 97 | elseValue 98 | } 99 | } 100 | }) 101 | } 102 | 103 | val precision: Int 104 | get() = evaluator.mathContext.precision 105 | 106 | val roundingMode: RoundingMode 107 | get() = evaluator.mathContext.roundingMode 108 | 109 | fun setPrecision(precision: Int): Expressions { 110 | evaluator.mathContext = MathContext(precision, roundingMode) 111 | 112 | 113 | return this 114 | } 115 | 116 | fun setRoundingMode(roundingMode: RoundingMode): Expressions { 117 | evaluator.mathContext = MathContext(precision, roundingMode) 118 | 119 | return this 120 | } 121 | 122 | fun define(name: String, value: Long): Expressions { 123 | define(name, value.toString()) 124 | 125 | return this 126 | } 127 | 128 | fun define(name: String, value: Double): Expressions { 129 | define(name, value.toString()) 130 | 131 | return this 132 | } 133 | 134 | fun define(name: String, value: BigDecimal): Expressions { 135 | define(name, value.toPlainString()) 136 | 137 | return this 138 | } 139 | 140 | fun define(name: String, expression: String): Expressions { 141 | val expr = parse(expression) 142 | evaluator.define(name, expr) 143 | 144 | return this 145 | } 146 | 147 | fun addFunction(name: String, function: Function): Expressions { 148 | evaluator.addFunction(name, function) 149 | 150 | return this 151 | } 152 | 153 | fun addFunction(name: String, func: (List) -> BigDecimal): Expressions { 154 | evaluator.addFunction(name, object : Function() { 155 | override fun call(arguments: List): BigDecimal { 156 | return func(arguments) 157 | } 158 | 159 | }) 160 | 161 | return this 162 | } 163 | 164 | fun eval(expression: String): BigDecimal { 165 | return evaluator.eval(parse(expression)) 166 | } 167 | 168 | /** 169 | * eval an expression then round it with {@link Evaluator#mathContext} and call toEngineeringString
170 | * if error will return message from Throwable 171 | * @param expression String 172 | * @return String 173 | */ 174 | fun evalToString(expression: String): String { 175 | return try { 176 | evaluator.eval(parse(expression)).round(evaluator.mathContext).stripTrailingZeros() 177 | .toEngineeringString() 178 | }catch (e:Throwable){ 179 | e.cause?.message ?: e.message ?: "unknown error" 180 | } 181 | } 182 | 183 | private fun parse(expression: String): Expr { 184 | return parse(scan(expression)) 185 | } 186 | 187 | private fun parse(tokens: List): Expr { 188 | return Parser(tokens).parse() 189 | } 190 | 191 | private fun scan(expression: String): List { 192 | return Scanner(expression, evaluator.mathContext).scanTokens() 193 | } 194 | } --------------------------------------------------------------------------------