diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt new file mode 100644 index 0000000..ab3a11e --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt @@ -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 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(0).value + newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value)) + } + decimalClass.addClassFn("fromReal") { + val value = requiredArg(0).value + newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode)) + } + decimalClass.addClassFn("fromString") { + val value = requiredArg(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(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().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().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().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(0).value + newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value)) + }, + rightToCommon = ObjExternCallable.fromBridge { + requiredArg(0) + } + ) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index e7a68d8..5c9ed93 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -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(scope).toInt() + } if (other !is ObjInstance || other.objClass != objClass) { OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it } return -1 diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt new file mode 100644 index 0000000..2da9013 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt @@ -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(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()) + } +} diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt index bca0f94..1a9b98e 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt @@ -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()) + } }