package com.lemenzo.calculator.engine import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext import java.math.RoundingMode import kotlin.math.* class ExpressionEngine { enum class AngleMode { DEG, RAD, GRAD } var angleMode = AngleMode.DEG private val mc = MathContext(34, RoundingMode.HALF_EVEN) private val transcendentalMc = MathContext(15, RoundingMode.HALF_EVEN) private val pi = BigDecimal("3.141592653589793238462643383279503", mc) private val eConst = BigDecimal("2.718281828459045235360287471352662", mc) private val phi = BigDecimal("1.618033988749894848204586834365638", mc) private data class Value(val number: BigDecimal, val percent: Boolean = false) fun evaluate(input: String): BigDecimal { require(input.length <= MAX_EXPRESSION_LENGTH) { "Expression too long" } val parser = Parser(input.replace("×", "*").replace("÷", "/").replace("−", "-")) val value = parser.parseExpression().number parser.skipWs() require(parser.end()) { "Unexpected input" } return value.stripTrailingZeros() } fun format(v: BigDecimal): String { if (v.compareTo(BigDecimal.ZERO) == 0) return "0" val rounded = v.round(mc).stripTrailingZeros() val a = rounded.abs() return if (a >= BigDecimal("1E15") || a < BigDecimal("1E-10")) rounded.toEngineeringString() else rounded.toPlainString() } private inner class Parser(private val s: String) { var i = 0 fun end() = i >= s.length fun skipWs() { while (!end() && s[i].isWhitespace()) i++ } fun parseExpression(): Value { var left = parseTerm() while (true) { skipWs() when { eat('+') -> { val right = parseTerm() val rhs = if (right.percent) left.number.multiply(right.number, mc) else right.number left = Value(left.number.add(rhs, mc)) } eat('-') -> { val right = parseTerm() val rhs = if (right.percent) left.number.multiply(right.number, mc) else right.number left = Value(left.number.subtract(rhs, mc)) } else -> return left } } } fun parseTerm(): Value { var left = parseUnary() while (true) { skipWs() when { eat('*') -> { val right = parseUnary() left = Value(left.number.multiply(right.number, mc)) } eat('/') -> { val right = parseUnary() require(right.number.compareTo(BigDecimal.ZERO) != 0) { "Division by zero" } left = Value(left.number.divide(right.number, mc)) } else -> return left } } } fun parsePower(): Value { var value = parsePrimary() skipWs() if (eat('^')) { // Exponent uses unary parsing so 2^-3 works, while unary itself // sits below power precedence so -2^2 is -(2^2), matching // conventional scientific-calculator semantics. val exponent = parseUnary().number value = Value(power(value.number, exponent)) } return value } fun parseUnary(): Value { skipWs() if (eat('+')) return parseUnary() if (eat('-')) return Value(parseUnary().number.negate(mc)) return parsePower() } fun parsePrimary(): Value { skipWs() if (eat('(')) { val value = parseExpression() require(eat(')')) { "Missing )" } return postfix(value) } if (!end() && (s[i].isLetter() || s[i] == '√')) { val name = readName() if (name == "pi") return postfix(Value(pi)) if (name == "e") return postfix(Value(eConst)) if (name == "phi") return postfix(Value(phi)) require(eat('(')) { "Missing (" } val first = parseExpression().number val value = if (name == "ncr" || name == "npr") { require(eat(',')) { "Missing ," } val second = parseExpression().number require(eat(')')) { "Missing )" } if (name == "ncr") combinations(first, second) else permutations(first, second) } else { require(eat(')')) { "Missing )" } function(name, first) } return postfix(Value(value)) } val start = i while (!end() && (s[i].isDigit() || s[i] == '.' || s[i] == 'E' || s[i] == 'e' || ((s[i] == '+' || s[i] == '-') && i > start && (s[i - 1] == 'E' || s[i - 1] == 'e')))) i++ require(i > start) { "Number expected" } return postfix(Value(BigDecimal(s.substring(start, i), mc))) } fun postfix(v0: Value): Value { var v = v0 while (true) { skipWs() v = when { eat('!') -> Value(factorial(v.number)) eat('%') -> Value(v.number.divide(BigDecimal("100"), mc), true) else -> return v } } } fun eat(c: Char): Boolean { skipWs() if (!end() && s[i] == c) { i++; return true } return false } fun readName(): String { if (s[i] == '√') { i++; return "sqrt" } val start = i while (!end() && s[i].isLetterOrDigit()) i++ return s.substring(start, i).lowercase() } } private fun power(base: BigDecimal, exponent: BigDecimal): BigDecimal { return try { val n = exponent.intValueExact() require(kotlin.math.abs(n.toLong()) <= MAX_INTEGER_EXPONENT) { "Exponent range" } if (n >= 0) base.pow(n, mc) else { require(base.compareTo(BigDecimal.ZERO) != 0) { "Division by zero" } BigDecimal.ONE.divide(base.pow(-n, mc), mc) } } catch (_: ArithmeticException) { bd(base.toDouble().pow(exponent.toDouble())) } } private fun function(name: String, x: BigDecimal): BigDecimal { val d = x.toDouble() return bd(when (name) { "sin" -> sin(angle(d)); "cos" -> cos(angle(d)); "tan" -> { val radians = angle(d) require(abs(cos(radians)) > 1e-14) { "Math error" } tan(radians) } "asin" -> { require(d in -1.0..1.0) { "Math error" }; unangle(asin(d)) } "acos" -> { require(d in -1.0..1.0) { "Math error" }; unangle(acos(d)) } "atan" -> unangle(atan(d)) "sinh" -> sinh(d); "cosh" -> cosh(d); "tanh" -> tanh(d) "sqrt" -> { require(x.signum() >= 0) { "Math error" }; return x.sqrt(mc) } "cbrt" -> cbrt(d) "ln" -> { require(d > 0.0) { "Math error" }; ln(d) } "log" -> { require(d > 0.0) { "Math error" }; log10(d) } "log2" -> { require(d > 0.0) { "Math error" }; ln(d) / ln(2.0) } "abs" -> return x.abs(mc) "floor" -> return x.setScale(0, RoundingMode.FLOOR) "ceil" -> return x.setScale(0, RoundingMode.CEILING) "exp" -> exp(d) else -> error("Unknown function") }) } private fun combinations(nv: BigDecimal, rv: BigDecimal): BigDecimal { val n = nv.intValueExact(); val r = rv.intValueExact() require(n >= 0 && r >= 0 && r <= n && n <= 10000) { "Combination range" } val k = minOf(r, n - r) var result = BigInteger.ONE for (i in 1..k) result = result.multiply(BigInteger.valueOf((n - k + i).toLong())).divide(BigInteger.valueOf(i.toLong())) return result.toBigDecimal() } private fun permutations(nv: BigDecimal, rv: BigDecimal): BigDecimal { val n = nv.intValueExact(); val r = rv.intValueExact() require(n >= 0 && r >= 0 && r <= n && n <= 10000) { "Permutation range" } var result = BigInteger.ONE for (i in 0 until r) result = result.multiply(BigInteger.valueOf((n - i).toLong())) return result.toBigDecimal() } private fun angle(d: Double) = when (angleMode) { AngleMode.DEG -> Math.toRadians(d) AngleMode.RAD -> d AngleMode.GRAD -> d * Math.PI / 200.0 } private fun unangle(d: Double) = when (angleMode) { AngleMode.DEG -> Math.toDegrees(d) AngleMode.RAD -> d AngleMode.GRAD -> d * 200.0 / Math.PI } private fun bd(d: Double): BigDecimal { require(d.isFinite()) { "Math error" } val normalized = when { abs(d) < 1e-15 -> 0.0 abs(d - d.roundToLong()) < 1e-14 -> d.roundToLong().toDouble() else -> d } return BigDecimal.valueOf(normalized).round(transcendentalMc) } private fun factorial(v: BigDecimal): BigDecimal { val n = v.intValueExact() require(n in 0..5000) { "Factorial range" } var result = BigInteger.ONE for (k in 2..n) result = result.multiply(BigInteger.valueOf(k.toLong())) return result.toBigDecimal() } private companion object { const val MAX_EXPRESSION_LENGTH = 512 const val MAX_INTEGER_EXPONENT = 10000L } }