feat(function): add edge and pad to ScalarFunctions
This commit is contained in:
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))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user