decimal-formatter
0.2.0-alpha02indexedLibrary formats decimal numbers with automatic input formatting and provides UI components for financial apps. Supports international formats, currency symbols, and real-time updates.
Library formats decimal numbers with automatic input formatting and provides UI components for financial apps. Supports international formats, currency symbols, and real-time updates.
A Kotlin Multiplatform library for decimal number formatting with automatic input formatting and Jetpack Compose UI components.
Perfect for financial apps, calculators, and any application that needs professional decimal number input and display.
dependencies {
implementation("dev.robercoding:decimal-formatter-core:0.0.1-alpha01")
}
dependencies {
implementation("dev.robercoding:decimal-formatter-compose:0.0.1-alpha01")
// Note: This automatically includes the core module
}
// Create a formatter
val formatter = DecimalFormatter(DecimalFormatterConfiguration.european())
// Format numbers
val formatted = formatter.format("123456") // "1.234,56"
The new API uses UiDecimalFormatter for better encapsulation and DecimalValue for structured state management:
| Component | Description |
|---|---|
OutlinedDecimalTextField | Material 3 outlined text field |
The DecimalValue data class provides structured access to formatted data:
data class DecimalValue internal constructor(
rawDigits: String,
display: String,
fullDisplay: String?
)
value = uiDecimalFormatter.format()
println(value.rawDigits)
println(value.display)
println(value.fullDisplay)
Text(value.fullDisplay ?: value.display)
// European: 1.234,56
DecimalFormatterConfiguration.european()
// US: 1,234.56
DecimalFormatterConfiguration.us()
// Swiss: 1'234.56
DecimalFormatterConfiguration.swiss()
// Plain: 1234.56 (no thousand separators)
DecimalFormatterConfiguration.plain()
Any configuration can be customized using DecimalFormatterConfiguration:
DecimalFormatterConfiguration(
decimalSeparator = DecimalSeparator.COMMA,
thousandSeparator = ThousandSeparator.SPACE,
decimalPlaces = 3,
maxDigits = 10
)
// The formatter automatically handles invalid input
formatter.format("abc123def") // Filters the letters
formatter.format("$1,234.56") // Extracts digits → "123456" → "1,234.56"
formatter.format("000123") // Removes leading zeros → "123" → "1.23"
Choose between two input interpretation modes:
Traditional behavior where digits fill decimal places from the right. Perfect for cash registers and financial apps.
val formatter = DecimalFormatter(
DecimalFormatterConfiguration.us(
decimalPlaces = 2,
inputMode = DecimalInputMode.FRACTIONAL // Default
)
)
formatter.format("1") // "0.01" (1 cent)
formatter.format("123") // "1.23" ($1.23)
formatter.format("123456") // "1,234.56"
Treats input as whole numbers and pads decimals with zeros. Also accepts formatted input with decimal separators. Ideal for measurements, weights, and scientific notation.
val formatter = DecimalFormatter(
DecimalFormatterConfiguration.us(
decimalPlaces = 3,
inputMode = DecimalInputMode.FIXED_DECIMALS
)
)
// Raw digits (no decimal separator)
formatter.format("1") // "1.000" (1 unit)
formatter.format("14") // "14.000" (14 units)
// Formatted input (with decimal separator)
formatter.format("1.45") // "1.450" (pads with zeros)
formatter.format("14.50") // "14.500"
formatter.format()
Key Differences:
Example Use Cases:
// Weight input with FIXED_DECIMALS
val weightFormatter = DecimalFormatter(
DecimalFormatterConfiguration.plain(
decimalPlaces = 2,
suffix = " kg",
inputMode = DecimalInputMode.FIXED_DECIMALS
)
)
weightFormatter.format("75.5") // "75.50 kg"
// Price input with FRACTIONAL (traditional)
val priceFormatter = DecimalFormatter(
DecimalFormatterConfiguration.usd(
inputMode = DecimalInputMode.FRACTIONAL
)
)
priceFormatter.format("1995") // "$19.95"
Enable debug logging to troubleshoot formatting issues:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
DecimalFormatterDebugConfig.setDebugEnabled(true)
}
}
This library is split into two modules:
decimal-formatter-core - Pure Kotlin logic, works everywheredecimal-formatter-compose - Jetpack Compose UI componentsdecimal-formatter/
└── core/ # Platform-agnostic formatting
└── formatter/
├── DecimalFormatter # Core formatting logic
├── DecimalFormatterConfiguration # Formatting rules
└── DecimalFormatterDebugConfig # Debug logging configuration
└── model/
├── DecimalInputMode # Input mode enum (FRACTIONAL/FIXED_DECIMALS)
├── ThousandSeparator
├── DecimalSeparator
└── FormattedDecimal
└── utils/
└── LoggerUtils
└── compose/
└── components/
├── DecimalTextField
└── OutlinedDecimalTextField
└── model/
└── DecimalValue
└── formatter/
└── UiDecimalFormatter
Contributions are welcome!
Copyright 2025 Roberto Fuentes
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance the License.
You may obtain a copy of the License at
http:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
Made with ❤️ by Roberto Fuentes
fun CurrencyInput() {
// Create a UI formatter with prefix
val europeanFormatter = rememberUiDecimalFormatter(configuration = DecimalFormatterConfiguration.european(), prefix = "€")
// Use DecimalValue for structured state
var price by remember {
mutableStateOf(europeanFormatter.format("")) // Start with empty if needed, will format to "€0,00"
}
OutlinedDecimalTextField(
decimalValue = price,
onValueChange = { price = it },
decimalFormatter = europeanFormatter,
label = { Text("Price") }
)
}
fun WeightInput() {
// Custom configuration without prefix
val decimalFormatter = remember {
UiDecimalFormatter(
decimalFormatter = DecimalFormatter(
DecimalFormatterConfiguration(
decimalSeparator = DecimalSeparator.DOT,
thousandSeparator = ThousandSeparator.COMMA,
decimalPlaces = 3,
maxDigits = 10
)
),
prefix = null
)
}
var weight by remember {
mutableStateOf(decimalFormatter.format("123456")) // Initialize with value, will format to "123,456"
}
OutlinedDecimalTextField(
decimalValue = weight,
onValueChange = { weight = it },
decimalFormatter = decimalFormatter,
label = { Text("Kilograms") }
)
}
fun DynamicFormatterExample() {
var isEuropean by remember { mutableStateOf(false) }
val usFormatter = rememberUiDecimalFormatter(DecimalFormatterConfiguration.us(), prefix = "$")
val europeanFormatter = rememberUiDecimalFormatter(DecimalFormatterConfiguration.european(), prefix = "€")
var amount by remember { mutableStateOf(usFormatter.format("123456")) }
Column {
OutlinedDecimalTextField(
decimalValue = amount,
onValueChange = { amount = it },
decimalFormatter = if (isEuropean) europeanFormatter else usFormatter
)
Button(
onClick = {
isEuropean = !isEuropean
// Reformat existing data with new formatter
val newFormatter = if (isEuropean) europeanFormatter else usFormatter
amount = newFormatter.format(amount.rawDigits)
}
) {
Text("Switch Format")
}
}
}
| Component |
|---|
| Description |
|---|
DecimalFormatter | Core formatting logic |
DecimalFormatterConfiguration | Formatting configuration |
DecimalSeparator / ThousandSeparator | Separator enums |
DecimalInputMode | Input mode enum (FRACTIONAL or FIXED_DECIMALS) |
UiDecimalFormatter| UI-layer formatter with prefix support |
DecimalValue | Structured data class for formatted values |
| Feature | FRACTIONAL | FIXED_DECIMALS |
|---|
| Input "1" with 2 decimals | "0.01" | "1.00" |
| Input "123" with 2 decimals | "1.23" | "123.00" |
| Accepts decimal separators | ❌ (digits only) | ✅ (. and ,) |
| Trailing zeros | ❌ | ✅ Padded |
| Use case | Currency input | Measurements, weights, scientific |
Surfaced from shared tags and platforms — no rankings paid for.