Skip to content

robercoding/decimal-formatter

Repository files navigation

Decimal Formatter πŸ”’

Maven Central
Kotlin
Compose
License

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.

✨ Features

  • 🌍 Kotlin Multiplatform - Works on Android, iOS, and more
  • 🎨 Jetpack Compose - Ready-to-use UI components
  • πŸ’° Currency Support - Built-in prefix support for currency symbols
  • 🌎 International Formats - European, US, Swiss, and custom formats
  • ⚑ Real-time Formatting - Automatic formatting as users type
  • 🧩 Modular Design - Use core logic only or with UI components
  • πŸ”§ Highly Configurable - Decimal places, separators, max digits
  • πŸ“Š Structured Data - Get raw digits, formatted display, and full formatted display

πŸ“¦ Installation

Core Module (Platform Agnostic)

dependencies {  
    implementation("dev.robercoding:decimal-formatter-core:0.0.1-alpha01")
}  

Compose Module (UI Components)

dependencies {
    implementation("dev.robercoding:decimal-formatter-compose:0.0.1-alpha01")
    // Note: This automatically includes the core module
}  

πŸš€ Quick Start

Basic Usage (Core)

// Create a formatter
val formatter = DecimalFormatter(DecimalFormatterConfiguration.european())  
  
// Format numbers  
val formatted = formatter.format("123456") // "1.234,56"

Compose UI Components

The new API uses UiDecimalFormatter for better encapsulation and DecimalValue for structured state management:

@Composable
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") }
    )
}

@Composable
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") }
    )
}

@Composable
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")
        } 
    }
}

🎯 Components

Core Module Components

Component Description
DecimalFormatter Core formatting logic
DecimalFormatterConfiguration Formatting configuration
DecimalSeparator / ThousandSeparator Separator enums

Compose Module Components

Component Description
OutlinedDecimalTextField Material 3 outlined text field
UiDecimalFormatter UI-layer formatter with prefix support
DecimalValue Structured data class for formatted values

πŸ—οΈ DecimalValue Structure

The DecimalValue data class provides structured access to formatted data:

data class DecimalValue internal constructor(
    val rawDigits: String,      // "123456" - pure digits for any calculation you need
    val display: String,        // "1.234,56" - formatted without prefix  
    val fullDisplay: String?    // "€1.234,56" - formatted with prefix. Nullable if you didn't provide a prefix
)

// Note: You can only create `DecimalValue` using `UiDecimalFormatter.format()`
// Usage examples
val value = uiDecimalFormatter.format("123456")
println(value.rawDigits)    // "123456"
println(value.display)      // "1.234,56" -> Can vary depending on the type of formatter used
println(value.fullDisplay)  // "€1.234,56" -> Can vary depending on the type of formatter used.

// Use display for showing to users  
Text(value.fullDisplay ?: value.display)

βš™οΈ Configuration

Built-in Formats

// 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()

Custom Configuration

Any configuration can be customized using DecimalFormatterConfiguration:

DecimalFormatterConfiguration(  
    decimalSeparator = DecimalSeparator.COMMA,
    thousandSeparator = ThousandSeparator.SPACE,
    decimalPlaces = 3,
    maxDigits = 10
)  

Input Validation & Cleaning

// 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"

πŸ› Debug Logging

Enable debug logging to troubleshoot formatting issues:

class MyApplication : Application() {  
    override fun onCreate() {  
        super.onCreate()
        DecimalFormatterDebugConfig.setDebugEnabled(true)
    }
}

πŸ—οΈ Architecture

This library is split into two modules:

  • decimal-formatter-core - Pure Kotlin logic, works everywhere
  • decimal-formatter-compose - Jetpack Compose UI components
decimal-formatter/  
└── core/                         # Platform-agnostic formatting  
    └── formatter/
        β”œβ”€β”€ DecimalFormatter         # Core formatting logic
        β”œβ”€β”€ DecimalFormatterConfiguration # Formatting rules
        └── DecimalFormatterDebugConfig # Debug logging configuration
    └── model/
        β”œβ”€β”€ ThousandSeparator       # Enum for thousand separators
        └── DecimalSeparator         # Enum for decimal separators
    └── utils/
        └── LoggerUtils              # Utility for logging
└── compose/                     # Compose UI components  
    └── components/
        β”œβ”€β”€ DecimalTextField # Basic text field without any decorations
        └── OutlinedDecimalTextField # Material text field
    └── model/
        └── DecimalValue
    └── formatter/
        └── UiDecimalFormatter      # UI-layer formatter with prefix support

🀝 Contributing

Contributions are welcome!

πŸ“„ License

Copyright 2025 Roberto Fuentes  
  
Licensed under the Apache License, Version 2.0 (the "License");  
you may not use this file except in compliance with the License.  
You may obtain a copy of the License at  
  
    http://www.apache.org/licenses/LICENSE-2.0  
    
Unless required by applicable law or agreed to in writing, software  
distributed under the License is distributed on an "AS IS" BASIS,  
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
See the License for the specific language governing permissions and  
limitations under the License.  

πŸ”— Links

Made with ❀️ by Roberto Fuentes