fixed Decimal (and other such classes ) comparison

This commit is contained in:
Sergey Chernov 2026-03-29 01:29:48 +03:00
parent 05d7432b37
commit aa9565b40b
4 changed files with 630 additions and 0 deletions

View File

@ -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)
}
)
}
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -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())
}
}