326 lines
8.0 KiB
Markdown
326 lines
8.0 KiB
Markdown
# Decimal (`lyng.decimal`)
|
|
|
|
`lyng.decimal` adds an arbitrary-precision decimal type to Lyng as a normal library module.
|
|
|
|
Import it when you need decimal arithmetic that should not inherit `Real`'s binary floating-point behavior:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
```
|
|
|
|
## What `Decimal` Is For
|
|
|
|
Use `Decimal` when values are fundamentally decimal:
|
|
|
|
- money
|
|
- human-entered quantities
|
|
- exact decimal text
|
|
- predictable decimal rounding
|
|
- user-facing formatting and tests
|
|
|
|
Do not use it just because a number has a fractional part. `Real` is still the right type for ordinary double-precision numeric work.
|
|
|
|
## Creating Decimal Values
|
|
|
|
There are three supported conversions:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
val a = 1.d
|
|
val b = 2.2.d
|
|
val c = "2.2".d
|
|
|
|
assertEquals("1", a.toStringExpanded())
|
|
assertEquals("2.2", b.toStringExpanded())
|
|
assertEquals("2.2", c.toStringExpanded())
|
|
```
|
|
|
|
The three forms mean different things:
|
|
|
|
- `1.d`: convert `Int -> Decimal`
|
|
- `2.2.d`: convert `Real -> Decimal`
|
|
- `"2.2".d`: parse exact decimal text
|
|
|
|
That distinction is intentional.
|
|
|
|
### `Real.d` vs `"..." .d`
|
|
|
|
`Real.d` preserves the current `Real` value. It does not pretend the source code was exact decimal text.
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
|
assertEquals("0.3", "0.3".d.toStringExpanded())
|
|
```
|
|
|
|
This follows the "minimal confusion" rule:
|
|
|
|
- if you start from a `Real`, you get a decimal representation of that `Real`
|
|
- if you want exact decimal source text, use a `String`
|
|
|
|
## Factory Functions
|
|
|
|
The explicit factory methods are:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
Decimal.fromInt(10)
|
|
Decimal.fromReal(2.5)
|
|
Decimal.fromString("12.34")
|
|
```
|
|
|
|
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.EvalSession
|
|
import net.sergeych.lyng.asFacade
|
|
import net.sergeych.lyng.newDecimal
|
|
|
|
val scope = EvalSession().getScope()
|
|
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
|
|
|
|
`Decimal` supports:
|
|
|
|
- `+`
|
|
- `-`
|
|
- `*`
|
|
- `/`
|
|
- `%`
|
|
- unary `-`
|
|
- comparison operators
|
|
- equality operators
|
|
|
|
Examples:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals("3.75", ("1.5".d + "2.25".d).toStringExpanded())
|
|
assertEquals("1.25", ("2.5".d - "1.25".d).toStringExpanded())
|
|
assertEquals("3.0", ("1.5".d * 2.d).toStringExpanded())
|
|
assertEquals("0.5", (1.d / 2.d).toStringExpanded())
|
|
assert("2.0".d > "1.5".d)
|
|
assert("2.0".d == 2.d)
|
|
```
|
|
|
|
## Interoperability With `Int` and `Real`
|
|
|
|
The decimal module registers mixed-operand operator bridges so both sides read naturally:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals(3.d, 1 + 2.d)
|
|
assertEquals(3.d, 2.d + 1)
|
|
assertEquals(1.5.d, 1.d + 0.5)
|
|
assertEquals(1.5.d, 0.5 + 1.d)
|
|
assert(2 == 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 + Decimal` and `Real + Decimal`.
|
|
|
|
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
|
|
|
## String Representation
|
|
|
|
Use `toStringExpanded()` when you want plain decimal output without scientific notation:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals("12.34", "12.34".d.toStringExpanded())
|
|
```
|
|
|
|
This is the recommended representation for:
|
|
|
|
- tests
|
|
- user-visible diagnostics
|
|
- decimal formatting checks
|
|
|
|
## Conversions Back To Built-ins
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals(2, "2.9".d.toInt())
|
|
assertEquals(2.9, "2.9".d.toReal())
|
|
```
|
|
|
|
Use `toReal()` only when you are willing to return to binary floating-point semantics.
|
|
|
|
## Non-Finite Checks
|
|
|
|
`Decimal` values are always finite, so these helpers exist for API symmetry with `Real` and always return `false`:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals(false, "2.9".d.isInfinite())
|
|
assertEquals(false, "2.9".d.isNaN())
|
|
```
|
|
|
|
## Division Context
|
|
|
|
Division is the operation where precision and rounding matter most.
|
|
|
|
By default, decimal division uses:
|
|
|
|
- precision: `34` significant digits
|
|
- rounding: `HalfEven`
|
|
|
|
Example:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
|
assertEquals("0.6666666666666666666666666666666667", ("2".d / 3.d).toStringExpanded())
|
|
```
|
|
|
|
## `withDecimalContext(...)`
|
|
|
|
Use `withDecimalContext(...)` to override decimal division rules inside a block:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals(
|
|
"0.3333333333",
|
|
withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
|
)
|
|
```
|
|
|
|
You can also pass an explicit context object:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
val ctx = DecimalContext(6, DecimalRounding.HalfAwayFromZero)
|
|
assertEquals("0.666667", withDecimalContext(ctx) { ("2".d / 3.d).toStringExpanded() })
|
|
```
|
|
|
|
The context is dynamic and local to the block. After the block exits, the previous context is restored.
|
|
|
|
## Rounding Modes
|
|
|
|
Available rounding modes:
|
|
|
|
- `HalfEven`
|
|
- `HalfAwayFromZero`
|
|
- `HalfTowardsZero`
|
|
- `Ceiling`
|
|
- `Floor`
|
|
- `AwayFromZero`
|
|
- `TowardsZero`
|
|
|
|
Tie example at precision `2`:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
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.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.Floor) { (1.d / 8.d).toStringExpanded() })
|
|
```
|
|
|
|
Negative values follow the same named policy in the obvious direction:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
|
```
|
|
|
|
## Recommended Usage Rules
|
|
|
|
## Decimal With Stdlib Math Functions
|
|
|
|
Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow`
|
|
now also accept `Decimal`.
|
|
|
|
Current behavior is intentionally split:
|
|
|
|
- exact decimal implementation:
|
|
- `abs(x)`
|
|
- `floor(x)`
|
|
- `ceil(x)`
|
|
- `round(x)`
|
|
- `pow(x, y)` when `x` is `Decimal` and `y` is an integral exponent
|
|
- temporary bridge through `Real`:
|
|
- `sin`, `cos`, `tan`
|
|
- `asin`, `acos`, `atan`
|
|
- `sinh`, `cosh`, `tanh`
|
|
- `asinh`, `acosh`, `atanh`
|
|
- `exp`, `ln`, `log10`, `log2`
|
|
- `sqrt`
|
|
- `pow` for the remaining non-integral decimal exponent cases
|
|
|
|
The temporary bridge is:
|
|
|
|
```lyng
|
|
Decimal -> Real -> host math -> Decimal
|
|
```
|
|
|
|
This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based
|
|
paths over time.
|
|
|
|
Examples:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
assertEquals("2.5", (abs("-2.5".d) as Decimal).toStringExpanded())
|
|
assertEquals("2", (floor("2.9".d) as Decimal).toStringExpanded())
|
|
|
|
// Temporary Real bridge:
|
|
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 Decimal).toStringExpanded())
|
|
```
|
|
|
|
If you care about exact decimal source text:
|
|
|
|
```lyng
|
|
"12.34".d
|
|
```
|
|
|
|
If you intentionally convert an existing binary floating-point value:
|
|
|
|
```lyng
|
|
someReal.d
|
|
```
|
|
|
|
If you want local control over division:
|
|
|
|
```lyng
|
|
withDecimalContext(precision, rounding) { ... }
|
|
```
|
|
|
|
If you want custom mixed operators for your own type, follow the same pattern as Decimal and use the operator registry:
|
|
|
|
- define the operators on your own class
|
|
- choose a common class
|
|
- register mixed operand bridges
|
|
|
|
See [OperatorInterop.md](OperatorInterop.md).
|