fixed Decimal (and other such classes ) comparison
This commit is contained in:
parent
05d7432b37
commit
aa9565b40b
@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import com.ionspin.kotlin.bignum.decimal.DecimalMode
|
||||
import com.ionspin.kotlin.bignum.decimal.RoundingMode
|
||||
import com.ionspin.kotlin.bignum.integer.BigInteger
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
import net.sergeych.lyng.requiredArg
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal
|
||||
|
||||
object ObjDecimalSupport {
|
||||
private const val decimalContextVar = "__lyng_decimal_context__"
|
||||
// For Real -> Decimal, preserve the actual IEEE-754 Double value using a
|
||||
// round-trip-safe precision. This intentionally does not try to recover source text.
|
||||
private val realConversionMode = DecimalMode(17L, RoundingMode.ROUND_HALF_TO_EVEN)
|
||||
// Division needs an explicit stopping rule for non-terminating results. Use a
|
||||
// decimal128-like default context until Lyng exposes per-operation contexts.
|
||||
private val defaultDivisionMode = DecimalMode(34L, RoundingMode.ROUND_HALF_TO_EVEN)
|
||||
private val zero: IonBigDecimal = IonBigDecimal.ZERO
|
||||
private val decimalTypeDecl = TypeDecl.Simple("lyng.decimal.Decimal", false)
|
||||
private object BoundMarker
|
||||
private data class DecimalRuntimeContext(
|
||||
val precision: Long,
|
||||
val rounding: RoundingMode
|
||||
) : Obj()
|
||||
|
||||
suspend fun bindTo(module: ModuleScope) {
|
||||
val decimalClass = module.requireClass("Decimal")
|
||||
if (decimalClass.kotlinClassData === BoundMarker) return
|
||||
decimalClass.kotlinClassData = BoundMarker
|
||||
decimalClass.isAbstract = false
|
||||
val hooks = decimalClass.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
|
||||
decimalClass.bridgeInitHooks = it
|
||||
}
|
||||
hooks += { _, instance ->
|
||||
instance.kotlinInstanceData = zero
|
||||
}
|
||||
decimalClass.addFn("plus") {
|
||||
newInstance(decimalClass, valueOf(thisObj).plus(coerceArg(requireScope(), args.firstAndOnly())))
|
||||
}
|
||||
decimalClass.addFn("minus") {
|
||||
newInstance(decimalClass, valueOf(thisObj).minus(coerceArg(requireScope(), args.firstAndOnly())))
|
||||
}
|
||||
decimalClass.addFn("mul") {
|
||||
newInstance(decimalClass, valueOf(thisObj).times(coerceArg(requireScope(), args.firstAndOnly())))
|
||||
}
|
||||
decimalClass.addFn("div") {
|
||||
newInstance(decimalClass, divideWithContext(valueOf(thisObj), coerceArg(requireScope(), args.firstAndOnly()), currentDivisionMode(requireScope())))
|
||||
}
|
||||
decimalClass.addFn("mod") {
|
||||
newInstance(decimalClass, valueOf(thisObj).rem(coerceArg(requireScope(), args.firstAndOnly())))
|
||||
}
|
||||
decimalClass.addFn("compareTo") {
|
||||
ObjInt.of(valueOf(thisObj).compareTo(coerceArg(requireScope(), args.firstAndOnly())).toLong())
|
||||
}
|
||||
decimalClass.addFn("negate") {
|
||||
newInstance(decimalClass, valueOf(thisObj).unaryMinus())
|
||||
}
|
||||
decimalClass.addFn("toInt") {
|
||||
ObjInt.of(valueOf(thisObj).longValue(false))
|
||||
}
|
||||
decimalClass.addFn("toReal") {
|
||||
ObjReal.of(valueOf(thisObj).doubleValue(false))
|
||||
}
|
||||
decimalClass.addFn("toString") {
|
||||
ObjString(valueOf(thisObj).toStringExpanded())
|
||||
}
|
||||
decimalClass.addFn("toStringExpanded") {
|
||||
ObjString(valueOf(thisObj).toStringExpanded())
|
||||
}
|
||||
decimalClass.addClassFn("fromInt") {
|
||||
val value = requiredArg<ObjInt>(0).value
|
||||
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
|
||||
}
|
||||
decimalClass.addClassFn("fromReal") {
|
||||
val value = requiredArg<ObjReal>(0).value
|
||||
newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode))
|
||||
}
|
||||
decimalClass.addClassFn("fromString") {
|
||||
val value = requiredArg<ObjString>(0).value
|
||||
try {
|
||||
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
||||
} catch (e: Throwable) {
|
||||
requireScope().raiseIllegalArgument("invalid Decimal string: $value")
|
||||
}
|
||||
}
|
||||
module.addFn("withDecimalContext") {
|
||||
val (context, block) = when (args.list.size) {
|
||||
2 -> {
|
||||
val first = args[0]
|
||||
val block = args[1]
|
||||
if (first is ObjInt) {
|
||||
DecimalRuntimeContext(first.value, RoundingMode.ROUND_HALF_TO_EVEN) to block
|
||||
} else {
|
||||
normalizeContext(requireScope(), first) to block
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
val precision = requiredArg<ObjInt>(0).value
|
||||
val rounding = roundingModeFromObj(requireScope(), args[1])
|
||||
DecimalRuntimeContext(precision, rounding) to args[2]
|
||||
}
|
||||
else -> requireScope().raiseIllegalArgument("withDecimalContext expects (context, block), (precision, block), or (precision, rounding, block)")
|
||||
}
|
||||
val child = requireScope().createChildScope()
|
||||
child.addConst(decimalContextVar, context)
|
||||
block.callOn(child)
|
||||
}
|
||||
registerBuiltinConversions(decimalClass)
|
||||
registerInterop(decimalClass)
|
||||
}
|
||||
|
||||
fun isDecimalValue(value: Obj): Boolean =
|
||||
value is ObjInstance && value.objClass.className == "Decimal"
|
||||
|
||||
suspend fun exactAbs(scope: ScopeFacade, value: Obj): Obj? =
|
||||
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.abs()) }
|
||||
|
||||
suspend fun exactFloor(scope: ScopeFacade, value: Obj): Obj? =
|
||||
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.floor()) }
|
||||
|
||||
suspend fun exactCeil(scope: ScopeFacade, value: Obj): Obj? =
|
||||
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.ceil()) }
|
||||
|
||||
suspend fun exactRound(scope: ScopeFacade, value: Obj): Obj? =
|
||||
decimalValueOrNull(value)?.let {
|
||||
scope.newInstanceLikeDecimal(value, it.roundToDigitPositionAfterDecimalPoint(0, RoundingMode.ROUND_HALF_CEILING))
|
||||
}
|
||||
|
||||
suspend fun exactPow(scope: ScopeFacade, base: Obj, exponent: Obj): Obj? {
|
||||
val decimal = decimalValueOrNull(base) ?: return null
|
||||
val intExponent = exponent as? ObjInt ?: return null
|
||||
return scope.newInstanceLikeDecimal(base, decimal.pow(intExponent.value))
|
||||
}
|
||||
|
||||
suspend fun fromRealLike(scope: ScopeFacade, sample: Obj, value: Double): Obj? {
|
||||
if (!isDecimalValue(sample)) return null
|
||||
return scope.newInstanceLikeDecimal(sample, IonBigDecimal.fromDouble(value, realConversionMode))
|
||||
}
|
||||
|
||||
fun toDoubleOrNull(value: Obj): Double? =
|
||||
decimalValueOrNull(value)?.doubleValue(false)
|
||||
|
||||
suspend fun newDecimal(scope: ScopeFacade, value: IonBigDecimal): ObjInstance {
|
||||
val decimalModule = scope.requireScope().currentImportProvider.createModuleScope(scope.pos, "lyng.decimal")
|
||||
val decimalClass = decimalModule.requireClass("Decimal")
|
||||
return scope.newInstance(decimalClass, value)
|
||||
}
|
||||
|
||||
private fun valueOf(obj: Obj): IonBigDecimal {
|
||||
val instance = obj as? ObjInstance ?: error("Decimal receiver must be an object instance")
|
||||
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
|
||||
}
|
||||
|
||||
private suspend fun currentDivisionMode(scope: Scope): DecimalMode {
|
||||
val context = findContextObject(scope) ?: return defaultDivisionMode
|
||||
return DecimalMode(context.precision, context.rounding)
|
||||
}
|
||||
|
||||
private fun divideWithContext(left: IonBigDecimal, right: IonBigDecimal, mode: DecimalMode): IonBigDecimal {
|
||||
if (mode.decimalPrecision <= 0L) {
|
||||
return stripMode(left.divide(right, mode))
|
||||
}
|
||||
val exactLeft = stripMode(left)
|
||||
val exactRight = stripMode(right)
|
||||
val guardMode = DecimalMode(mode.decimalPrecision + 2, RoundingMode.TOWARDS_ZERO)
|
||||
var guarded = stripMode(exactLeft.divide(exactRight, guardMode))
|
||||
val hasMoreTail = !stripMode(exactLeft - stripMode(guarded * exactRight)).isZero()
|
||||
if (hasMoreTail && isHalfRounding(mode.roundingMode) && looksLikeExactHalf(guarded, mode.decimalPrecision)) {
|
||||
guarded = nudgeLastDigitAwayFromZero(guarded)
|
||||
}
|
||||
return stripMode(guarded.roundSignificand(mode))
|
||||
}
|
||||
|
||||
private suspend fun ScopeFacade.newInstance(decimalClass: ObjClass, value: IonBigDecimal): ObjInstance {
|
||||
val instance = call(decimalClass) as? ObjInstance
|
||||
?: raiseIllegalState("Decimal() did not return an object instance")
|
||||
instance.kotlinInstanceData = value
|
||||
return instance
|
||||
}
|
||||
|
||||
private suspend fun ScopeFacade.newInstanceLikeDecimal(sample: Obj, value: IonBigDecimal): ObjInstance {
|
||||
val decimalClass = (sample as? ObjInstance)?.objClass
|
||||
?: raiseIllegalState("Decimal sample must be an object instance")
|
||||
return newInstance(decimalClass, value)
|
||||
}
|
||||
|
||||
private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) {
|
||||
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
|
||||
is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode)
|
||||
is ObjInstance -> {
|
||||
if (value.objClass.className != "Decimal") {
|
||||
scope.raiseIllegalArgument("expected Decimal-compatible value, got ${value.objClass.className}")
|
||||
}
|
||||
value.kotlinInstanceData as? IonBigDecimal ?: zero
|
||||
}
|
||||
else -> scope.raiseIllegalArgument("expected Decimal-compatible value, got ${value.objClass.className}")
|
||||
}
|
||||
|
||||
private suspend fun normalizeContext(scope: Scope, value: Obj): DecimalRuntimeContext {
|
||||
val instance = value as? ObjInstance
|
||||
?: scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
|
||||
if (instance.objClass.className != "DecimalContext") {
|
||||
scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
|
||||
}
|
||||
return decimalRuntimeContextFromInstance(scope, instance)
|
||||
}
|
||||
|
||||
private fun findContextObject(scope: Scope): DecimalRuntimeContext? {
|
||||
var current: Scope? = scope
|
||||
while (current != null) {
|
||||
val record = current.objects[decimalContextVar] ?: current.localBindings[decimalContextVar]
|
||||
val value = when (val raw = record?.value) {
|
||||
is FrameSlotRef -> raw.peekValue()
|
||||
is RecordSlotRef -> raw.peekValue()
|
||||
else -> raw
|
||||
}
|
||||
when (value) {
|
||||
is DecimalRuntimeContext -> return value
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun decimalRuntimeContextFromInstance(scope: Scope, context: ObjInstance): DecimalRuntimeContext {
|
||||
val precision = context.readField(scope, "precision").value as? ObjInt
|
||||
?: scope.raiseClassCastError("DecimalContext.precision must be Int")
|
||||
if (precision.value <= 0L) {
|
||||
scope.raiseIllegalArgument("DecimalContext precision must be positive")
|
||||
}
|
||||
val rounding = roundingModeFromObj(scope, context.readField(scope, "rounding").value)
|
||||
return DecimalRuntimeContext(precision.value, rounding)
|
||||
}
|
||||
|
||||
private fun stripMode(value: IonBigDecimal): IonBigDecimal =
|
||||
IonBigDecimal.fromBigIntegerWithExponent(value.significand, value.exponent)
|
||||
|
||||
private fun isHalfRounding(mode: RoundingMode): Boolean = when (mode) {
|
||||
RoundingMode.ROUND_HALF_TO_EVEN,
|
||||
RoundingMode.ROUND_HALF_AWAY_FROM_ZERO,
|
||||
RoundingMode.ROUND_HALF_TOWARDS_ZERO,
|
||||
RoundingMode.ROUND_HALF_CEILING,
|
||||
RoundingMode.ROUND_HALF_FLOOR,
|
||||
RoundingMode.ROUND_HALF_TO_ODD -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun looksLikeExactHalf(value: IonBigDecimal, targetPrecision: Long): Boolean {
|
||||
val digits = value.significand.abs().toString(10)
|
||||
if (digits.length <= targetPrecision) return false
|
||||
val discarded = digits.substring(targetPrecision.toInt())
|
||||
return discarded[0] == '5' && discarded.drop(1).all { it == '0' }
|
||||
}
|
||||
|
||||
private fun nudgeLastDigitAwayFromZero(value: IonBigDecimal): IonBigDecimal {
|
||||
val ulpExponent = value.exponent - value.precision + 1
|
||||
val ulp = IonBigDecimal.fromBigIntegerWithExponent(BigInteger.ONE, ulpExponent)
|
||||
return if (value.significand.signum() < 0) value - ulp else value + ulp
|
||||
}
|
||||
|
||||
private fun roundingModeFromObj(scope: Scope, value: Obj): RoundingMode {
|
||||
val entry = value as? ObjEnumEntry ?: scope.raiseClassCastError("DecimalContext.rounding must be DecimalRounding")
|
||||
return when (entry.name.value) {
|
||||
"HalfEven" -> RoundingMode.ROUND_HALF_TO_EVEN
|
||||
"HalfAwayFromZero" -> RoundingMode.ROUND_HALF_AWAY_FROM_ZERO
|
||||
"HalfTowardsZero" -> RoundingMode.ROUND_HALF_TOWARDS_ZERO
|
||||
"Ceiling" -> RoundingMode.CEILING
|
||||
"Floor" -> RoundingMode.FLOOR
|
||||
"AwayFromZero" -> RoundingMode.AWAY_FROM_ZERO
|
||||
"TowardsZero" -> RoundingMode.TOWARDS_ZERO
|
||||
else -> scope.raiseIllegalArgument("unsupported DecimalRounding: ${entry.name.value}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun decimalValueOrNull(value: Obj): IonBigDecimal? {
|
||||
if (!isDecimalValue(value)) return null
|
||||
val instance = value as ObjInstance
|
||||
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
|
||||
}
|
||||
|
||||
private fun registerBuiltinConversions(decimalClass: ObjClass) {
|
||||
ObjInt.type.addPropertyDoc(
|
||||
name = "d",
|
||||
doc = "Convert this integer to a Decimal.",
|
||||
type = type("lyng.decimal.Decimal"),
|
||||
moduleName = "lyng.decimal",
|
||||
getter = { newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(thisAs<ObjInt>().value)) }
|
||||
)
|
||||
ObjInt.type.members["d"] = ObjInt.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
|
||||
ObjReal.type.addPropertyDoc(
|
||||
name = "d",
|
||||
doc = "Convert this real number to a Decimal by preserving the current IEEE-754 value with 17 significant digits and half-even rounding.",
|
||||
type = type("lyng.decimal.Decimal"),
|
||||
moduleName = "lyng.decimal",
|
||||
getter = { newInstance(decimalClass, IonBigDecimal.fromDouble(thisAs<ObjReal>().value, realConversionMode)) }
|
||||
)
|
||||
ObjReal.type.members["d"] = ObjReal.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
|
||||
ObjString.type.addPropertyDoc(
|
||||
name = "d",
|
||||
doc = "Parse this string as a Decimal.",
|
||||
type = type("lyng.decimal.Decimal"),
|
||||
moduleName = "lyng.decimal",
|
||||
getter = {
|
||||
val value = thisAs<ObjString>().value
|
||||
try {
|
||||
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
||||
} catch (e: Throwable) {
|
||||
requireScope().raiseIllegalArgument("invalid Decimal string: $value")
|
||||
}
|
||||
}
|
||||
)
|
||||
ObjString.type.members["d"] = ObjString.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
|
||||
}
|
||||
|
||||
private fun registerInterop(decimalClass: ObjClass) {
|
||||
OperatorInteropRegistry.register(
|
||||
leftClass = ObjInt.type,
|
||||
rightClass = decimalClass,
|
||||
commonClass = decimalClass,
|
||||
operatorNames = listOf(
|
||||
InteropOperator.Plus.name,
|
||||
InteropOperator.Minus.name,
|
||||
InteropOperator.Mul.name,
|
||||
InteropOperator.Div.name,
|
||||
InteropOperator.Mod.name,
|
||||
InteropOperator.Compare.name,
|
||||
InteropOperator.Equals.name
|
||||
),
|
||||
leftToCommon = ObjExternCallable.fromBridge {
|
||||
val value = requiredArg<ObjInt>(0).value
|
||||
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
|
||||
},
|
||||
rightToCommon = ObjExternCallable.fromBridge {
|
||||
requiredArg<Obj>(0)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -587,6 +587,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
val explicitCompare = objClass.getInstanceMemberOrNull("compareTo", includeStatic = false)
|
||||
if (explicitCompare != null) {
|
||||
return invokeInstanceMethod(scope, "compareTo", Arguments(other)).cast<ObjInt>(scope).toInt()
|
||||
}
|
||||
if (other !is ObjInstance || other.objClass != objClass) {
|
||||
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||
return -1
|
||||
|
||||
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DecimalModuleTest {
|
||||
@Test
|
||||
fun testDecimalModuleFactoriesAndConversions() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals("12.34", Decimal.fromString("12.34").toStringExpanded())
|
||||
assertEquals("1", Decimal.fromInt(1).toStringExpanded())
|
||||
assertEquals("2.5", "2.5".d.toStringExpanded())
|
||||
assertEquals("1", 1.d.toStringExpanded())
|
||||
assertEquals("2.2", 2.2.d.toStringExpanded())
|
||||
assertEquals("3", (1 + 2).d.toStringExpanded())
|
||||
assertEquals("1.5", (1 + 0.5).d.toStringExpanded())
|
||||
assertEquals("3", (1 + 2.d).toStringExpanded())
|
||||
assertEquals("3", (2.d + 1).toStringExpanded())
|
||||
assertEquals(2.5, "2.5".d.toReal())
|
||||
assertEquals(2, "2.5".d.toInt())
|
||||
assertEquals(2.2, 2.2.d.toReal())
|
||||
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecimalModuleMixedIntOperators() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals(3.d, 1 + 2.d)
|
||||
assertEquals(3.d, 2.d + 1)
|
||||
assertEquals(1.d, 3 - 2.d)
|
||||
assertEquals(1.d, 3.d - 2)
|
||||
assertEquals(8.d, 4 * 2.d)
|
||||
assertEquals(8.d, 4.d * 2)
|
||||
assertEquals(4.d, 8 / 2.d)
|
||||
assertEquals(4.d, 8.d / 2)
|
||||
assertEquals(1.d, 7 % 2.d)
|
||||
assertEquals(1.d, 7.d % 2)
|
||||
assert(1 < 2.d)
|
||||
assert(2 <= 2.d)
|
||||
assert(3 > 2.d)
|
||||
assert(3.d > 2)
|
||||
assert(2 == 2.d)
|
||||
assert(2.d == 2)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecimalDivisionUsesDefaultContext() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals("0.125", (1.d / 8.d).toStringExpanded())
|
||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||
assertEquals("0.6666666666666666666666666666666667", ("2".d / 3.d).toStringExpanded())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithDecimalContextOverridesDivisionContext() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||
assertEquals("0.3333333333", withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
||||
assertEquals("0.666667", withDecimalContext(6) { ("2".d / 3.d).toStringExpanded() })
|
||||
assertEquals("0.666667", withDecimalContext(DecimalContext(6)) { ("2".d / 3.d).toStringExpanded() })
|
||||
assertEquals("0.12", withDecimalContext(2) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("0.13", withDecimalContext(DecimalContext(2, DecimalRounding.HalfAwayFromZero)) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecimalDivisionRoundingMatrix() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (-1.d / 8.d).toStringExpanded() })
|
||||
|
||||
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
||||
|
||||
assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
||||
|
||||
assertEquals("0.13", withDecimalContext(2, DecimalRounding.Ceiling) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.Ceiling) { (-1.d / 8.d).toStringExpanded() })
|
||||
|
||||
assertEquals("0.12", withDecimalContext(2, DecimalRounding.Floor) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.Floor) { (-1.d / 8.d).toStringExpanded() })
|
||||
|
||||
assertEquals("0.13", withDecimalContext(2, DecimalRounding.AwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.AwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
||||
|
||||
assertEquals("0.12", withDecimalContext(2, DecimalRounding.TowardsZero) { (1.d / 8.d).toStringExpanded() })
|
||||
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.TowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDefaultToString() = runTest {
|
||||
eval("""
|
||||
import lyng.decimal
|
||||
|
||||
var s0 = "0.1".d + "0.1".d
|
||||
assertEquals("0.2", s0.toStringExpanded())
|
||||
assertEquals("0.2", s0.toString())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecimalMathHelpersUseExactImplementationsWhenAvailable() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
val absValue = abs("-2.5".d) as Decimal
|
||||
val floorPos = floor("2.9".d) as Decimal
|
||||
val floorNeg = floor("-2.1".d) as Decimal
|
||||
val ceilPos = ceil("2.1".d) as Decimal
|
||||
val ceilNeg = ceil("-2.1".d) as Decimal
|
||||
val roundPos = round("2.5".d) as Decimal
|
||||
val roundNeg = round("-2.5".d) as Decimal
|
||||
val powInt = pow("1.5".d, 2) as Decimal
|
||||
|
||||
assertEquals("2.5", absValue.toStringExpanded())
|
||||
assertEquals("2", floorPos.toStringExpanded())
|
||||
assertEquals("-3", floorNeg.toStringExpanded())
|
||||
assertEquals("3", ceilPos.toStringExpanded())
|
||||
assertEquals("-2", ceilNeg.toStringExpanded())
|
||||
assertEquals("3", roundPos.toStringExpanded())
|
||||
assertEquals("-2", roundNeg.toStringExpanded())
|
||||
assertEquals("2.25", powInt.toStringExpanded())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecimalMathHelpersFallbackThroughRealTemporarily() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import lyng.decimal
|
||||
|
||||
val sinDecimal = sin("0.5".d) as Decimal
|
||||
val expDecimal = exp("1.25".d) as Decimal
|
||||
val sqrtDecimal = sqrt("2".d) as Decimal
|
||||
val lnDecimal = ln("2".d) as Decimal
|
||||
val log10Decimal = log10("2".d) as Decimal
|
||||
val log2Decimal = log2("2".d) as Decimal
|
||||
val powDecimal = pow("2".d, "0.5".d) as Decimal
|
||||
|
||||
assertEquals((sin(0.5) as Real).d.toStringExpanded(), sinDecimal.toStringExpanded())
|
||||
assertEquals((exp(1.25) as Real).d.toStringExpanded(), expDecimal.toStringExpanded())
|
||||
assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), sqrtDecimal.toStringExpanded())
|
||||
assertEquals((ln(2.0) as Real).d.toStringExpanded(), lnDecimal.toStringExpanded())
|
||||
assertEquals((log10(2.0) as Real).d.toStringExpanded(), log10Decimal.toStringExpanded())
|
||||
assertEquals((log2(2.0) as Real).d.toStringExpanded(), log2Decimal.toStringExpanded())
|
||||
assertEquals((pow(2.0, 0.5) as Real).d.toStringExpanded(), powDecimal.toStringExpanded())
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decimalMustBeObj() = runTest {
|
||||
eval("""
|
||||
import lyng.decimal
|
||||
|
||||
val decimal = 42.d
|
||||
val context = DecimalContext(12)
|
||||
|
||||
assert(decimal is Decimal)
|
||||
assertEquals(Decimal, decimal::class)
|
||||
|
||||
assert(context is DecimalContext)
|
||||
assertEquals(DecimalContext, context::class)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFromRealLife1() = runTest {
|
||||
eval("""
|
||||
import lyng.decimal
|
||||
var X = 42.d
|
||||
X += 11
|
||||
assertEquals(53.d, X)
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kotlinHelperCanWrapIonBigDecimal() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34"))
|
||||
|
||||
assertEquals("Decimal", decimal.objClass.className)
|
||||
assertEquals("12.34", decimal.toString(scope).value)
|
||||
assertEquals("12.34", decimal.invokeInstanceMethod(scope, "toStringExpanded").cast<net.sergeych.lyng.obj.ObjString>(scope).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecimalComparisons() = runTest {
|
||||
eval("""
|
||||
import lyng.decimal
|
||||
val X = 42.d
|
||||
assert(X < 43.d)
|
||||
assert(X < 43)
|
||||
assert(X == 42)
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
@ -168,4 +168,21 @@ class MatrixModuleTest {
|
||||
assertEquals(Vector, vectorValue::class)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatrixAndVectorComparisons() = runTest {
|
||||
eval("""
|
||||
import lyng.matrix
|
||||
|
||||
val v0 = vector([1, 2, 3])
|
||||
val v1 = vector([1, 2, 4])
|
||||
assert(v0 < v1)
|
||||
assert(v0 == vector([1, 2, 3]))
|
||||
|
||||
val m0 = matrix([[1, 2], [3, 4]])
|
||||
val m1 = matrix([[1, 2], [3, 5]])
|
||||
assert(m0 < m1)
|
||||
assert(m0 == matrix([[1, 2], [3, 4]]))
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user