fixes on Decimals and bound kotlin classes
This commit is contained in:
parent
c7c333b71a
commit
05d7432b37
@ -8,9 +8,9 @@ Import it when you need decimal arithmetic that should not inherit `Real`'s bina
|
|||||||
import lyng.decimal
|
import lyng.decimal
|
||||||
```
|
```
|
||||||
|
|
||||||
## What `BigDecimal` Is For
|
## What `Decimal` Is For
|
||||||
|
|
||||||
Use `BigDecimal` when values are fundamentally decimal:
|
Use `Decimal` when values are fundamentally decimal:
|
||||||
|
|
||||||
- money
|
- money
|
||||||
- human-entered quantities
|
- human-entered quantities
|
||||||
@ -38,8 +38,8 @@ assertEquals("2.2", c.toStringExpanded())
|
|||||||
|
|
||||||
The three forms mean different things:
|
The three forms mean different things:
|
||||||
|
|
||||||
- `1.d`: convert `Int -> BigDecimal`
|
- `1.d`: convert `Int -> Decimal`
|
||||||
- `2.2.d`: convert `Real -> BigDecimal`
|
- `2.2.d`: convert `Real -> Decimal`
|
||||||
- `"2.2".d`: parse exact decimal text
|
- `"2.2".d`: parse exact decimal text
|
||||||
|
|
||||||
That distinction is intentional.
|
That distinction is intentional.
|
||||||
@ -67,16 +67,36 @@ The explicit factory methods are:
|
|||||||
```lyng
|
```lyng
|
||||||
import lyng.decimal
|
import lyng.decimal
|
||||||
|
|
||||||
BigDecimal.fromInt(10)
|
Decimal.fromInt(10)
|
||||||
BigDecimal.fromReal(2.5)
|
Decimal.fromReal(2.5)
|
||||||
BigDecimal.fromString("12.34")
|
Decimal.fromString("12.34")
|
||||||
```
|
```
|
||||||
|
|
||||||
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code.
|
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code.
|
||||||
|
|
||||||
|
## From Kotlin
|
||||||
|
|
||||||
|
If you already have an ionspin `BigDecimal` on the host side, the simplest supported way to create a Lyng `Decimal` is:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.asFacade
|
||||||
|
import net.sergeych.lyng.newDecimal
|
||||||
|
|
||||||
|
val scope = Script.newScope()
|
||||||
|
val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `newDecimal(...)` loads `lyng.decimal` if needed
|
||||||
|
- it returns a real Lyng `Decimal` object instance
|
||||||
|
- this is the preferred Kotlin-side construction path when you already hold a host `BigDecimal`
|
||||||
|
|
||||||
## Core Operations
|
## Core Operations
|
||||||
|
|
||||||
`BigDecimal` supports:
|
`Decimal` supports:
|
||||||
|
|
||||||
- `+`
|
- `+`
|
||||||
- `-`
|
- `-`
|
||||||
@ -115,7 +135,7 @@ assert(2 == 2.d)
|
|||||||
assert(3 > 2.d)
|
assert(3 > 2.d)
|
||||||
```
|
```
|
||||||
|
|
||||||
Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + BigDecimal` and `Real + BigDecimal`.
|
Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + Decimal` and `Real + Decimal`.
|
||||||
|
|
||||||
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
||||||
|
|
||||||
@ -226,7 +246,7 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (
|
|||||||
## Decimal With Stdlib Math Functions
|
## Decimal With Stdlib Math Functions
|
||||||
|
|
||||||
Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow`
|
Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow`
|
||||||
now also accept `BigDecimal`.
|
now also accept `Decimal`.
|
||||||
|
|
||||||
Current behavior is intentionally split:
|
Current behavior is intentionally split:
|
||||||
|
|
||||||
@ -235,7 +255,7 @@ Current behavior is intentionally split:
|
|||||||
- `floor(x)`
|
- `floor(x)`
|
||||||
- `ceil(x)`
|
- `ceil(x)`
|
||||||
- `round(x)`
|
- `round(x)`
|
||||||
- `pow(x, y)` when `x` is `BigDecimal` and `y` is an integral exponent
|
- `pow(x, y)` when `x` is `Decimal` and `y` is an integral exponent
|
||||||
- temporary bridge through `Real`:
|
- temporary bridge through `Real`:
|
||||||
- `sin`, `cos`, `tan`
|
- `sin`, `cos`, `tan`
|
||||||
- `asin`, `acos`, `atan`
|
- `asin`, `acos`, `atan`
|
||||||
@ -248,7 +268,7 @@ Current behavior is intentionally split:
|
|||||||
The temporary bridge is:
|
The temporary bridge is:
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
BigDecimal -> Real -> host math -> BigDecimal
|
Decimal -> Real -> host math -> Decimal
|
||||||
```
|
```
|
||||||
|
|
||||||
This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based
|
This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based
|
||||||
@ -259,12 +279,12 @@ Examples:
|
|||||||
```lyng
|
```lyng
|
||||||
import lyng.decimal
|
import lyng.decimal
|
||||||
|
|
||||||
assertEquals("2.5", (abs("-2.5".d) as BigDecimal).toStringExpanded())
|
assertEquals("2.5", (abs("-2.5".d) as Decimal).toStringExpanded())
|
||||||
assertEquals("2", (floor("2.9".d) as BigDecimal).toStringExpanded())
|
assertEquals("2", (floor("2.9".d) as Decimal).toStringExpanded())
|
||||||
|
|
||||||
// Temporary Real bridge:
|
// Temporary Real bridge:
|
||||||
assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".d) as BigDecimal).toStringExpanded())
|
assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".d) as Decimal).toStringExpanded())
|
||||||
assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as BigDecimal).toStringExpanded())
|
assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as Decimal).toStringExpanded())
|
||||||
```
|
```
|
||||||
|
|
||||||
If you care about exact decimal source text:
|
If you care about exact decimal source text:
|
||||||
|
|||||||
@ -160,15 +160,15 @@ import lyng.decimal
|
|||||||
3 > 2.d
|
3 > 2.d
|
||||||
```
|
```
|
||||||
|
|
||||||
work naturally even though `Int` and `Real` themselves were not edited to know `BigDecimal`.
|
work naturally even though `Int` and `Real` themselves were not edited to know `Decimal`.
|
||||||
|
|
||||||
The shape is:
|
The shape is:
|
||||||
|
|
||||||
- `leftClass = Int` or `Real`
|
- `leftClass = Int` or `Real`
|
||||||
- `rightClass = BigDecimal`
|
- `rightClass = Decimal`
|
||||||
- `commonClass = BigDecimal`
|
- `commonClass = Decimal`
|
||||||
- convert built-ins into `BigDecimal`
|
- convert built-ins into `Decimal`
|
||||||
- leave `BigDecimal` values unchanged
|
- leave `Decimal` values unchanged
|
||||||
|
|
||||||
## Step-By-Step Pattern For Your Own Type
|
## Step-By-Step Pattern For Your Own Type
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,9 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
- Preconditions: `require`, `check`.
|
- Preconditions: `require`, `check`.
|
||||||
- Async/concurrency: `launch`, `yield`, `flow`, `delay`.
|
- Async/concurrency: `launch`, `yield`, `flow`, `delay`.
|
||||||
- Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`.
|
- Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`.
|
||||||
- These helpers also accept `lyng.decimal.BigDecimal`.
|
- These helpers also accept `lyng.decimal.Decimal`.
|
||||||
- Exact Decimal path today: `abs`, `floor`, `ceil`, `round`, and `pow` with integral exponent.
|
- Exact Decimal path today: `abs`, `floor`, `ceil`, `round`, and `pow` with integral exponent.
|
||||||
- Temporary Decimal path for the rest: convert `BigDecimal -> Real`, compute, then convert back to `BigDecimal`.
|
- Temporary Decimal path for the rest: convert `Decimal -> Real`, compute, then convert back to `Decimal`.
|
||||||
- Treat that bridge as temporary; prefer native Decimal implementations when they become available.
|
- Treat that bridge as temporary; prefer native Decimal implementations when they become available.
|
||||||
|
|
||||||
## 3. Core Global Constants/Types
|
## 3. Core Global Constants/Types
|
||||||
@ -60,6 +60,9 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
|||||||
## 5. Additional Built-in Modules (import explicitly)
|
## 5. Additional Built-in Modules (import explicitly)
|
||||||
- `import lyng.observable`
|
- `import lyng.observable`
|
||||||
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`.
|
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`.
|
||||||
|
- `import lyng.decimal`
|
||||||
|
- `Decimal`, `DecimalContext`, `DecimalRounding`, `withDecimalContext(...)`.
|
||||||
|
- Kotlin host helper: `ScopeFacade.newDecimal(BigDecimal)` wraps an ionspin host decimal as a Lyng `Decimal`.
|
||||||
- `import lyng.complex`
|
- `import lyng.complex`
|
||||||
- `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`.
|
- `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`.
|
||||||
- `import lyng.matrix`
|
- `import lyng.matrix`
|
||||||
|
|||||||
22
docs/math.md
22
docs/math.md
@ -62,9 +62,9 @@ but:
|
|||||||
|
|
||||||
The following functions return the argument unchanged if it is `Int`.
|
The following functions return the argument unchanged if it is `Int`.
|
||||||
|
|
||||||
For `BigDecimal`:
|
For `Decimal`:
|
||||||
- `floor(x)`, `ceil(x)`, and `round(x)` currently use exact decimal operations
|
- `floor(x)`, `ceil(x)`, and `round(x)` currently use exact decimal operations
|
||||||
- the result stays `BigDecimal`
|
- the result stays `Decimal`
|
||||||
|
|
||||||
For `Real`, the result is a transformed `Real`.
|
For `Real`, the result is a transformed `Real`.
|
||||||
|
|
||||||
@ -78,11 +78,11 @@ For `Real`, the result is a transformed `Real`.
|
|||||||
## Lyng math functions
|
## Lyng math functions
|
||||||
|
|
||||||
Decimal note:
|
Decimal note:
|
||||||
- all scalar math helpers accept `BigDecimal`
|
- all scalar math helpers accept `Decimal`
|
||||||
- `abs(x)` stays exact for `BigDecimal`
|
- `abs(x)` stays exact for `Decimal`
|
||||||
- `pow(x, y)` is exact for `BigDecimal` when `y` is an integral exponent
|
- `pow(x, y)` is exact for `Decimal` when `y` is an integral exponent
|
||||||
- the remaining `BigDecimal` cases currently use a temporary bridge:
|
- the remaining `Decimal` cases currently use a temporary bridge:
|
||||||
`BigDecimal -> Real -> host math -> BigDecimal`
|
`Decimal -> Real -> host math -> Decimal`
|
||||||
- this is temporary; native decimal implementations are planned
|
- this is temporary; native decimal implementations are planned
|
||||||
|
|
||||||
| name | meaning |
|
| name | meaning |
|
||||||
@ -104,7 +104,7 @@ Decimal note:
|
|||||||
| log10(x) | $log_{10}(x)$ |
|
| log10(x) | $log_{10}(x)$ |
|
||||||
| pow(x, y) | ${x^y}$ |
|
| pow(x, y) | ${x^y}$ |
|
||||||
| sqrt(x) | $ \sqrt {x}$ |
|
| sqrt(x) | $ \sqrt {x}$ |
|
||||||
| abs(x) | absolute value of x. Int if x is Int, BigDecimal if x is BigDecimal, Real otherwise |
|
| abs(x) | absolute value of x. Int if x is Int, Decimal if x is Decimal, Real otherwise |
|
||||||
| clamp(x, range) | limit x to be inside range boundaries |
|
| clamp(x, range) | limit x to be inside range boundaries |
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
@ -120,9 +120,9 @@ For example:
|
|||||||
import lyng.decimal
|
import lyng.decimal
|
||||||
|
|
||||||
// Decimal-aware math works too. Some functions are exact, some still bridge through Real temporarily:
|
// Decimal-aware math works too. Some functions are exact, some still bridge through Real temporarily:
|
||||||
assert( (abs("-2.5".d) as BigDecimal).toStringExpanded() == "2.5" )
|
assert( (abs("-2.5".d) as Decimal).toStringExpanded() == "2.5" )
|
||||||
assert( (floor("2.9".d) as BigDecimal).toStringExpanded() == "2" )
|
assert( (floor("2.9".d) as Decimal).toStringExpanded() == "2" )
|
||||||
assert( sin("0.5".d) is BigDecimal )
|
assert( sin("0.5".d) is Decimal )
|
||||||
|
|
||||||
// clamp() limits value to the range:
|
// clamp() limits value to the range:
|
||||||
assert( clamp(15, 0..10) == 10 )
|
assert( clamp(15, 0..10) == 10 )
|
||||||
|
|||||||
@ -57,7 +57,7 @@ Lyng now ships a first-class decimal module built as a regular extension library
|
|||||||
|
|
||||||
It provides:
|
It provides:
|
||||||
|
|
||||||
- `BigDecimal`
|
- `Decimal`
|
||||||
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
||||||
- mixed arithmetic with `Int` and `Real`
|
- mixed arithmetic with `Int` and `Real`
|
||||||
- local division precision and rounding control via `withDecimalContext(...)`
|
- local division precision and rounding control via `withDecimalContext(...)`
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,3 +134,6 @@ fun ScopeFacade.raiseIllegalOperation(message: String = "Operation is illegal"):
|
|||||||
|
|
||||||
fun ScopeFacade.raiseIterationFinished(): Nothing =
|
fun ScopeFacade.raiseIterationFinished(): Nothing =
|
||||||
raiseError(ObjIterationFinishedException(requireScope()))
|
raiseError(ObjIterationFinishedException(requireScope()))
|
||||||
|
|
||||||
|
suspend fun ScopeFacade.newDecimal(value: BigDecimal): ObjInstance =
|
||||||
|
ObjDecimalSupport.newDecimal(this, value)
|
||||||
|
|||||||
@ -253,7 +253,7 @@ class Script(
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private suspend fun ScopeFacade.numberToDouble(value: Obj): Double =
|
private suspend fun ScopeFacade.numberToDouble(value: Obj): Double =
|
||||||
ObjBigDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
|
ObjDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
|
||||||
|
|
||||||
private suspend fun ScopeFacade.decimalAwareUnaryMath(
|
private suspend fun ScopeFacade.decimalAwareUnaryMath(
|
||||||
value: Obj,
|
value: Obj,
|
||||||
@ -263,9 +263,9 @@ class Script(
|
|||||||
exactDecimal?.let { exact ->
|
exactDecimal?.let { exact ->
|
||||||
exact(value)?.let { return it }
|
exact(value)?.let { return it }
|
||||||
}
|
}
|
||||||
if (ObjBigDecimalSupport.isDecimalValue(value)) {
|
if (ObjDecimalSupport.isDecimalValue(value)) {
|
||||||
return ObjBigDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
|
return ObjDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
|
||||||
?: raiseIllegalState("failed to convert Real result back to BigDecimal")
|
?: raiseIllegalState("failed to convert Real result back to Decimal")
|
||||||
}
|
}
|
||||||
return ObjReal(fallback(numberToDouble(value)))
|
return ObjReal(fallback(numberToDouble(value)))
|
||||||
}
|
}
|
||||||
@ -281,11 +281,11 @@ class Script(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj {
|
private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj {
|
||||||
ObjBigDecimalSupport.exactPow(this, base, exponent)?.let { return it }
|
ObjDecimalSupport.exactPow(this, base, exponent)?.let { return it }
|
||||||
if (ObjBigDecimalSupport.isDecimalValue(base) || ObjBigDecimalSupport.isDecimalValue(exponent)) {
|
if (ObjDecimalSupport.isDecimalValue(base) || ObjDecimalSupport.isDecimalValue(exponent)) {
|
||||||
return ObjBigDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
|
return ObjDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
|
||||||
?: ObjBigDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
|
?: ObjDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
|
||||||
?: raiseIllegalState("failed to convert Real pow result back to BigDecimal")
|
?: raiseIllegalState("failed to convert Real pow result back to Decimal")
|
||||||
}
|
}
|
||||||
return ObjReal(numberToDouble(base).pow(numberToDouble(exponent)))
|
return ObjReal(numberToDouble(base).pow(numberToDouble(exponent)))
|
||||||
}
|
}
|
||||||
@ -327,15 +327,15 @@ class Script(
|
|||||||
}
|
}
|
||||||
addFn("floor") {
|
addFn("floor") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
|
decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
|
||||||
}
|
}
|
||||||
addFn("ceil") {
|
addFn("ceil") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
|
decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
|
||||||
}
|
}
|
||||||
addFn("round") {
|
addFn("round") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
|
decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("sin") {
|
addFn("sin") {
|
||||||
@ -401,7 +401,7 @@ class Script(
|
|||||||
addFn("abs") {
|
addFn("abs") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
if (x is ObjInt) ObjInt(x.value.absoluteValue)
|
if (x is ObjInt) ObjInt(x.value.absoluteValue)
|
||||||
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue }
|
else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
addFnDoc(
|
addFnDoc(
|
||||||
@ -661,15 +661,15 @@ class Script(
|
|||||||
)
|
)
|
||||||
ensureFn("floor") {
|
ensureFn("floor") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
|
decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
|
||||||
}
|
}
|
||||||
ensureFn("ceil") {
|
ensureFn("ceil") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
|
decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
|
||||||
}
|
}
|
||||||
ensureFn("round") {
|
ensureFn("round") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
|
decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
|
||||||
}
|
}
|
||||||
ensureFn("sin") {
|
ensureFn("sin") {
|
||||||
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
|
||||||
@ -729,7 +729,7 @@ class Script(
|
|||||||
ensureFn("abs") {
|
ensureFn("abs") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
if (x is ObjInt) ObjInt(x.value.absoluteValue)
|
if (x is ObjInt) ObjInt(x.value.absoluteValue)
|
||||||
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue }
|
else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -867,7 +867,7 @@ class Script(
|
|||||||
}
|
}
|
||||||
addPackage("lyng.decimal") { module ->
|
addPackage("lyng.decimal") { module ->
|
||||||
module.eval(Source("lyng.decimal", decimalLyng))
|
module.eval(Source("lyng.decimal", decimalLyng))
|
||||||
ObjBigDecimalSupport.bindTo(module)
|
ObjDecimalSupport.bindTo(module)
|
||||||
}
|
}
|
||||||
addPackage("lyng.matrix") { module ->
|
addPackage("lyng.matrix") { module ->
|
||||||
module.eval(Source("lyng.matrix", matrixLyng))
|
module.eval(Source("lyng.matrix", matrixLyng))
|
||||||
|
|||||||
@ -448,37 +448,37 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
|||||||
// Math helpers (scalar versions)
|
// Math helpers (scalar versions)
|
||||||
fun math1(name: String) = mod.funDoc(
|
fun math1(name: String) = mod.funDoc(
|
||||||
name = name,
|
name = name,
|
||||||
doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x). Accepts Int, Real, and BigDecimal. BigDecimal currently uses a temporary Real bridge and will get native decimal implementations later.",
|
doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x). Accepts Int, Real, and Decimal. Decimal currently uses a temporary Real bridge and will get native decimal implementations later.",
|
||||||
params = listOf(ParamDoc("x", type("lyng.Number")))
|
params = listOf(ParamDoc("x", type("lyng.Number")))
|
||||||
)
|
)
|
||||||
math1("sin"); math1("cos"); math1("tan"); math1("asin"); math1("acos"); math1("atan")
|
math1("sin"); math1("cos"); math1("tan"); math1("asin"); math1("acos"); math1("atan")
|
||||||
mod.funDoc(name = "floor", doc = StdlibInlineDocIndex.topFunDoc("floor") ?: "Round down the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "floor", doc = StdlibInlineDocIndex.topFunDoc("floor") ?: "Round down the number to the nearest integer. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
|
|
||||||
// Hyperbolic and inverse hyperbolic
|
// Hyperbolic and inverse hyperbolic
|
||||||
math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh")
|
math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh")
|
||||||
|
|
||||||
// Exponentials and logarithms
|
// Exponentials and logarithms
|
||||||
mod.funDoc(name = "exp", doc = StdlibInlineDocIndex.topFunDoc("exp") ?: "Euler's exponential e^x. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "exp", doc = StdlibInlineDocIndex.topFunDoc("exp") ?: "Euler's exponential e^x. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e). BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e). Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
|
||||||
|
|
||||||
// Power/roots and absolute value
|
// Power/roots and absolute value
|
||||||
mod.funDoc(
|
mod.funDoc(
|
||||||
name = "pow",
|
name = "pow",
|
||||||
doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`. BigDecimal with integral `y` is handled directly; other BigDecimal cases currently use a temporary Real bridge.",
|
doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`. Decimal with integral `y` is handled directly; other Decimal cases currently use a temporary Real bridge.",
|
||||||
params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number")))
|
params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number")))
|
||||||
)
|
)
|
||||||
mod.funDoc(
|
mod.funDoc(
|
||||||
name = "sqrt",
|
name = "sqrt",
|
||||||
doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.",
|
doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.",
|
||||||
params = listOf(ParamDoc("x", type("lyng.Number")))
|
params = listOf(ParamDoc("x", type("lyng.Number")))
|
||||||
)
|
)
|
||||||
mod.funDoc(
|
mod.funDoc(
|
||||||
name = "abs",
|
name = "abs",
|
||||||
doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number. Int stays Int, and BigDecimal stays BigDecimal.",
|
doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number. Int stays Int, and Decimal stays Decimal.",
|
||||||
params = listOf(ParamDoc("x", type("lyng.Number")))
|
params = listOf(ParamDoc("x", type("lyng.Number")))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,351 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 ObjBigDecimalSupport {
|
|
||||||
private const val decimalContextVar = "__lyng_decimal_context__"
|
|
||||||
// For Real -> BigDecimal, 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.BigDecimal", false)
|
|
||||||
private object BoundMarker
|
|
||||||
private data class DecimalRuntimeContext(
|
|
||||||
val precision: Long,
|
|
||||||
val rounding: RoundingMode
|
|
||||||
) : Obj()
|
|
||||||
|
|
||||||
suspend fun bindTo(module: ModuleScope) {
|
|
||||||
val decimalClass = module.requireClass("BigDecimal")
|
|
||||||
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 BigDecimal 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 == "BigDecimal"
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
private fun valueOf(obj: Obj): IonBigDecimal {
|
|
||||||
val instance = obj as? ObjInstance ?: error("BigDecimal 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("BigDecimal() 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("BigDecimal 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 != "BigDecimal") {
|
|
||||||
scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
|
|
||||||
}
|
|
||||||
value.kotlinInstanceData as? IonBigDecimal ?: zero
|
|
||||||
}
|
|
||||||
else -> scope.raiseIllegalArgument("expected BigDecimal-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 BigDecimal.",
|
|
||||||
type = type("lyng.decimal.BigDecimal"),
|
|
||||||
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 BigDecimal by preserving the current IEEE-754 value with 17 significant digits and half-even rounding.",
|
|
||||||
type = type("lyng.decimal.BigDecimal"),
|
|
||||||
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 BigDecimal.",
|
|
||||||
type = type("lyng.decimal.BigDecimal"),
|
|
||||||
moduleName = "lyng.decimal",
|
|
||||||
getter = {
|
|
||||||
val value = thisAs<ObjString>().value
|
|
||||||
try {
|
|
||||||
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
requireScope().raiseIllegalArgument("invalid BigDecimal 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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -48,7 +48,7 @@ enum DecimalRounding {
|
|||||||
/**
|
/**
|
||||||
* Dynamic decimal arithmetic settings.
|
* Dynamic decimal arithmetic settings.
|
||||||
*
|
*
|
||||||
* A decimal context is not attached permanently to a `BigDecimal` value. Instead, it is applied dynamically
|
* A decimal context is not attached permanently to a `Decimal` value. Instead, it is applied dynamically
|
||||||
* inside `withDecimalContext(...)`, which makes the rule local to a block of code.
|
* inside `withDecimalContext(...)`, which makes the rule local to a block of code.
|
||||||
*
|
*
|
||||||
* Default context:
|
* Default context:
|
||||||
@ -76,7 +76,7 @@ class DecimalContext(
|
|||||||
/**
|
/**
|
||||||
* Arbitrary-precision decimal value.
|
* Arbitrary-precision decimal value.
|
||||||
*
|
*
|
||||||
* `BigDecimal` is intended for decimal arithmetic where binary floating-point (`Real`) is the wrong tool:
|
* `Decimal` is intended for decimal arithmetic where binary floating-point (`Real`) is the wrong tool:
|
||||||
* - money
|
* - money
|
||||||
* - human-entered decimal values
|
* - human-entered decimal values
|
||||||
* - ratios that should round in decimal, not in binary
|
* - ratios that should round in decimal, not in binary
|
||||||
@ -84,10 +84,10 @@ class DecimalContext(
|
|||||||
*
|
*
|
||||||
* Creating values:
|
* Creating values:
|
||||||
*
|
*
|
||||||
* - `1.d` converts `Int -> BigDecimal`
|
* - `1.d` converts `Int -> Decimal`
|
||||||
* - `2.2.d` converts `Real -> BigDecimal` by preserving the current IEEE-754 value
|
* - `2.2.d` converts `Real -> Decimal` by preserving the current IEEE-754 value
|
||||||
* - `"2.2".d` parses exact decimal text
|
* - `"2.2".d` parses exact decimal text
|
||||||
* - `BigDecimal.fromInt(...)`, `fromReal(...)`, `fromString(...)` are explicit factory forms
|
* - `Decimal.fromInt(...)`, `fromReal(...)`, `fromString(...)` are explicit factory forms
|
||||||
*
|
*
|
||||||
* Important distinction:
|
* Important distinction:
|
||||||
*
|
*
|
||||||
@ -109,7 +109,7 @@ class DecimalContext(
|
|||||||
*
|
*
|
||||||
* Mixed arithmetic:
|
* Mixed arithmetic:
|
||||||
*
|
*
|
||||||
* `BigDecimal` defines its own operators against decimal-compatible values, and the decimal module also registers
|
* `Decimal` defines its own operators against decimal-compatible values, and the decimal module also registers
|
||||||
* interop bridges so built-in left-hand operands work naturally:
|
* interop bridges so built-in left-hand operands work naturally:
|
||||||
*
|
*
|
||||||
* import lyng.decimal
|
* import lyng.decimal
|
||||||
@ -134,15 +134,15 @@ class DecimalContext(
|
|||||||
*
|
*
|
||||||
* "2.2".d
|
* "2.2".d
|
||||||
*
|
*
|
||||||
* That is the precise form. `2.2.d` remains a `Real -> BigDecimal` conversion by design.
|
* That is the precise form. `2.2.d` remains a `Real -> Decimal` conversion by design.
|
||||||
*/
|
*/
|
||||||
extern class BigDecimal() {
|
extern class Decimal() {
|
||||||
/** Add another decimal-compatible value. */
|
/** Add another decimal-compatible value. */
|
||||||
extern fun plus(other: Object): BigDecimal
|
extern fun plus(other: Object): Decimal
|
||||||
/** Subtract another decimal-compatible value. */
|
/** Subtract another decimal-compatible value. */
|
||||||
extern fun minus(other: Object): BigDecimal
|
extern fun minus(other: Object): Decimal
|
||||||
/** Multiply by another decimal-compatible value. */
|
/** Multiply by another decimal-compatible value. */
|
||||||
extern fun mul(other: Object): BigDecimal
|
extern fun mul(other: Object): Decimal
|
||||||
/**
|
/**
|
||||||
* Divide by another decimal-compatible value.
|
* Divide by another decimal-compatible value.
|
||||||
*
|
*
|
||||||
@ -150,13 +150,13 @@ extern class BigDecimal() {
|
|||||||
* - by default: `34` significant digits, `HalfEven`
|
* - by default: `34` significant digits, `HalfEven`
|
||||||
* - inside `withDecimalContext(...)`: the context active for the current block
|
* - inside `withDecimalContext(...)`: the context active for the current block
|
||||||
*/
|
*/
|
||||||
extern fun div(other: Object): BigDecimal
|
extern fun div(other: Object): Decimal
|
||||||
/** Remainder with another decimal-compatible value. */
|
/** Remainder with another decimal-compatible value. */
|
||||||
extern fun mod(other: Object): BigDecimal
|
extern fun mod(other: Object): Decimal
|
||||||
/** Compare with another decimal-compatible value. */
|
/** Compare with another decimal-compatible value. */
|
||||||
extern fun compareTo(other: Object): Int
|
extern fun compareTo(other: Object): Int
|
||||||
/** Unary minus. */
|
/** Unary minus. */
|
||||||
extern fun negate(): BigDecimal
|
extern fun negate(): Decimal
|
||||||
/** Convert to `Int` by dropping the fractional part according to backend conversion rules. */
|
/** Convert to `Int` by dropping the fractional part according to backend conversion rules. */
|
||||||
extern fun toInt(): Int
|
extern fun toInt(): Int
|
||||||
/** Convert to `Real`. */
|
/** Convert to `Real`. */
|
||||||
@ -169,16 +169,16 @@ extern class BigDecimal() {
|
|||||||
extern fun toStringExpanded(): String
|
extern fun toStringExpanded(): String
|
||||||
|
|
||||||
/** Create a decimal from an `Int`. */
|
/** Create a decimal from an `Int`. */
|
||||||
static extern fun fromInt(value: Int): BigDecimal
|
static extern fun fromInt(value: Int): Decimal
|
||||||
/**
|
/**
|
||||||
* Create a decimal from a `Real`.
|
* Create a decimal from a `Real`.
|
||||||
*
|
*
|
||||||
* This preserves the current IEEE-754 value using a round-trip-safe decimal conversion.
|
* This preserves the current IEEE-754 value using a round-trip-safe decimal conversion.
|
||||||
* It does not try to recover the original source text.
|
* It does not try to recover the original source text.
|
||||||
*/
|
*/
|
||||||
static extern fun fromReal(value: Real): BigDecimal
|
static extern fun fromReal(value: Real): Decimal
|
||||||
/** Parse exact decimal text. */
|
/** Parse exact decimal text. */
|
||||||
static extern fun fromString(value: String): BigDecimal
|
static extern fun fromString(value: String): Decimal
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -97,7 +97,7 @@ enum BinaryOperator {
|
|||||||
* The registry is symmetric for the converted values, but not for the original syntax.
|
* The registry is symmetric for the converted values, but not for the original syntax.
|
||||||
* Its job is specifically to fill the gap where your custom type appears on the right:
|
* Its job is specifically to fill the gap where your custom type appears on the right:
|
||||||
*
|
*
|
||||||
* - `myDecimal + 1` usually already works if `BigDecimal.plus(Int)` exists
|
* - `myDecimal + 1` usually already works if `Decimal.plus(Int)` exists
|
||||||
* - `1 + myDecimal` needs registration because `Int` itself is not rewritten
|
* - `1 + myDecimal` needs registration because `Int` itself is not rewritten
|
||||||
*
|
*
|
||||||
* Typical pattern for a custom type:
|
* Typical pattern for a custom type:
|
||||||
@ -135,7 +135,7 @@ enum BinaryOperator {
|
|||||||
* - `3 > Rational(5, 2)` works
|
* - `3 > Rational(5, 2)` works
|
||||||
* - `2 == Rational(2, 1)` works
|
* - `2 == Rational(2, 1)` works
|
||||||
*
|
*
|
||||||
* Decimal uses the same mechanism internally to make `Int + BigDecimal` and `Real + BigDecimal`
|
* Decimal uses the same mechanism internally to make `Int + Decimal` and `Real + Decimal`
|
||||||
* work without changing the built-in `Int` or `Real` classes.
|
* work without changing the built-in `Int` or `Real` classes.
|
||||||
*/
|
*/
|
||||||
extern object OperatorInterop {
|
extern object OperatorInterop {
|
||||||
|
|||||||
@ -1,229 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 kotlinx.coroutines.test.runTest
|
|
||||||
import kotlin.test.Test
|
|
||||||
|
|
||||||
class BigDecimalModuleTest {
|
|
||||||
@Test
|
|
||||||
fun testDecimalModuleFactoriesAndConversions() = runTest {
|
|
||||||
val scope = Script.newScope()
|
|
||||||
scope.eval(
|
|
||||||
"""
|
|
||||||
import lyng.decimal
|
|
||||||
|
|
||||||
assertEquals("12.34", BigDecimal.fromString("12.34").toStringExpanded())
|
|
||||||
assertEquals("1", BigDecimal.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 BigDecimal
|
|
||||||
val floorPos = floor("2.9".d) as BigDecimal
|
|
||||||
val floorNeg = floor("-2.1".d) as BigDecimal
|
|
||||||
val ceilPos = ceil("2.1".d) as BigDecimal
|
|
||||||
val ceilNeg = ceil("-2.1".d) as BigDecimal
|
|
||||||
val roundPos = round("2.5".d) as BigDecimal
|
|
||||||
val roundNeg = round("-2.5".d) as BigDecimal
|
|
||||||
val powInt = pow("1.5".d, 2) as BigDecimal
|
|
||||||
|
|
||||||
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 BigDecimal
|
|
||||||
val expDecimal = exp("1.25".d) as BigDecimal
|
|
||||||
val sqrtDecimal = sqrt("2".d) as BigDecimal
|
|
||||||
val lnDecimal = ln("2".d) as BigDecimal
|
|
||||||
val log10Decimal = log10("2".d) as BigDecimal
|
|
||||||
val log2Decimal = log2("2".d) as BigDecimal
|
|
||||||
val powDecimal = pow("2".d, "0.5".d) as BigDecimal
|
|
||||||
|
|
||||||
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 BigDecimal)
|
|
||||||
assertEquals(BigDecimal, 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)
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -79,10 +79,10 @@ extern class MapEntry<K,V> : Array<Object> {
|
|||||||
// Built-in math helpers (implemented in host runtime).
|
// Built-in math helpers (implemented in host runtime).
|
||||||
//
|
//
|
||||||
// Decimal note:
|
// Decimal note:
|
||||||
// - these helpers accept `BigDecimal` values too
|
// - these helpers accept `Decimal` values too
|
||||||
// - `abs`, `floor`, `ceil`, `round`, and `pow(x, y)` with integral `y` keep decimal arithmetic
|
// - `abs`, `floor`, `ceil`, `round`, and `pow(x, y)` with integral `y` keep decimal arithmetic
|
||||||
// - the remaining decimal cases currently use a temporary bridge:
|
// - the remaining decimal cases currently use a temporary bridge:
|
||||||
// `BigDecimal -> Real -> host math -> BigDecimal`
|
// `Decimal -> Real -> host math -> Decimal`
|
||||||
// - this is temporary and will be replaced with dedicated decimal implementations
|
// - this is temporary and will be replaced with dedicated decimal implementations
|
||||||
extern fun abs(x: Object): Object
|
extern fun abs(x: Object): Object
|
||||||
extern fun ln(x: Object): Object
|
extern fun ln(x: Object): Object
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# Decimal Math TODO
|
# Decimal Math TODO
|
||||||
|
|
||||||
These stdlib math helpers currently accept `BigDecimal`, but some still use the temporary compatibility path
|
These stdlib math helpers currently accept `Decimal`, but some still use the temporary compatibility path
|
||||||
|
|
||||||
`BigDecimal -> Real -> host math -> BigDecimal`
|
`Decimal -> Real -> host math -> Decimal`
|
||||||
|
|
||||||
instead of a native decimal implementation.
|
instead of a native decimal implementation.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user