feat(function): add edge and pad to ScalarFunctions

This commit is contained in:
dualfroz committed 2026-09-02 17:23:13 +02:00
1 parent 8c2043edf9
commit 16da420d79
1 file changed
+88
@@ -161,3 +161,91 @@ object ScalarFunctions {
return Value.TextValue(if (start >= end) "" else source.substring(start, end))
}
/** `LEFT(str, n)`/`RIGHT(str, n)`: the first/last `n` characters, clamped to `[0, str.length]` (a non-positive `n` is empty, an `n` past the string's length is the whole string). */
private fun edge(args: List<Value>, function: String, fromStart: Boolean): String {
val source = text(args[0], function)
val count = integer(args[1], function).coerceIn(0, source.length.toLong()).toInt()
return if (fromStart) source.substring(0, count) else source.substring(source.length - count)
}
/**
* MySQL-compatible LPAD/RPAD: the result is always exactly [target length][Value.IntegerValue]
* characters (never longer) - a source already at or past that length is truncated to it (from
* the start, regardless of which side would have been padded), and a source shorter than it is
* padded by cycling [padString] (default a single space) until the target length is reached.
*/
private fun pad(args: List<Value>, function: String, left: Boolean): Value {
val source = text(args[0], function)
val targetLength = integer(args[1], function)
if (targetLength <= 0) return Value.TextValue("")
val length = targetLength.toInt()
if (length <= source.length) return Value.TextValue(source.substring(0, length))
val padString = if (args.size == 3) text(args[2], function) else " "
if (padString.isEmpty()) throw KestrelException("function $function requires a non-empty pad string")
val needed = length - source.length
val padding = buildString {
while (this.length < needed) append(padString)
}.substring(0, needed)
return Value.TextValue(if (left) padding + source else source + padding)
}
/** Two INTEGER operands with a non-negative exponent stay exact (repeated Long multiplication); anything else - a negative exponent or a REAL operand on either side - falls back to `Math.pow`. */
private fun power(base: Value, exponent: Value): Value {
if (base is Value.IntegerValue && exponent is Value.IntegerValue && exponent.value >= 0) {
var result = 1L
repeat(exponent.value.toInt()) { result *= base.value }
return Value.IntegerValue(result)
}
return Value.RealValue(number(base, "POWER").pow(number(exponent, "POWER")))
}
/** Two INTEGER operands use exact `Long` remainder; a REAL operand on either side uses floating-point remainder instead. */
private fun mod(dividend: Value, divisor: Value): Value {
if (dividend is Value.IntegerValue && divisor is Value.IntegerValue) {
if (divisor.value == 0L) throw KestrelException("division by zero")
return Value.IntegerValue(dividend.value % divisor.value)
}
val divisorNumber = number(divisor, "MOD")
if (divisorNumber == 0.0) throw KestrelException("division by zero")
return Value.RealValue(number(dividend, "MOD") % divisorNumber)
}
private fun round(args: List<Value>): Value {
val digits = if (args.size == 2) {
(args[1] as? Value.IntegerValue)?.value?.toInt()
?: throw KestrelException("function ROUND requires an INTEGER digit count")
} else {
0
}
if (args[0] is Value.IntegerValue && digits >= 0) return args[0]
val decimal = args[0] as? Value.DecimalValue
if (decimal != null) return Value.DecimalValue(decimal.value.setScale(digits, RoundingMode.HALF_UP))
val value = number(args[0], "ROUND")
val factor = 10.0.pow(digits)
return Value.RealValue((value * factor).roundToLong() / factor)
}
private fun sign(value: Value): Int = when (value) {
is Value.IntegerValue -> if (value.value > 0) 1 else if (value.value < 0) -1 else 0
is Value.RealValue -> if (value.value > 0) 1 else if (value.value < 0) -1 else 0
is Value.DecimalValue -> value.value.signum()
else -> throw KestrelException("function SIGN requires a numeric argument but got ${value.typeName}")
}
/** Same digit-count/type handling as [round], but truncates toward zero (`RoundingMode.DOWN`) instead of rounding half-up. */
private fun trunc(args: List<Value>): Value {
val digits = if (args.size == 2) {
(args[1] as? Value.IntegerValue)?.value?.toInt()
?: throw KestrelException("function TRUNC requires an INTEGER digit count")
} else {
0
}
if (args[0] is Value.IntegerValue && digits >= 0) return args[0]
val decimal = args[0] as? Value.DecimalValue
if (decimal != null) return Value.DecimalValue(decimal.value.setScale(digits, RoundingMode.DOWN))
val value = number(args[0], "TRUNC")
val factor = 10.0.pow(digits)
val scaled = value * factor
return Value.RealValue((if (scaled >= 0) floor(scaled) else ceil(scaled)) / factor)
}
}