Skip to content

Kimplify/Kurrency

Repository files navigation

Kurrency 💱

CI Kotlin Kotlin Multiplatform Android iOS JVM JS WasmJs License

Type-safe currency formatting for Kotlin Multiplatform, with locale-aware output and optional Compose integration.

Table of Contents

Features

  • 🌍 Multiplatform — Android, iOS, JVM, JS, and WasmJs from a single API
  • 🌐 Locale-aware — separators, grouping, and symbol placement per locale
  • 🎨 Compose integration — ready-to-use composables with reactive locale updates
  • Type-safe errorsResult-based API with a sealed KurrencyError hierarchy
  • 🔄 Multiple styles — symbol, ISO code, and compact formatting
  • 📦 Lightweight — minimal dependencies

Installation

Core library

dependencies {
    implementation("org.kimplify:kurrency-core:0.4.0")
}

Compose integration (optional)

dependencies {
    implementation("org.kimplify:kurrency-core:0.4.0")
    implementation("org.kimplify:kurrency-compose:0.4.0")
}

Quick Start

Create a CurrencyFormatter for a locale (the no-argument constructor uses the system locale), then format with Result-based methods.

import org.kimplify.kurrency.CurrencyFormatter

val formatter = CurrencyFormatter() // system locale

formatter.formatCurrencyStyleResult("1234.56", "USD")    // "$1,234.56" (en-US)
formatter.formatIsoCurrencyStyleResult("1234.56", "USD") // "USD 1,234.56"
formatter.formatCompactStyleResult("1234567.89", "USD")  // "$1.2M"

Fraction digits are a property of the currency and never vary by locale, so they are available statically:

CurrencyFormatter.getFractionDigits("USD")          // Result.success(2)
CurrencyFormatter.getFractionDigitsOrDefault("JPY") // 0

Locale vs. fraction digits

A currency always uses the same number of fraction digits, regardless of where it is displayed:

  • USD → 2 digits, JPY → 0 digits, BHD → 3 digits

The locale only controls presentation: decimal separator, grouping separator, symbol placement, and spacing.

val us = CurrencyFormatter(KurrencyLocale.US)
val de = CurrencyFormatter(KurrencyLocale.GERMANY)

us.formatCurrencyStyleResult("1234.56", "USD") // "$1,234.56"
de.formatCurrencyStyleResult("1234.56", "USD") // "1.234,56 $"

// JPY has no fraction digits in any locale
us.formatCurrencyStyleResult("1234", "JPY")    // "¥1,234"

Working with the Currency type

Currencies are represented by Kurrency. Build one from a code with fromCode, or use a predefined constant.

import org.kimplify.kurrency.Kurrency

val usd = Kurrency.fromCode("USD").getOrThrow()
val eur = Kurrency.EUR
val valid = Kurrency.isValid("USD") // true

usd.formatAmount("1234.56").getOrNull() // "$1,234.56" (system locale)
eur.formatAmount("1234.56", locale = KurrencyLocale.GERMANY).getOrNull() // "1.234,56 €"

Locale Management

Predefined locales

import org.kimplify.kurrency.CurrencyFormatter
import org.kimplify.kurrency.KurrencyLocale

val formatter = CurrencyFormatter(KurrencyLocale.JAPAN)
formatter.formatCurrencyStyleResult("1234.56", "JPY") // "¥1,235"

Available constants include:

KurrencyLocale.US             // en-US
KurrencyLocale.UK             // en-GB
KurrencyLocale.CANADA         // en-CA
KurrencyLocale.CANADA_FRENCH  // fr-CA
KurrencyLocale.GERMANY        // de-DE
KurrencyLocale.FRANCE         // fr-FR
KurrencyLocale.ITALY          // it-IT
KurrencyLocale.SPAIN          // es-ES
KurrencyLocale.JAPAN          // ja-JP
KurrencyLocale.CHINA          // zh-CN
KurrencyLocale.KOREA          // ko-KR
KurrencyLocale.BRAZIL         // pt-BR
KurrencyLocale.RUSSIA         // ru-RU
KurrencyLocale.SAUDI_ARABIA   // ar-SA
KurrencyLocale.INDIA          // hi-IN

Custom and system locales

// BCP 47 language tag
val austrian = KurrencyLocale.fromLanguageTag("de-AT").getOrNull()

// Device locale
val system = KurrencyLocale.systemLocale()

Compose Integration

Add kurrency-compose for Jetpack Compose Multiplatform support.

rememberCurrencyFormatter

The formatter is recreated when the locale changes (key-based recomposition). Formatting is cheap, so call it directly during composition rather than caching it.

import org.kimplify.kurrency.compose.rememberCurrencyFormatter
import org.kimplify.kurrency.KurrencyLocale

@Composable
fun PriceDisplay(amount: String, currencyCode: String) {
    var locale by remember { mutableStateOf(KurrencyLocale.US) }
    val formatter = rememberCurrencyFormatter(locale)

    Column {
        Text("Price: ${formatter.formatCurrencyStyle(amount, currencyCode)}")
        Button(onClick = { locale = KurrencyLocale.GERMANY }) {
            Text("Switch to German locale")
        }
    }
}

LocalCurrencyFormatter

Provide a formatter to an entire subtree via CompositionLocal.

import org.kimplify.kurrency.compose.ProvideCurrencyFormatter
import org.kimplify.kurrency.compose.LocalCurrencyFormatter
import org.kimplify.kurrency.KurrencyLocale

