package com.vgmlr.grain
import java.math.BigDecimal
import java.math.RoundingMode
private const val OPS = "+-×/"
private const val MAX_SCALE = 12
internal const val ERR = "Error"
private val NUM = Regex("""(?:\d+\.?\d*|\.\d+)(?:E-?\d+)?%?""")
private val TAIL = Regex("""\d*\.?\d*(?:E-?\d+)?%?$""")
private data class Num(val v: Double, val pct: Boolean)
internal object GrainCalc {
fun isOp(c: Char) = c in OPS
// trailing number
fun tail(expr: String): String = TAIL.find(expr)?.value ?: ""
// groups first
fun eval(expr: String): Double? = run(expr)?.first
// has ops
fun isCompound(expr: String): Boolean = run(expr)?.second ?: false
// auto-close
fun close(expr: String): String {
val t = trim(expr)
return t + ")".repeat((t.count { it == '(' } - t.count { it == ')' }).coerceAtLeast(0))
}
fun openCount(expr: String): Int = expr.count { it == '(' } - expr.count { it == ')' }
private fun trim(expr: String) = expr.trimEnd { isOp(it) || it == '(' }
private fun run(expr: String): Pair<Double, Boolean>? {
val p = Parser(trim(expr))
val v = p.expr() ?: return null
if (!p.done) return null // stray )
return v to p.compound
}
// plain, half-up
fun fmt(v: Double, places: Int? = null): String {
if (v.isNaN() || v.isInfinite()) return ERR
if (v == 0.0) return "0"
val b = BigDecimal.valueOf(v)
val r = places?.let { round(b, it) } ?: b
return if (r.signum() == 0) "0" else r.stripTrailingZeros().toPlainString()
}
private fun round(b: BigDecimal, places: Int): BigDecimal {
if (places == 0) return b.setScale(0, RoundingMode.HALF_UP)
val a = b.abs()
val lead = if (a < BigDecimal.ONE) a.scale() - a.precision() else 0 // zero run
val scale = minOf(lead + places, MAX_SCALE)
val r = b.setScale(scale, RoundingMode.HALF_UP)
val whole = { x: BigDecimal -> x.abs().setScale(0, RoundingMode.DOWN) }
return if (whole(r) > whole(b)) b.setScale(scale, RoundingMode.DOWN) else r // int bump
}
fun parse(raw: String): Double? =
raw.trim().replace(",", "").replace(" ", "")
.toDoubleOrNull()?.takeIf { it.isFinite() }
private class Parser(private val s: String) {
private var i = 0
var compound = false
private set
val done: Boolean get() = i >= s.length
// linear
fun expr(): Double? {
val first = operand() ?: return null
if (first.pct) compound = true
var acc = if (first.pct) first.v / 100 else first.v
while (i < s.length && s[i] != ')') {
val op = s[i]
if (!isOp(op)) return null
i++
val n = operand() ?: return null
compound = true
val b = when {
!n.pct -> n.v
op == '+' || op == '-' -> acc * n.v / 100
else -> n.v / 100
}
acc = when (op) {
'+' -> acc + b
'-' -> acc - b
'×' -> acc * b
else -> acc / b
}
}
return acc
}
private fun operand(): Num? {
if (i >= s.length) return null
return when (s[i]) {
'-' -> {
i++
operand()?.let { Num(-it.v, it.pct) } // unary
}
'(' -> {
i++
compound = true
val v = expr() ?: return null
if (i < s.length) i++ // consume )
Num(v, false)
}
else -> {
val m = NUM.find(s, i)?.takeIf { it.range.first == i } ?: return null
i = m.range.last + 1
val v = m.value.removeSuffix("%").toDoubleOrNull() ?: return null
Num(v, m.value.endsWith('%'))
}
}
}
}
}