8.0 KiB
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:
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:
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: convertInt -> Decimal2.2.d: convertReal -> 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.
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 thatReal - if you want exact decimal source text, use a
String
Factory Functions
The explicit factory methods are:
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:
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(...)loadslyng.decimalif needed- it returns a real Lyng
Decimalobject 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:
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:
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 for the generic mechanism behind that.
String Representation
Use toStringExpanded() when you want plain decimal output without scientific notation:
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
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:
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:
34significant digits - rounding:
HalfEven
Example:
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:
import lyng.decimal
assertEquals(
"0.3333333333",
withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
)
You can also pass an explicit context object:
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:
HalfEvenHalfAwayFromZeroHalfTowardsZeroCeilingFloorAwayFromZeroTowardsZero
Tie example at precision 2:
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:
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)whenxisDecimalandyis an integral exponent
- temporary bridge through
Real:sin,cos,tanasin,acos,atansinh,cosh,tanhasinh,acosh,atanhexp,ln,log10,log2sqrtpowfor the remaining non-integral decimal exponent cases
The temporary bridge is:
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:
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:
"12.34".d
If you intentionally convert an existing binary floating-point value:
someReal.d
If you want local control over division:
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.