diff --git a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatOptions.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatOptions.kt index bf5e995..5f5712e 100644 --- a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatOptions.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatOptions.kt @@ -41,6 +41,8 @@ import kotlinx.serialization.Serializable * @property hideZeroFractionDigits When `true`, omit fraction digits entirely if they are all zero * (e.g. `"$34.00"` → `"$34"`), while non-zero fractions are displayed unchanged * (e.g. `"$34.20"` remains `"$34.20"`). Takes priority over [minFractionDigits]. + * @property roundingMode Strategy used when the amount has more fraction digits than the + * target scale. Defaults to [RoundingMode.HALF_EVEN]. */ @Serializable data class CurrencyFormatOptions( @@ -52,6 +54,7 @@ data class CurrencyFormatOptions( val symbolDisplay: SymbolDisplay = SymbolDisplay.SYMBOL, val zeroDisplay: ZeroDisplay = ZeroDisplay.SHOW, val hideZeroFractionDigits: Boolean = false, + val roundingMode: RoundingMode = RoundingMode.HALF_EVEN, ) { init { if (minFractionDigits != null && maxFractionDigits != null) { @@ -112,6 +115,7 @@ data class CurrencyFormatOptions( var symbolDisplay: SymbolDisplay = SymbolDisplay.SYMBOL var zeroDisplay: ZeroDisplay = ZeroDisplay.SHOW var hideZeroFractionDigits: Boolean = false + var roundingMode: RoundingMode = RoundingMode.HALF_EVEN fun symbolPosition(value: SymbolPosition) = apply { symbolPosition = value } fun grouping(value: Boolean) = apply { grouping = value } @@ -121,11 +125,12 @@ data class CurrencyFormatOptions( fun symbolDisplay(value: SymbolDisplay) = apply { symbolDisplay = value } fun zeroDisplay(value: ZeroDisplay) = apply { zeroDisplay = value } fun hideZeroFractionDigits(value: Boolean) = apply { hideZeroFractionDigits = value } + fun roundingMode(value: RoundingMode) = apply { roundingMode = value } /** Builds an immutable [CurrencyFormatOptions] from the current builder state. */ fun build() = CurrencyFormatOptions( symbolPosition, grouping, minFractionDigits, maxFractionDigits, - negativeStyle, symbolDisplay, zeroDisplay, hideZeroFractionDigits, + negativeStyle, symbolDisplay, zeroDisplay, hideZeroFractionDigits, roundingMode, ) } } diff --git a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt index ee41eb0..c35645b 100644 --- a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt @@ -1,7 +1,6 @@ package org.kimplify.kurrency import org.kimplify.kurrency.extensions.normalizeAmount -import kotlin.math.abs expect class CurrencyFormatterImpl(kurrencyLocale: KurrencyLocale = KurrencyLocale.systemLocale()) : CurrencyFormat { override fun getFractionDigitsOrDefault(currencyCode: String, default: Int): Int @@ -331,28 +330,21 @@ class CurrencyFormatter(private val locale: KurrencyLocale = KurrencyLocale.syst currencyCode: String, options: CurrencyFormatOptions, ): Result { - return formatWithValidation(amount, currencyCode) { normalizedAmount -> + return formatWithValidation(amount, currencyCode) { rawAmount -> runCatching { - val numericValue = normalizedAmount.toDoubleOrNull() - ?: throw KurrencyError.InvalidAmount(amount) - - // Zero display handling - if (numericValue == 0.0) { + val normalizedAmount = Decimals.expandScientific(rawAmount.normalizeAmount()) + if (Decimals.isZero(normalizedAmount)) { when (options.zeroDisplay) { - ZeroDisplay.DASH -> return@runCatching "\u2014" // em-dash + ZeroDisplay.DASH -> return@runCatching "\u2014" ZeroDisplay.EMPTY -> return@runCatching "" - ZeroDisplay.SHOW -> { /* fall through to normal formatting */ } + ZeroDisplay.SHOW -> {} } } val metadata = CurrencyMetadata.parse(currencyCode).getOrNull() val symbol = metadata?.symbol ?: "" - val isNegative = numericValue < 0 - val absAmount = if (isNegative) { - if (normalizedAmount.startsWith("-")) normalizedAmount.drop(1) else normalizedAmount - } else { - normalizedAmount - } + val isNegative = Decimals.isNegative(normalizedAmount) + val absAmount = Decimals.abs(normalizedAmount) // Format the absolute amount with custom fraction digits if needed val formattedAbsAmount = formatNumberPortion(absAmount, currencyCode, options) @@ -362,7 +354,7 @@ class CurrencyFormatter(private val locale: KurrencyLocale = KurrencyLocale.syst SymbolDisplay.SYMBOL -> symbol SymbolDisplay.ISO_CODE -> currencyCode SymbolDisplay.NAME -> { - if (abs(numericValue) == 1.0) metadata?.displayName ?: currencyCode + if (Decimals.isOne(normalizedAmount)) metadata?.displayName ?: currencyCode else metadata?.displayNamePlural ?: currencyCode } SymbolDisplay.NONE -> "" @@ -415,10 +407,7 @@ class CurrencyFormatter(private val locale: KurrencyLocale = KurrencyLocale.syst val maxFrac = rawMaxFrac val minFrac = minOf(rawMinFrac, maxFrac) - val doubleVal = absAmount.toDoubleOrNull() ?: 0.0 - - // Format the number to a plain decimal string with the right precision - val plainFormatted = formatDecimal(doubleVal, minFrac, maxFrac) + val plainFormatted = formatDecimal(absAmount, minFrac, maxFrac, options.roundingMode) // Split into integer and fractional parts val parts = plainFormatted.split(".") @@ -592,21 +581,17 @@ class CurrencyFormatter(private val locale: KurrencyLocale = KurrencyLocale.syst return result } - /** - * Formats a Double to a plain decimal string with the specified fraction digit range. - * Uses rounding-half-even (banker's rounding) via standard Kotlin rounding. - */ - internal fun formatDecimal(value: Double, minFractionDigits: Int, maxFractionDigits: Int): String { - // Round to maxFractionDigits - val factor = tenPow(maxFractionDigits) - val rounded = kotlin.math.round(value * factor) / factor - - val plain = doubleToPlainString(rounded) - val parts = plain.split(".") + internal fun formatDecimal( + amount: String, + minFractionDigits: Int, + maxFractionDigits: Int, + mode: RoundingMode, + ): String { + val rounded = Decimals.roundToScale(amount, maxFractionDigits, mode) + val parts = rounded.split(".") val intPart = parts[0] val rawFrac = if (parts.size > 1) parts[1] else "" - // Trim trailing zeros down to minFractionDigits, but keep at least minFractionDigits val paddedFrac = rawFrac.padEnd(maxFractionDigits, '0').take(maxFractionDigits) val trimmedFrac = if (paddedFrac.length > minFractionDigits) { val trimmed = paddedFrac.trimEnd('0') @@ -636,12 +621,6 @@ class CurrencyFormatter(private val locale: KurrencyLocale = KurrencyLocale.syst return sb.reverse().toString() } - private fun tenPow(n: Int): Double { - var result = 1.0 - repeat(n) { result *= 10.0 } - return result - } - /** * Converts a Double to a plain decimal string without scientific notation. * This is needed because Kotlin's Double.toString() may use scientific notation diff --git a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/Decimals.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/Decimals.kt new file mode 100644 index 0000000..309b9f3 --- /dev/null +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/Decimals.kt @@ -0,0 +1,86 @@ +package org.kimplify.kurrency + +internal object Decimals { + + fun isZero(s: String): Boolean = s.none { it in '1'..'9' } + + fun isNegative(s: String): Boolean = s.startsWith("-") && !isZero(s) + + fun abs(s: String): String = if (s.startsWith("-") || s.startsWith("+")) s.substring(1) else s + + fun isOne(s: String): Boolean { + val a = abs(s) + val dot = a.indexOf('.') + val intPart = (if (dot >= 0) a.substring(0, dot) else a).trimStart('0').ifEmpty { "0" } + val fracPart = if (dot >= 0) a.substring(dot + 1) else "" + return intPart == "1" && fracPart.all { it == '0' } + } + + fun roundToScale(s: String, scale: Int, mode: RoundingMode): String { + val dot = s.indexOf('.') + val intDigits = (if (dot >= 0) s.substring(0, dot) else s).ifEmpty { "0" } + val fracPart = if (dot >= 0) s.substring(dot + 1) else "" + + if (fracPart.length <= scale) { + val padded = fracPart.padEnd(scale, '0') + return if (scale == 0) intDigits else "$intDigits.$padded" + } + + val kept = fracPart.substring(0, scale) + val dropped = fracPart.substring(scale) + val combined = intDigits + kept + + val roundUp = when (mode) { + RoundingMode.DOWN -> false + RoundingMode.UP -> dropped.any { it != '0' } + RoundingMode.HALF_UP -> dropped[0] >= '5' + RoundingMode.HALF_EVEN -> when { + dropped[0] > '5' -> true + dropped[0] < '5' -> false + dropped.drop(1).any { it != '0' } -> true + else -> (combined.last() - '0') % 2 == 1 + } + } + + val resultDigits = if (roundUp) incrementDigits(combined) else combined + val intResult = (if (scale == 0) resultDigits else resultDigits.dropLast(scale)) + .trimStart('0').ifEmpty { "0" } + val fracResult = if (scale == 0) "" else resultDigits.takeLast(scale) + return if (scale == 0) intResult else "$intResult.$fracResult" + } + + fun expandScientific(s: String): String { + if (s.none { it == 'e' || it == 'E' }) return s + val sign = if (s.startsWith("-")) "-" else "" + val unsigned = if (s.startsWith("-") || s.startsWith("+")) s.substring(1) else s + val ei = unsigned.indexOfFirst { it == 'e' || it == 'E' } + val mantissa = unsigned.substring(0, ei) + val exp = unsigned.substring(ei + 1).toIntOrNull() ?: return s + val dot = mantissa.indexOf('.') + val intDigits = if (dot >= 0) mantissa.substring(0, dot) else mantissa + val fracDigits = if (dot >= 0) mantissa.substring(dot + 1) else "" + val digits = intDigits + fracDigits + val pointPos = intDigits.length + exp + val body = when { + pointPos <= 0 -> "0." + "0".repeat(-pointPos) + digits + pointPos >= digits.length -> digits + "0".repeat(pointPos - digits.length) + else -> digits.substring(0, pointPos) + "." + digits.substring(pointPos) + } + return sign + body + } + + private fun incrementDigits(digits: String): String { + val chars = digits.toCharArray() + var i = chars.size - 1 + while (i >= 0) { + if (chars[i] == '9') { + chars[i] = '0' + i-- + } else { + chars[i] = chars[i] + 1 + return chars.concatToString() + } + } + return "1" + chars.concatToString() + } +} diff --git a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/RoundingMode.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/RoundingMode.kt new file mode 100644 index 0000000..a080940 --- /dev/null +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/RoundingMode.kt @@ -0,0 +1,22 @@ +package org.kimplify.kurrency + +import kotlinx.serialization.Serializable + +/** + * Rounding strategy applied when an amount has more fraction digits than the + * target scale. Used by [CurrencyFormatOptions.roundingMode]. + */ +@Serializable +enum class RoundingMode { + /** Round to the nearest neighbor; ties go to the even digit (banker's rounding). */ + HALF_EVEN, + + /** Round to the nearest neighbor; ties round away from zero. */ + HALF_UP, + + /** Round toward zero (truncate). */ + DOWN, + + /** Round away from zero. */ + UP, +} diff --git a/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/DecimalExactFormattingTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/DecimalExactFormattingTest.kt new file mode 100644 index 0000000..9126416 --- /dev/null +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/DecimalExactFormattingTest.kt @@ -0,0 +1,64 @@ +package org.kimplify.kurrency + +import kotlin.test.Test +import kotlin.test.assertEquals + +class DecimalExactFormattingTest { + + private val us = CurrencyFormatter(KurrencyLocale.US) + + @Test + fun exactBeyondDoublePrecision() { + val r = us.formatWithOptions("9007199254740993.01", "USD", CurrencyFormatOptions()).getOrThrow() + assertEquals("$9,007,199,254,740,993.01", r) + } + + @Test + fun halfEvenDefault_correctsDoubleArtifact() { + assertEquals("$2.68", us.formatWithOptions("2.675", "USD", CurrencyFormatOptions()).getOrThrow()) + } + + @Test + fun roundingMode_halfUp() { + val opts = CurrencyFormatOptions { roundingMode = RoundingMode.HALF_UP } + assertEquals("$2.67", us.formatWithOptions("2.665", "USD", opts).getOrThrow()) + } + + @Test + fun roundingMode_down_truncates() { + val opts = CurrencyFormatOptions { roundingMode = RoundingMode.DOWN } + assertEquals("$2.67", us.formatWithOptions("2.679", "USD", opts).getOrThrow()) + } + + @Test + fun roundingMode_up_awayFromZero() { + val opts = CurrencyFormatOptions { roundingMode = RoundingMode.UP } + assertEquals("$2.68", us.formatWithOptions("2.671", "USD", opts).getOrThrow()) + } + + @Test + fun leadingPlus_isHandled() { + assertEquals("$5.00", us.formatWithOptions("+5", "USD", CurrencyFormatOptions()).getOrThrow()) + } + + @Test + fun scientificNotation_isFormatted() { + assertEquals("$1,000.00", us.formatWithOptions("1e3", "USD", CurrencyFormatOptions()).getOrThrow()) + } + + @Test + fun defaultHalfEven_tieRoundsToEven() { + assertEquals("$2.66", us.formatWithOptions("2.665", "USD", CurrencyFormatOptions()).getOrThrow()) + } + + @Test + fun negativeAmount_isRounded_minusSign() { + assertEquals("-$2.68", us.formatWithOptions("-2.675", "USD", CurrencyFormatOptions()).getOrThrow()) + } + + @Test + fun negativeAmount_isRounded_parentheses() { + val opts = CurrencyFormatOptions { negativeStyle = NegativeStyle.PARENTHESES } + assertEquals("($2.68)", us.formatWithOptions("-2.675", "USD", opts).getOrThrow()) + } +} diff --git a/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/DecimalsTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/DecimalsTest.kt new file mode 100644 index 0000000..5d0b424 --- /dev/null +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/DecimalsTest.kt @@ -0,0 +1,121 @@ +package org.kimplify.kurrency + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DecimalsTest { + + @Test fun halfEven_tieToEven_roundsDown() { + assertEquals("2.66", Decimals.roundToScale("2.665", 2, RoundingMode.HALF_EVEN)) + } + + @Test fun halfEven_tieToEven_roundsUp() { + assertEquals("2.68", Decimals.roundToScale("2.675", 2, RoundingMode.HALF_EVEN)) + } + + @Test fun halfEven_aboveHalf_roundsUp() { + assertEquals("2.68", Decimals.roundToScale("2.6751", 2, RoundingMode.HALF_EVEN)) + } + + @Test fun halfEven_belowHalf_roundsDown() { + assertEquals("2.67", Decimals.roundToScale("2.6749", 2, RoundingMode.HALF_EVEN)) + } + + @Test fun halfEven_scaleZero() { + assertEquals("2", Decimals.roundToScale("2.5", 0, RoundingMode.HALF_EVEN)) + assertEquals("4", Decimals.roundToScale("3.5", 0, RoundingMode.HALF_EVEN)) + } + + @Test fun halfUp_tie_roundsAwayFromZero() { + assertEquals("2.67", Decimals.roundToScale("2.665", 2, RoundingMode.HALF_UP)) + assertEquals("3", Decimals.roundToScale("2.5", 0, RoundingMode.HALF_UP)) + } + + @Test fun down_truncates() { + assertEquals("2.67", Decimals.roundToScale("2.679", 2, RoundingMode.DOWN)) + assertEquals("2", Decimals.roundToScale("2.999", 0, RoundingMode.DOWN)) + } + + @Test fun up_anyRemainderRoundsUp() { + assertEquals("2.68", Decimals.roundToScale("2.671", 2, RoundingMode.UP)) + assertEquals("2.67", Decimals.roundToScale("2.670", 2, RoundingMode.UP)) + assertEquals("3", Decimals.roundToScale("2.001", 0, RoundingMode.UP)) + } + + @Test fun carryPropagates() { + assertEquals("10.00", Decimals.roundToScale("9.999", 2, RoundingMode.HALF_UP)) + assertEquals("1", Decimals.roundToScale("0.999", 0, RoundingMode.HALF_UP)) + assertEquals("100.0", Decimals.roundToScale("99.95", 1, RoundingMode.HALF_UP)) + } + + @Test fun padsWhenFewerDigitsThanScale() { + assertEquals("2.50", Decimals.roundToScale("2.5", 2, RoundingMode.HALF_EVEN)) + assertEquals("2.00", Decimals.roundToScale("2", 2, RoundingMode.HALF_EVEN)) + } + + @Test fun beyondDoublePrecision_isExact() { + assertEquals( + "9007199254740993.01", + Decimals.roundToScale("9007199254740993.005", 2, RoundingMode.HALF_UP), + ) + } + + @Test fun isZero_variants() { + assertTrue(Decimals.isZero("0")) + assertTrue(Decimals.isZero("0.00")) + assertTrue(Decimals.isZero("-0.0")) + assertFalse(Decimals.isZero("0.01")) + } + + @Test fun isNegative_excludesNegativeZero() { + assertTrue(Decimals.isNegative("-3.50")) + assertFalse(Decimals.isNegative("-0.00")) + assertFalse(Decimals.isNegative("3.50")) + } + + @Test fun abs_stripsSign() { + assertEquals("3.50", Decimals.abs("-3.50")) + assertEquals("3.50", Decimals.abs("3.50")) + } + + @Test fun abs_stripsLeadingPlus() { + assertEquals("3.50", Decimals.abs("+3.50")) + } + + @Test fun isOne_variants() { + assertTrue(Decimals.isOne("1")) + assertTrue(Decimals.isOne("1.00")) + assertTrue(Decimals.isOne("-1.0")) + assertFalse(Decimals.isOne("1.5")) + assertFalse(Decimals.isOne("10")) + } + + @Test fun carryGrowsIntegerWidthAtNonZeroScale() { + assertEquals("100.00", Decimals.roundToScale("99.999", 2, RoundingMode.HALF_UP)) + } + + @Test fun halfEven_tieWithTrailingNonZero_roundsUp() { + assertEquals("2.67", Decimals.roundToScale("2.6650001", 2, RoundingMode.HALF_EVEN)) + } + + @Test fun expandScientific_positiveExponent() { + assertEquals("1000", Decimals.expandScientific("1e3")) + assertEquals("1500", Decimals.expandScientific("1.5e3")) + assertEquals("1000", Decimals.expandScientific("1e+3")) + } + + @Test fun expandScientific_negativeExponent() { + assertEquals("0.0123", Decimals.expandScientific("1.23e-2")) + } + + @Test fun expandScientific_uppercaseAndSign() { + assertEquals("-2000", Decimals.expandScientific("-2E3")) + assertEquals("0", Decimals.expandScientific("0E0")) + } + + @Test fun expandScientific_plainPassthrough() { + assertEquals("1234.56", Decimals.expandScientific("1234.56")) + } +} diff --git a/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/serialization/CurrencyFormatOptionsSerializerTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/serialization/CurrencyFormatOptionsSerializerTest.kt index 7527f0e..94f78df 100644 --- a/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/serialization/CurrencyFormatOptionsSerializerTest.kt +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/serialization/CurrencyFormatOptionsSerializerTest.kt @@ -5,6 +5,7 @@ import org.kimplify.kurrency.CurrencyFormatOptions import org.kimplify.kurrency.NegativeStyle import org.kimplify.kurrency.SymbolDisplay import org.kimplify.kurrency.SymbolPosition +import org.kimplify.kurrency.RoundingMode import org.kimplify.kurrency.ZeroDisplay import kotlin.test.Test import kotlin.test.assertEquals @@ -79,4 +80,22 @@ class CurrencyFormatOptionsSerializerTest { assertTrue(!serialized.contains("grouping")) assertTrue(!serialized.contains("zeroDisplay")) } + + @Test + fun roundingModeRoundTrip() { + val options = CurrencyFormatOptions(roundingMode = RoundingMode.HALF_UP) + val serialized = json.encodeToString(CurrencyFormatOptions.serializer(), options) + assertTrue(serialized.contains("HALF_UP")) + val deserialized = json.decodeFromString(CurrencyFormatOptions.serializer(), serialized) + assertEquals(RoundingMode.HALF_UP, deserialized.roundingMode) + } + + @Test + fun roundingModeDefaultsToHalfEvenWhenAbsent() { + val deserialized = json.decodeFromString( + CurrencyFormatOptions.serializer(), + """{"symbolDisplay":"NONE"}""", + ) + assertEquals(RoundingMode.HALF_EVEN, deserialized.roundingMode) + } } diff --git a/kurrency-deci/src/commonMain/kotlin/org/kimplify/kurrency/deci/DeciExtensions.kt b/kurrency-deci/src/commonMain/kotlin/org/kimplify/kurrency/deci/DeciExtensions.kt index 139c214..c683c02 100644 --- a/kurrency-deci/src/commonMain/kotlin/org/kimplify/kurrency/deci/DeciExtensions.kt +++ b/kurrency-deci/src/commonMain/kotlin/org/kimplify/kurrency/deci/DeciExtensions.kt @@ -49,9 +49,6 @@ fun CurrencyFormat.formatIsoCurrencyStyle(amount: Deci, currency: Kurrency): Str /** * Formats a [Deci] amount with fine-grained [CurrencyFormatOptions] using this formatter's locale. * - * Formatting is delegated to the `Double`-based engine, so values beyond `Double` precision - * may be rounded; this does not guarantee exact decimal formatting of the [Deci]. - * * @param amount The [Deci] amount to format * @param currencyCode The ISO 4217 currency code (e.g., "USD", "EUR") * @param options The formatting options to apply @@ -66,9 +63,6 @@ fun CurrencyFormatter.formatWithOptions( /** * Formats a [Deci] amount with fine-grained [CurrencyFormatOptions] using this formatter's locale. * - * Formatting is delegated to the `Double`-based engine, so values beyond `Double` precision - * may be rounded; this does not guarantee exact decimal formatting of the [Deci]. - * * @param amount The [Deci] amount to format * @param currency The [Kurrency] whose ISO code should be used * @param options The formatting options to apply diff --git a/kurrency-deci/src/commonTest/kotlin/org/kimplify/kurrency/deci/DeciExactFormattingTest.kt b/kurrency-deci/src/commonTest/kotlin/org/kimplify/kurrency/deci/DeciExactFormattingTest.kt new file mode 100644 index 0000000..99150df --- /dev/null +++ b/kurrency-deci/src/commonTest/kotlin/org/kimplify/kurrency/deci/DeciExactFormattingTest.kt @@ -0,0 +1,18 @@ +package org.kimplify.kurrency.deci + +import org.kimplify.deci.Deci +import org.kimplify.kurrency.CurrencyFormatOptions +import org.kimplify.kurrency.CurrencyFormatter +import org.kimplify.kurrency.KurrencyLocale +import kotlin.test.Test +import kotlin.test.assertEquals + +class DeciExactFormattingTest { + + @Test + fun deci_beyondDoublePrecision_isExact() { + val formatter = CurrencyFormatter(KurrencyLocale.US) + val r = formatter.formatWithOptions(Deci("9007199254740993.01"), "USD", CurrencyFormatOptions()).getOrThrow() + assertEquals("$9,007,199,254,740,993.01", r) + } +}