π― Goal
Add rounding capabilities to the Decimal type, allowing users to:
- reduce the number of fractional digits of a
Decimal while staying within the decimal domain,
- exit to the nearest
Integer,
- exit to platform floating-point types (
Float, Double) for interoperability with systems that expect them (e.g. Protocol Buffers float fields, general-purpose Double usage).
π Specifications
Common specifications
- These functions MUST be available for all platforms.
- Architecture Decision Records (see
documentation/decisions directory) MAY be updated.
- Changelog (see
CHANGELOG.md file) MAY be updated.
Rounding mode
- All rounding functions introduced in this ticket use HALF_UP (rounds half away from zero), hardcoded β no
RoundingMode parameter is introduced.
- This is a deliberate choice favoring the rounding behavior most users intuitively expect (the "school" convention) over statistical neutrality. A more rigorous alternative β HALF_EVEN ("banker's rounding"), which avoids the cumulative upward bias of HALF_UP β is the default in IEEE 754, Python's
decimal module, and .NET's decimal type, but was set aside here in favor of intuitiveness for general usage.
- This rationale must be documented in the KDoc of
round() and roundToInteger(), to preempt questions from contributors familiar with Python's or .NET's conventions.
roundToFloat()/roundToDouble() (see below) don't follow this HALF_UP convention: they delegate to the platform's native, correctly-rounded decimal-string-to-binary-floating-point conversion, which uses IEEE 754's round-to-nearest-ties-to-even. This is intentional and shouldn't be "fixed" to HALF_UP β it's the universally expected behavior for this specific kind of conversion, distinct from the decimal-precision rounding round()/roundToInteger() perform.
round(fractionDigits: NonNegativeInteger): Decimal
- Returns a
Decimal with at most fractionDigits fractional digits β if the canonical representation of this decimal already has fewer fractional digits, it's returned unchanged.
- The result goes through the same trailing-zero stripping (
normalize()) as the existing arithmetic operators (plus, minus, times), for consistency.
- Implemented entirely in
commonMain via Integer arithmetic (div/rem on powers of ten), following the same approach as the existing scaleUpTo/normalize internals. No platform-specific decimal API is used or available β java.math.BigDecimal/RoundingMode is JVM-only and has no Kotlin/JS or Kotlin/Native equivalent.
roundToInteger(): Integer
- Returns the
Integer nearest to this decimal, using HALF_UP rounding.
- Semantically equivalent to
round(NonNegativeInteger.ZERO) followed by an exit to Integer, exposed as its own function because the return type differs.
- Naming follows the
roundToX convention used throughout this ticket for approximating boundary exits β as opposed to the toX convention (toInteger(), toLong()) reserved for conversions that are always exact when they succeed.
roundToFloat() / roundToDouble()
- Two distinct floating-point exits, each justified independently:
Double is the general-purpose Kotlin floating-point type.
Float is required for interoperability with systems using 32-bit floats, such as Protocol Buffers float fields (chosen there for bandwidth efficiency, e.g. for proportions).
- Implemented via the existing canonical
toString() representation, delegating to the common-stdlib String.toFloat() / String.toDouble() extensions, which are correctly-rounded on all three platform targets (JVM, JS, Native) β no manual floating-point conversion logic is needed.
- These functions throw
ArithmeticException if the magnitude of this decimal exceeds the finite range representable by Float/Double β they never silently return Float.POSITIVE_INFINITY/Double.POSITIVE_INFINITY (or their negative counterparts). This mirrors Integer.toLong()'s behavior on overflow, and avoids propagating a silent error, which would conflict with this library's "correct by design" principle.
- They keep the
roundToX naming (rather than toX) deliberately: unlike toLong(), which returns the exact value whenever it doesn't throw, these functions return an approximation of this decimal on essentially every successful call β Float/Double cannot exactly represent most decimal values, regardless of magnitude. The roundToX prefix signals this approximation even on the success path, consistent with roundToInteger().
roundToFloatOrNull() / roundToDoubleOrNull() return null instead of throwing, mirroring Integer.toLongOrNull(). Like it, they're hidden from Java via @JvmSynthetic, since nullability isn't explicit in Java's type system.
π§© API
File: subprojects/library/src/commonMain/kotlin/org/kotools/types/number/Decimal.kt
public class Decimal {
// -------------------------- Rounding functions ---------------------------
/**
* Returns a [Decimal] representing this decimal rounded to at most
* [fractionDigits] fractional digits, using HALF_UP rounding (rounds half
* away from zero).
*/
public fun round(fractionDigits: NonNegativeInteger): Decimal
/**
* Returns the [Integer] nearest to this decimal, using HALF_UP rounding
* (rounds half away from zero).
*/
public fun roundToInteger(): Integer
/**
* Returns an approximation of this decimal as a [Float], or throws
* [ArithmeticException] if this decimal's magnitude is out of [Float]'s
* range.
*/
public fun roundToFloat(): Float
/**
* Returns an approximation of this decimal as a [Float], or returns
* `null` if this decimal's magnitude is out of [Float]'s range.
*/
public fun roundToFloatOrNull(): Float?
/**
* Returns an approximation of this decimal as a [Double], or throws
* [ArithmeticException] if this decimal's magnitude is out of [Double]'s
* range.
*/
public fun roundToDouble(): Double
/**
* Returns an approximation of this decimal as a [Double], or returns
* `null` if this decimal's magnitude is out of [Double]'s range.
*/
public fun roundToDoubleOrNull(): Double?
}
π― Goal
Add rounding capabilities to the
Decimaltype, allowing users to:Decimalwhile staying within the decimal domain,Integer,Float,Double) for interoperability with systems that expect them (e.g. Protocol Buffersfloatfields, general-purposeDoubleusage).π Specifications
Common specifications
documentation/decisionsdirectory) MAY be updated.CHANGELOG.mdfile) MAY be updated.Rounding mode
RoundingModeparameter is introduced.decimalmodule, and .NET'sdecimaltype, but was set aside here in favor of intuitiveness for general usage.round()androundToInteger(), to preempt questions from contributors familiar with Python's or .NET's conventions.roundToFloat()/roundToDouble()(see below) don't follow this HALF_UP convention: they delegate to the platform's native, correctly-rounded decimal-string-to-binary-floating-point conversion, which uses IEEE 754's round-to-nearest-ties-to-even. This is intentional and shouldn't be "fixed" to HALF_UP β it's the universally expected behavior for this specific kind of conversion, distinct from the decimal-precision roundinground()/roundToInteger()perform.round(fractionDigits: NonNegativeInteger): DecimalDecimalwith at mostfractionDigitsfractional digits β if the canonical representation of this decimal already has fewer fractional digits, it's returned unchanged.normalize()) as the existing arithmetic operators (plus,minus,times), for consistency.commonMainviaIntegerarithmetic (div/remon powers of ten), following the same approach as the existingscaleUpTo/normalizeinternals. No platform-specific decimal API is used or available βjava.math.BigDecimal/RoundingModeis JVM-only and has no Kotlin/JS or Kotlin/Native equivalent.roundToInteger(): IntegerIntegernearest to this decimal, using HALF_UP rounding.round(NonNegativeInteger.ZERO)followed by an exit toInteger, exposed as its own function because the return type differs.roundToXconvention used throughout this ticket for approximating boundary exits β as opposed to thetoXconvention (toInteger(),toLong()) reserved for conversions that are always exact when they succeed.roundToFloat()/roundToDouble()Doubleis the general-purpose Kotlin floating-point type.Floatis required for interoperability with systems using 32-bit floats, such as Protocol Buffersfloatfields (chosen there for bandwidth efficiency, e.g. for proportions).toString()representation, delegating to the common-stdlibString.toFloat()/String.toDouble()extensions, which are correctly-rounded on all three platform targets (JVM, JS, Native) β no manual floating-point conversion logic is needed.ArithmeticExceptionif the magnitude of this decimal exceeds the finite range representable byFloat/Doubleβ they never silently returnFloat.POSITIVE_INFINITY/Double.POSITIVE_INFINITY(or their negative counterparts). This mirrorsInteger.toLong()'s behavior on overflow, and avoids propagating a silent error, which would conflict with this library's "correct by design" principle.roundToXnaming (rather thantoX) deliberately: unliketoLong(), which returns the exact value whenever it doesn't throw, these functions return an approximation of this decimal on essentially every successful call βFloat/Doublecannot exactly represent most decimal values, regardless of magnitude. TheroundToXprefix signals this approximation even on the success path, consistent withroundToInteger().roundToFloatOrNull()/roundToDoubleOrNull()returnnullinstead of throwing, mirroringInteger.toLongOrNull(). Like it, they're hidden from Java via@JvmSynthetic, since nullability isn't explicit in Java's type system.π§© API
File:
subprojects/library/src/commonMain/kotlin/org/kotools/types/number/Decimal.kt