@Composable
fun App() {
    ProvideCurrencyFormatter(locale = KurrencyLocale.US) {
        ProductScreen()
    }
}

@Composable
fun ProductScreen() {
    val formatter = LocalCurrencyFormatter.current
    Text("Price: ${formatter.formatCurrencyStyle("99.99", "USD")}")
}

Compose locale interop

Convert a Compose Locale to a KurrencyLocale:

import androidx.compose.ui.text.intl.Locale
import org.kimplify.kurrency.KurrencyLocale
import org.kimplify.kurrency.compose.fromComposeLocale

@Composable
fun MyComposable() {
    val kurrencyLocale = KurrencyLocale.fromComposeLocale(Locale.current)
    val formatter = rememberCurrencyFormatter(kurrencyLocale)
}

API Reference

CurrencyFormatter

A formatter bound to a locale. Fraction-digit lookups are available on the companion object.

// Construction
CurrencyFormatter(locale: KurrencyLocale = KurrencyLocale.systemLocale())

// Instance formatting (Result-based)
fun formatCurrencyStyleResult(amount: String, currencyCode: String): Result<String>
fun formatIsoCurrencyStyleResult(amount: String, currencyCode: String): Result<String>
fun formatCompactStyleResult(amount: String, currencyCode: String): Result<String>

// Companion (fraction digits do not vary by locale)
CurrencyFormatter.getFractionDigits(currencyCode: String): Result<Int>
CurrencyFormatter.getFractionDigitsOrDefault(currencyCode: String): Int

Kurrency

// Factory and validation (no public constructor)
Kurrency.fromCode(code: String): Result<Kurrency>
Kurrency.isValid(code: String): Boolean

// Predefined constants: USD, EUR, GBP, JPY, AUD, CAD, CHF, CNY, INR, KRW, MXN, ...

// Instance formatting
fun formatAmount(amount: String, style: CurrencyStyle = CurrencyStyle.Standard, locale: KurrencyLocale = KurrencyLocale.systemLocale()): Result<String>
fun formatAmount(amount: Double, style: CurrencyStyle = CurrencyStyle.Standard, locale: KurrencyLocale = KurrencyLocale.systemLocale()): Result<String>
fun formatAmountOrEmpty(amount: String, style: CurrencyStyle = CurrencyStyle.Standard, locale: KurrencyLocale = KurrencyLocale.systemLocale()): String

CurrencyStyle is one of Standard (symbol), Iso (ISO code), or Accounting (parentheses for negatives).

CurrencyFormat (interface)

The shared surface implemented per platform. These methods return a plain String (falling back to the input on failure); use the *Result methods on CurrencyFormatter for explicit error handling.

interface CurrencyFormat {
    fun getFractionDigitsOrDefault(currencyCode: String, default: Int = 2): Int
    fun formatCurrencyStyle(amount: String, currencyCode: String): String
    fun formatIsoCurrencyStyle(amount: String, currencyCode: String): String
    fun formatCompactStyle(amount: String, currencyCode: String): String
}

KurrencyLocale

KurrencyLocale.fromLanguageTag(languageTag: String): Result<KurrencyLocale>
KurrencyLocale.systemLocale(): KurrencyLocale

val languageTag: String        // e.g. "en-US"
val decimalSeparator: Char
val groupingSeparator: Char

Compose extensions (kurrency-compose)

@Composable fun rememberCurrencyFormatter(locale: KurrencyLocale = KurrencyLocale.current()): CurrencyFormat
@Composable fun rememberSystemCurrencyFormatter(): CurrencyFormat

val LocalCurrencyFormatter: ProvidableCompositionLocal<CurrencyFormat>

@Composable fun ProvideCurrencyFormatter(locale: KurrencyLocale, content: @Composable () -> Unit)
@Composable fun ProvideSystemCurrencyFormatter(content: @Composable () -> Unit)

fun KurrencyLocale.Companion.fromComposeLocale(composeLocale: Locale): KurrencyLocale
@Composable fun KurrencyLocale.Companion.current(): KurrencyLocale

Error Handling

Result-based methods return Result<String>, with failures modeled as KurrencyError:

CurrencyFormatter(KurrencyLocale.US)
    .formatCurrencyStyleResult("1234.56", "USD")
    .onSuccess { println(it) }
    .onFailure { error ->
        when (error) {
            is KurrencyError.InvalidAmount -> println("Invalid amount")
            is KurrencyError.InvalidCurrencyCode -> println("Invalid currency")
            else -> println("Formatting error")
        }
    }
Error Meaning
KurrencyError.InvalidCurrencyCode Unknown or malformed currency code
KurrencyError.InvalidAmount Amount could not be parsed
KurrencyError.FormattingFailure Platform formatting error
KurrencyError.FractionDigitsFailure Could not resolve fraction digits
KurrencyError.InvalidLocale Unrecognized locale tag

Platform Support

Platform Backend
Android (API 24+) ICU (android.icu)
iOS (13+) NSNumberFormatter
JVM (17+) java.text.NumberFormat
JS (Browser / Node.js) Intl.NumberFormat
WasmJs (Browser) Intl.NumberFormat

License

Apache License 2.0 — Copyright © 2025

About

Type-safe currency formatting for Kotlin Multiplatform with Compose support.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors