diff --git a/README.md b/README.md index b5230e1..4bf70c6 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,14 @@ ![WasmJs](https://img.shields.io/badge/WasmJs-βœ“-purple.svg?style=flat&logo=webassembly) ![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat) -Type-safe currency formatting for Kotlin Multiplatform with Compose support. +Type-safe currency formatting for Kotlin Multiplatform with Compose support and comprehensive locale management. ## Table of Contents - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) +- [Locale Management](#locale-management) - [Compose Integration](#compose-integration) - [API Reference](#api-reference) - [Error Handling](#error-handling) @@ -25,106 +26,296 @@ Type-safe currency formatting for Kotlin Multiplatform with Compose support. ## Features -- Multi-platform support (Android, iOS, JVM, JS, WasmJs) -- Compose integration -- Type-safe error handling +- 🌍 **Multi-platform support** - Android, iOS, JVM, JS, WasmJs +- 🌐 **Locale management** - Format currencies for any locale +- 🎨 **Compose integration** - Ready-to-use Composables with reactive locale updates +- βœ… **Type-safe error handling** - Result-based API +- πŸ”„ **Multiple format styles** - Standard currency symbols or ISO codes +- πŸ“¦ **Lightweight** - Minimal dependencies ## Installation +### Core Library + +```kotlin +dependencies { + implementation("org.kimplify:kurrency:1.0.0") +} +``` + +### Compose Integration (Optional) + ```kotlin dependencies { - implementation("io.github.chilinoodles:kurrency:0.1.0") + implementation("org.kimplify:kurrency:1.0.0") + implementation("org.kimplify:kurrency-compose:1.0.0") } ``` ## Quick Start -### Basic Usage +### Basic Usage (System Locale) ```kotlin -import com.chilinoodles.kurrency.Currency +import org.kimplify.kurrency.CurrencyFormatter -val currency = Currency("USD") +// Using the singleton with system locale +val result: Result = CurrencyFormatter.formatCurrencyStyle("1234.56", "USD") +val formatted = result.getOrNull() // "$1,234.56" (in en-US locale) -val result: Result = currency.formatAmount("1234.56") -val formatted = currency.formatAmountOrEmpty("1234.56") +// Get fraction digits for a currency +val fractionDigits = CurrencyFormatter.getFractionDigits("USD").getOrNull() // 2 ``` ### Formatting Styles ```kotlin -val currency = Currency("USD") +// Standard currency format (with symbol) +CurrencyFormatter.formatCurrencyStyle("1234.56", "USD") +// Result: "$1,234.56" (US), "1.234,56 $" (DE) -currency.formatAmount("1234.56", CurrencyStyle.Standard) // "$1,234.56" -currency.formatAmount("1234.56", CurrencyStyle.Iso) // "USD 1,234.56" +// ISO format (with currency code) +CurrencyFormatter.formatIsoCurrencyStyle("1234.56", "USD") +// Result: "USD 1,234.56" ``` -### Validation +## Locale Management + +### Using Predefined Locales ```kotlin -Currency.isValidCode("USD") // true -CurrencyMetadata.isSupported("USD") // true +import org.kimplify.kurrency.CurrencyFormatter +import org.kimplify.kurrency.KurrencyLocale + +// Create formatters for specific locales +val usFormatter = CurrencyFormatter.create(KurrencyLocale.US) +val germanFormatter = CurrencyFormatter.create(KurrencyLocale.GERMANY) +val japaneseFormatter = CurrencyFormatter.create(KurrencyLocale.JAPAN) + +// Format the same amount in different locales +usFormatter.formatCurrencyStyle("1234.56", "USD") // "$1,234.56" +germanFormatter.formatCurrencyStyle("1234.56", "EUR") // "1.234,56 €" +japaneseFormatter.formatCurrencyStyle("1234.56", "JPY") // "Β₯1,235" +``` + +### Available Predefined Locales + +```kotlin +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 Locales from Language Tags + +```kotlin +// Create locale from BCP 47 language tag +val locale = KurrencyLocale.fromLanguageTag("de-AT").getOrNull() // German (Austria) +val formatter = CurrencyFormatter.create(locale) +``` + +### System Locale + +```kotlin +// Get the device's current locale +val systemLocale = KurrencyLocale.systemLocale() +val formatter = CurrencyFormatter.createWithSystemLocale() +``` + +### Integration with Compose Multiplatform Locale + +```kotlin +import androidx.compose.ui.text.intl.Locale +import org.kimplify.kurrency.toKurrencyLocale + +@Composable +fun MyComposable() { + val composeLocale = Locale.current + val kurrencyLocale = composeLocale.toKurrencyLocale().getOrNull() + val formatter = kurrencyLocale?.let { CurrencyFormatter.create(it) } +} ``` ## Compose Integration +Add the `kurrency-compose` dependency for Jetpack Compose Multiplatform support. + +### Using rememberCurrencyFormatter + +The formatter automatically recreates when the locale changes (key-based recomposition). + +```kotlin +import org.kimplify.kurrency.compose.rememberCurrencyFormatter +import org.kimplify.kurrency.KurrencyLocale + +@Composable +fun PriceDisplay(amount: String, currencyCode: String) { + var selectedLocale by remember { mutableStateOf(KurrencyLocale.US) } + + // Formatter recreates when locale changes + val formatter = rememberCurrencyFormatter(locale = selectedLocale) + + val formattedPrice = remember(amount, currencyCode) { + formatter.formatCurrencyStyle(amount, currencyCode).getOrNull() ?: "" + } + + Column { + Text("Price: $formattedPrice") + + Button(onClick = { selectedLocale = KurrencyLocale.GERMANY }) { + Text("Switch to German locale") + } + } +} +``` + +### Using LocalCurrencyFormatter (CompositionLocal) + +Provide a formatter for an entire subtree of your composition. + ```kotlin -import com.chilinoodles.kurrency.rememberCurrencyState +import org.kimplify.kurrency.compose.ProvideCurrencyFormatter +import org.kimplify.kurrency.compose.LocalCurrencyFormatter +import org.kimplify.kurrency.KurrencyLocale @Composable -fun PriceDisplay() { - val currencyState = rememberCurrencyState("EUR", "1234.56") - - Text(text = currencyState.formattedAmount) - - Button(onClick = { currencyState.updateCurrency("USD") }) { - Text("Switch to USD") +fun App() { + var appLocale by remember { mutableStateOf(KurrencyLocale.US) } + + ProvideCurrencyFormatter(locale = appLocale) { + // All child composables can access the formatter + HomeScreen() + ProductScreen() } } + +@Composable +fun ProductScreen() { + // Access the provided formatter + val formatter = LocalCurrencyFormatter.current + + val price = remember { + formatter.formatCurrencyStyle("99.99", "USD").getOrNull() ?: "" + } + + Text("Price: $price") +} ``` -### Property Delegation +### Reactive Locale Updates + +Combine with Compose's State system for dynamic locale switching: ```kotlin @Composable -fun PriceDisplay() { - val currencyState = rememberCurrencyState("USD", "1234.56") - val price by currencyState.formattedAmount() - - Text(text = price) +fun MultiCurrencyDisplay() { + var locale by remember { mutableStateOf(KurrencyLocale.US) } + val formatter = rememberCurrencyFormatter(locale) + + val prices = listOf( + "USD" to "100.00", + "EUR" to "85.50", + "JPY" to "11000" + ) + + Column { + prices.forEach { (currency, amount) -> + val formatted = remember(locale, currency, amount) { + formatter.formatCurrencyStyle(amount, currency).getOrNull() ?: "" + } + Text(formatted) + } + + Row { + Button(onClick = { locale = KurrencyLocale.US }) { Text("US") } + Button(onClick = { locale = KurrencyLocale.UK }) { Text("UK") } + Button(onClick = { locale = KurrencyLocale.JAPAN }) { Text("JP") } + } + } } ``` ## API Reference -### Currency +### CurrencyFormatter (Singleton) ```kotlin -val currency = Currency(code = "USD") +// Format with system locale +CurrencyFormatter.formatCurrencyStyle(amount: String, currencyCode: String): Result +CurrencyFormatter.formatIsoCurrencyStyle(amount: String, currencyCode: String): Result +CurrencyFormatter.getFractionDigits(currencyCode: String): Result +CurrencyFormatter.getFractionDigitsOrDefault(currencyCode: String): Int + +// Create instances with custom locales +CurrencyFormatter.create(locale: KurrencyLocale? = null): CurrencyFormat +CurrencyFormatter.createWithSystemLocale(): CurrencyFormat +``` -currency.formatAmount(amount: String, style: CurrencyStyle = Standard): Result -currency.formatAmount(amount: Double, style: CurrencyStyle = Standard): Result -currency.formatAmountOrEmpty(amount: String, style: CurrencyStyle = Standard): String -currency.formatAmountOrEmpty(amount: Double, style: CurrencyStyle = Standard): String +### CurrencyFormat (Interface) -Currency.isValidCode(code: String): Boolean +```kotlin +interface CurrencyFormat { + fun getFractionDigits(currencyCode: String): Result + fun formatCurrencyStyle(amount: String, currencyCode: String): Result + fun formatIsoCurrencyStyle(amount: String, currencyCode: String): Result +} ``` -### CurrencyState +### KurrencyLocale ```kotlin -val currencyState = rememberCurrencyState(currencyCode: String, amount: String?) +// Create from language tag +KurrencyLocale.fromLanguageTag(languageTag: String): Result + +// Get system locale +KurrencyLocale.systemLocale(): KurrencyLocale -currencyState.formattedAmount: String -currencyState.formattedAmountResult: Result -currencyState.updateCurrency(currencyCode: String) -currencyState.updateAmount(newAmount: String) +// Predefined locales +KurrencyLocale.US, UK, CANADA, GERMANY, FRANCE, JAPAN, etc. + +// Properties +val languageTag: String // e.g., "en-US" ``` -### CurrencyMetadata +### Compose Extensions (kurrency-compose) ```kotlin -CurrencyMetadata.isSupported(currencyCode: String): Boolean +// Remember formatter with specific locale +@Composable +fun rememberCurrencyFormatter(locale: KurrencyLocale): CurrencyFormat + +// Remember formatter with system locale +@Composable +fun rememberSystemCurrencyFormatter(): CurrencyFormat + +// CompositionLocal +val LocalCurrencyFormatter: CompositionLocal + +// Provider composables +@Composable +fun ProvideCurrencyFormatter(locale: KurrencyLocale, content: @Composable () -> Unit) + +@Composable +fun ProvideSystemCurrencyFormatter(content: @Composable () -> Unit) + +// Compose Locale extensions +fun Locale.toKurrencyLocale(): Result +fun KurrencyLocale.Companion.fromComposeLocale(composeLocale: Locale): Result + +@Composable +fun KurrencyLocale.Companion.current(): KurrencyLocale ``` ## Error Handling @@ -132,9 +323,11 @@ CurrencyMetadata.isSupported(currencyCode: String): Boolean All formatting methods return `Result` for type-safe error handling: ```kotlin -currency.formatAmount("1234.56") +val formatter = CurrencyFormatter.create(KurrencyLocale.US) + +formatter.formatCurrencyStyle("1234.56", "USD") .onSuccess { formatted -> println(formatted) } - .onFailure { error -> + .onFailure { error -> when (error) { is KurrencyError.InvalidAmount -> println("Invalid amount") is KurrencyError.InvalidCurrencyCode -> println("Invalid currency") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03c1e53..2eaf1be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -appVersionCode = "2" -appVersionName = "0.1.1" +appVersionCode = "3" +appVersionName = "1.0.0" # SDK Versions minSdk = "21" @@ -10,13 +10,13 @@ javaVersion = "21" jvmVersion = "JVM_21" agp = "8.13.1" -androidx-activity = "1.11.0" +androidx-activity = "1.12.0" androidx-appcompat = "1.7.1" androidx-core = "1.17.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.6" androidx-testExt = "1.3.0" -composeHotReload = "1.0.0-rc03" +composeHotReload = "1.0.0-rc04" composeMultiplatform = "1.9.3" junit = "4.13.2" kotlin = "2.2.21" @@ -24,7 +24,7 @@ kotlinx-coroutines = "1.10.2" kotlinStdlib = "2.2.21" runner = "1.7.0" core = "1.7.0" -maven-publish = "0.34.0" +maven-publish = "0.35.0" kotlinx-datetime = "0.7.1" cedar-logging = "0.2.2" @@ -47,7 +47,7 @@ kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } -cedar-logging = { group = "io.github.chilinoodles", name = "cedar-logging", version.ref = "cedar-logging" } +cedar-logging = { group = "org.kimplify", name = "cedar-logging", version.ref = "cedar-logging" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig index 81ab67e..111269e 100644 --- a/iosApp/Configuration/Config.xcconfig +++ b/iosApp/Configuration/Config.xcconfig @@ -1,7 +1,7 @@ TEAM_ID= PRODUCT_NAME=Kurrency -PRODUCT_BUNDLE_IDENTIFIER=io.github.chilinoodles.Kurrency$(TEAM_ID) +PRODUCT_BUNDLE_IDENTIFIER=org.kimplify.Kurrency$(TEAM_ID) CURRENT_PROJECT_VERSION=1 -MARKETING_VERSION=1.0 \ No newline at end of file +MARKETING_VERSION=1.0 diff --git a/kurrency-compose/build.gradle.kts b/kurrency-compose/build.gradle.kts new file mode 100644 index 0000000..2ccbd85 --- /dev/null +++ b/kurrency-compose/build.gradle.kts @@ -0,0 +1,141 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.maven.publish) +} + +kotlin { + androidTarget { + compilations.configureEach { + compileTaskProvider.get().compilerOptions { + jvmTarget.set(JvmTarget.valueOf(libs.versions.jvmVersion.get())) + } + } + } + + jvm() + + wasmJs { + browser() + outputModuleName.set("Kurrency-Compose") + } + + js(IR) { + browser() + nodejs() + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "KurrencyCompose" + isStatic = true + } + } + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":kurrency-core")) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.ui) + } + } + + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + } + } + + val androidMain by getting { + dependencies { + implementation("androidx.core:core-ktx:1.12.0") + } + } + + val jvmMain by getting + + val wasmJsMain by getting + val jsMain by getting + + val webMain by creating { + dependsOn(commonMain) + wasmJsMain.dependsOn(this) + jsMain.dependsOn(this) + } + + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + namespace = "org.kimplify.kurrency.compose" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +mavenPublishing { + publishToMavenCentral() + signAllPublications() + coordinates("org.kimplify", "kurrency-compose", libs.versions.appVersionName.get()) + + pom { + name = "Kurrency Compose" + description = "Jetpack Compose Multiplatform extensions for Kurrency currency formatting library" + url = "https://github.com/ChiliNoodles/Kurrency" + + licenses { + license { + name = "Apache License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0" + } + } + + developers { + developer { + id = "merkost" + name = "Konstantin Merenkov" + email = "merkostdev@gmail.com" + } + + developer { + id = "diogocavaiar" + name = "Diogo Cavaiar" + email = "cavaiarconsulting@gmail.com" + } + } + + scm { + url = "https://github.com/ChiliNoodles/Kurrency" + } + } +} diff --git a/kurrency-compose/src/commonMain/kotlin/org/kimplify/kurrency/compose/CurrencyFormatterComposables.kt b/kurrency-compose/src/commonMain/kotlin/org/kimplify/kurrency/compose/CurrencyFormatterComposables.kt new file mode 100644 index 0000000..7ddcd94 --- /dev/null +++ b/kurrency-compose/src/commonMain/kotlin/org/kimplify/kurrency/compose/CurrencyFormatterComposables.kt @@ -0,0 +1,154 @@ +package org.kimplify.kurrency.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import org.kimplify.kurrency.CurrencyFormat +import org.kimplify.kurrency.CurrencyFormatter +import org.kimplify.kurrency.KurrencyLocale + +/** + * CompositionLocal for providing a default [CurrencyFormat] throughout the composition hierarchy. + * + * By default, uses the system locale. Override this to provide a custom formatter for the entire + * app or a subtree of the composition. + * + * Example usage: + * ``` + * CompositionLocalProvider(LocalCurrencyFormatter provides customFormatter) { + * // All composables in this tree will use customFormatter by default + * MyApp() + * } + * ``` + */ +val LocalCurrencyFormatter = staticCompositionLocalOf { + CurrencyFormatter.createWithSystemLocale() +} + +/** + * Remembers a [CurrencyFormat] instance with the specified locale. + * + * The formatter will be recreated whenever the locale changes, ensuring that formatting + * automatically updates when the locale is changed. + * + * @param locale The locale to use for formatting. When this value changes, a new formatter + * will be created and composables will recompose. + * @return A memoized CurrencyFormat instance + * + * Example usage: + * ``` + * @Composable + * fun PriceDisplay(amount: String, currencyCode: String) { + * var selectedLocale by remember { mutableStateOf(KurrencyLocale.US) } + * val formatter = rememberCurrencyFormatter(locale = selectedLocale) + * + * val formattedPrice = remember(amount, currencyCode) { + * formatter.formatCurrencyStyle(amount, currencyCode).getOrNull() ?: "" + * } + * + * Text(formattedPrice) + * } + * ``` + */ +@Composable +fun rememberCurrencyFormatter(locale: KurrencyLocale): CurrencyFormat { + return remember(locale) { + CurrencyFormatter.create(locale) + } +} + +/** + * Remembers a [CurrencyFormat] instance with the system's default locale. + * + * This is useful when you want to explicitly use the system locale and recreate + * the formatter in response to system locale changes. + * + * @return A memoized CurrencyFormat instance using the system locale + * + * Example usage: + * ``` + * @Composable + * fun PriceDisplay(amount: String, currencyCode: String) { + * val formatter = rememberSystemCurrencyFormatter() + * val formattedPrice = remember(amount, currencyCode) { + * formatter.formatCurrencyStyle(amount, currencyCode).getOrNull() ?: "" + * } + * Text(formattedPrice) + * } + * ``` + */ +@Composable +fun rememberSystemCurrencyFormatter(): CurrencyFormat { + val systemLocale = KurrencyLocale.systemLocale() + return remember(systemLocale) { + CurrencyFormatter.create(systemLocale) + } +} + +/** + * Provides a [CurrencyFormat] to the composition hierarchy using [LocalCurrencyFormatter]. + * + * All child composables can access this formatter using `LocalCurrencyFormatter.current`. + * + * @param locale The locale to use for the provided formatter + * @param content The composable content that will have access to this formatter + * + * Example usage: + * ``` + * @Composable + * fun App() { + * var appLocale by remember { mutableStateOf(KurrencyLocale.US) } + * + * ProvideCurrencyFormatter(locale = appLocale) { + * // All composables here can use LocalCurrencyFormatter.current + * Screen1() + * Screen2() + * } + * } + * + * @Composable + * fun Screen1() { + * val formatter = LocalCurrencyFormatter.current + * val price = formatter.formatCurrencyStyle("100.50", "USD").getOrNull() + * Text(price ?: "") + * } + * ``` + */ +@Composable +fun ProvideCurrencyFormatter( + locale: KurrencyLocale, + content: @Composable () -> Unit +) { + val formatter = rememberCurrencyFormatter(locale) + CompositionLocalProvider(LocalCurrencyFormatter provides formatter) { + content() + } +} + +/** + * Provides a [CurrencyFormat] using the system locale to the composition hierarchy. + * + * This is a convenience function equivalent to `ProvideCurrencyFormatter(KurrencyLocale.systemLocale())`. + * + * @param content The composable content that will have access to this formatter + * + * Example usage: + * ``` + * @Composable + * fun App() { + * ProvideSystemCurrencyFormatter { + * MyAppContent() + * } + * } + * ``` + */ +@Composable +fun ProvideSystemCurrencyFormatter( + content: @Composable () -> Unit +) { + val formatter = rememberSystemCurrencyFormatter() + CompositionLocalProvider(LocalCurrencyFormatter provides formatter) { + content() + } +} diff --git a/kurrency/.gitignore b/kurrency-core/.gitignore similarity index 100% rename from kurrency/.gitignore rename to kurrency-core/.gitignore diff --git a/kurrency/build.gradle.kts b/kurrency-core/build.gradle.kts similarity index 98% rename from kurrency/build.gradle.kts rename to kurrency-core/build.gradle.kts index 34701d2..110d5d2 100644 --- a/kurrency/build.gradle.kts +++ b/kurrency-core/build.gradle.kts @@ -99,7 +99,7 @@ kotlin { } android { - namespace = "com.chilinoodles.kurrency" + namespace = "org.kimplify.kurrency" compileSdk = 36 defaultConfig { diff --git a/kurrency/src/androidMain/AndroidManifest.xml b/kurrency-core/src/androidMain/AndroidManifest.xml similarity index 100% rename from kurrency/src/androidMain/AndroidManifest.xml rename to kurrency-core/src/androidMain/AndroidManifest.xml diff --git a/kurrency/src/androidMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt b/kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt similarity index 67% rename from kurrency/src/androidMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt rename to kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt index c0f8677..16c48b3 100644 --- a/kurrency/src/androidMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt +++ b/kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt @@ -1,12 +1,14 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import android.icu.text.NumberFormat import android.icu.util.Currency -import com.chilinoodles.kurrency.extensions.replaceCommaWithDot +import org.kimplify.kurrency.extensions.replaceCommaWithDot import java.math.BigDecimal import java.util.Locale -actual class CurrencyFormatterImpl : CurrencyFormat { +actual class CurrencyFormatterImpl actual constructor(kurrencyLocale: KurrencyLocale) : CurrencyFormat { + + private val platformLocale: Locale = kurrencyLocale.locale actual override fun getFractionDigits(currencyCode: String): Result = runCatching { @@ -36,7 +38,6 @@ actual class CurrencyFormatterImpl : CurrencyFormat { style: Int ): Result = runCatching { - val locale = Locale.getDefault() val currency = Currency.getInstance(currencyCode.uppercase()) val normalized = amount.replaceCommaWithDot().trim() @@ -46,7 +47,7 @@ actual class CurrencyFormatterImpl : CurrencyFormat { val value = BigDecimal(normalized) - val numberFormat = NumberFormat.getInstance(locale, style).apply { + val numberFormat = NumberFormat.getInstance(platformLocale, style).apply { this.currency = currency val fractionDigits = currency.defaultFractionDigits if (fractionDigits >= 0) { @@ -59,8 +60,15 @@ actual class CurrencyFormatterImpl : CurrencyFormat { } } -actual fun isValidCurrency(currencyCode: String): Boolean = - runCatching { - Currency.getInstance(currencyCode.uppercase()) - true - }.getOrDefault(false) \ No newline at end of file +actual fun isValidCurrency(currencyCode: String): Boolean { + if (currencyCode.length != 3 || !currencyCode.all { it.isLetter() }) { + return false + } + + val upperCode = currencyCode.uppercase() + return runCatching { + // Use Currency.getAvailableCurrencies() to get the valid currency codes + val availableCurrencies = Currency.getAvailableCurrencies() + availableCurrencies.any { it.currencyCode == upperCode } + }.getOrDefault(false) +} diff --git a/kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt b/kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt new file mode 100644 index 0000000..e1fd33f --- /dev/null +++ b/kurrency-core/src/androidMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt @@ -0,0 +1,68 @@ +package org.kimplify.kurrency + +import java.util.Locale + +/** + * Android implementation of KurrencyLocale using java.util.Locale. + */ +actual class KurrencyLocale internal constructor(internal val locale: Locale) { + actual val languageTag: String + get() = locale.toLanguageTag() + + actual companion object { + actual fun fromLanguageTag(languageTag: String): Result { + return try { + // Validate format before creating locale + if (languageTag.isBlank()) { + return Result.failure(IllegalArgumentException("Language tag cannot be blank")) + } + + // Basic BCP 47 validation + val bcp47Pattern = Regex( + "^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?(-[0-9A-Za-z]+)*$", + RegexOption.IGNORE_CASE + ) + + if (!bcp47Pattern.matches(languageTag)) { + return Result.failure(IllegalArgumentException("Invalid language tag format: $languageTag")) + } + + val locale = Locale.forLanguageTag(languageTag) + Result.success(KurrencyLocale(locale)) + } catch (e: Exception) { + Result.failure(e) + } + } + + actual fun systemLocale(): KurrencyLocale { + return KurrencyLocale(Locale.getDefault()) + } + + // Predefined locales + actual val US: KurrencyLocale = KurrencyLocale(Locale.US) + actual val UK: KurrencyLocale = KurrencyLocale(Locale.UK) + actual val CANADA: KurrencyLocale = KurrencyLocale(Locale.CANADA) + actual val CANADA_FRENCH: KurrencyLocale = KurrencyLocale(Locale.CANADA_FRENCH) + actual val GERMANY: KurrencyLocale = KurrencyLocale(Locale.GERMANY) + actual val FRANCE: KurrencyLocale = KurrencyLocale(Locale.FRANCE) + actual val ITALY: KurrencyLocale = KurrencyLocale(Locale.ITALY) + actual val SPAIN: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("es-ES")) + actual val JAPAN: KurrencyLocale = KurrencyLocale(Locale.JAPAN) + actual val CHINA: KurrencyLocale = KurrencyLocale(Locale.CHINA) + actual val KOREA: KurrencyLocale = KurrencyLocale(Locale.KOREA) + actual val BRAZIL: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("pt-BR")) + actual val RUSSIA: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("ru-RU")) + actual val SAUDI_ARABIA: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("ar-SA")) + actual val INDIA: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("hi-IN")) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KurrencyLocale) return false + return locale == other.locale + } + + override fun hashCode(): Int = locale.hashCode() + + override fun toString(): String = "KurrencyLocale($languageTag)" +} diff --git a/kurrency/src/androidTest/kotlin/com/chilinoodles/kurrency/CurrencyFormatterAndroidTest.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyFormatterAndroidTest.kt similarity index 98% rename from kurrency/src/androidTest/kotlin/com/chilinoodles/kurrency/CurrencyFormatterAndroidTest.kt rename to kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyFormatterAndroidTest.kt index fd1621f..a6e7c05 100644 --- a/kurrency/src/androidTest/kotlin/com/chilinoodles/kurrency/CurrencyFormatterAndroidTest.kt +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyFormatterAndroidTest.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyFormatterTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyFormatterTestInstrumented.kt new file mode 100644 index 0000000..d198dcc --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyFormatterTestInstrumented.kt @@ -0,0 +1,172 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class CurrencyFormatterTestInstrumented { + + @Test + fun testGetFractionDigitsForUSD() { + val result = CurrencyFormatter.getFractionDigits("USD") + + assertTrue(result.isSuccess) + assertEquals(2, result.getOrNull()) + } + + @Test + fun testGetFractionDigitsForJPY() { + val result = CurrencyFormatter.getFractionDigits("JPY") + + assertTrue(result.isSuccess) + assertEquals(0, result.getOrNull()) + } + + @Test + fun testGetFractionDigitsForInvalidCurrency() { + val result = CurrencyFormatter.getFractionDigits("INVALID") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testGetFractionDigitsForShortCode() { + val result = CurrencyFormatter.getFractionDigits("US") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testGetFractionDigitsOrDefaultForUSD() { + val fractionDigits = CurrencyFormatter.getFractionDigitsOrDefault("USD") + + assertEquals(2, fractionDigits) + } + + @Test + fun testGetFractionDigitsOrDefaultForInvalidCurrency() { + val fractionDigits = CurrencyFormatter.getFractionDigitsOrDefault("INVALID") + + assertEquals(2, fractionDigits) + } + + @Test + fun testFormatCurrencyStyleSuccess() { + val result = CurrencyFormatter.formatCurrencyStyle("100.50", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatIsoCurrencyStyleSuccess() { + val result = CurrencyFormatter.formatIsoCurrencyStyle("100.50", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatCurrencyStyleWithInvalidAmount() { + val result = CurrencyFormatter.formatCurrencyStyle("invalid", "USD") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidAmount) + } + + @Test + fun testFormatCurrencyStyleWithInvalidCurrency() { + val result = CurrencyFormatter.formatCurrencyStyle("100.00", "INVALID") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testFormatWithEmptyAmount() { + val result = CurrencyFormatter.formatCurrencyStyle("", "USD") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidAmount) + } + + @Test + fun testFormatWithBlankAmount() { + val result = CurrencyFormatter.formatCurrencyStyle(" ", "USD") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidAmount) + } + + @Test + fun testFormatWithZero() { + val result = CurrencyFormatter.formatCurrencyStyle("0", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatWithNegative() { + val result = CurrencyFormatter.formatCurrencyStyle("-50.25", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatWithCommaDecimalSeparator() { + val result = CurrencyFormatter.formatCurrencyStyle("100,50", "EUR") + + print(result.exceptionOrNull()) + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatMultipleCurrencies() { + val currencies = listOf("USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD") + + currencies.forEach { code -> + val result = CurrencyFormatter.formatCurrencyStyle("100.00", code) + assertTrue(result.isSuccess, "Failed to format $code") + } + } + + @Test + fun testCurrencyCodeValidation() { + val invalidCodes = listOf("", "US", "USDD", "123", "US1", "us$") + + invalidCodes.forEach { code -> + val result = CurrencyFormatter.formatCurrencyStyle("100.00", code) + assertTrue(result.isFailure, "Should fail for invalid code: $code") + } + } + + @Test + fun testAmountValidation() { + val invalidAmounts = listOf("", " ", "abc", "12.34.56", "12,34.56", "NaN") + + invalidAmounts.forEach { amount -> + val result = CurrencyFormatter.formatCurrencyStyle(amount, "USD") + assertTrue(result.isFailure, "Should fail for invalid amount: $amount") + } + } + + @Test + fun testValidAmountFormats() { + val validAmounts = listOf("0", "1", "100", "100.5", "100.50", "1234.56", "-100", "999999.99") + + validAmounts.forEach { amount -> + val result = CurrencyFormatter.formatCurrencyStyle(amount, "USD") + assertTrue(result.isSuccess, "Should succeed for valid amount: $amount") + } + } +} + diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTestInstrumented.kt new file mode 100644 index 0000000..cce921b --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTestInstrumented.kt @@ -0,0 +1,229 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class CurrencyMetadataTestInstrumented { + + @Test + fun testParseValidCurrencyCodeUppercase() { + val result = CurrencyMetadata.parse("USD") + + assertTrue(result.isSuccess) + val metadata = result.getOrNull() + assertNotNull(metadata) + assertEquals("USD", metadata.code) + assertEquals("US Dollar", metadata.displayName) + assertEquals("$", metadata.symbol) + assertEquals("US", metadata.countryIso) + assertEquals(2, metadata.fractionDigits) + } + + @Test + fun testParseValidCurrencyCodeLowercase() { + val result = CurrencyMetadata.parse("usd") + + assertTrue(result.isSuccess) + val metadata = result.getOrNull() + assertNotNull(metadata) + assertEquals("USD", metadata.code) + } + + @Test + fun testParseValidCurrencyCodeMixedCase() { + val result = CurrencyMetadata.parse("EuR") + + assertTrue(result.isSuccess) + val metadata = result.getOrNull() + assertNotNull(metadata) + assertEquals("EUR", metadata.code) + assertEquals("Euro", metadata.displayName) + } + + @Test + fun testParseInvalidCurrencyCode() { + val result = CurrencyMetadata.parse("XXX") + + assertTrue(result.isFailure) + val exception = result.exceptionOrNull() + assertTrue(exception is KurrencyError.InvalidCurrencyCode) + assertEquals("XXX", (exception as KurrencyError.InvalidCurrencyCode).code) + } + + @Test + fun testParseEmptyString() { + val result = CurrencyMetadata.parse("") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testParseBlankString() { + val result = CurrencyMetadata.parse(" ") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testParseShortCode() { + val result = CurrencyMetadata.parse("US") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testParseLongCode() { + val result = CurrencyMetadata.parse("USDD") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testGetAllReturnsAllCurrencies() { + val allCurrencies = CurrencyMetadata.getAll() + + assertEquals(50, allCurrencies.size) + assertTrue(allCurrencies.contains(CurrencyMetadata.USD)) + assertTrue(allCurrencies.contains(CurrencyMetadata.EUR)) + assertTrue(allCurrencies.contains(CurrencyMetadata.GBP)) + assertTrue(allCurrencies.contains(CurrencyMetadata.JPY)) + assertTrue(allCurrencies.contains(CurrencyMetadata.KRW)) + assertTrue(allCurrencies.contains(CurrencyMetadata.TWD)) + assertTrue(allCurrencies.contains(CurrencyMetadata.KWD)) + } + + @Test + fun testAllCurrenciesHaveValidData() { + val allCurrencies = CurrencyMetadata.getAll() + + allCurrencies.forEach { currency -> + assertEquals(3, currency.code.length) + assertTrue(currency.code.all { it.isLetter() }) + assertFalse(currency.displayName.isBlank()) + assertFalse(currency.symbol.isBlank()) + assertEquals(2, currency.countryIso.length) + assertTrue(currency.fractionDigits >= 0) + assertFalse(currency.flag.isBlank()) + } + } + + @Test + fun testUsdMetadata() { + assertEquals("USD", CurrencyMetadata.USD.code) + assertEquals("US Dollar", CurrencyMetadata.USD.displayName) + assertEquals("$", CurrencyMetadata.USD.symbol) + assertEquals("US", CurrencyMetadata.USD.countryIso) + assertEquals("πŸ‡ΊπŸ‡Έ", CurrencyMetadata.USD.flag) + assertEquals(2, CurrencyMetadata.USD.fractionDigits) + } + + @Test + fun testEurMetadata() { + assertEquals("EUR", CurrencyMetadata.EUR.code) + assertEquals("Euro", CurrencyMetadata.EUR.displayName) + assertEquals("€", CurrencyMetadata.EUR.symbol) + assertEquals("EU", CurrencyMetadata.EUR.countryIso) + assertEquals("πŸ‡ͺπŸ‡Ί", CurrencyMetadata.EUR.flag) + assertEquals(2, CurrencyMetadata.EUR.fractionDigits) + } + + @Test + fun testJpyMetadata() { + assertEquals("JPY", CurrencyMetadata.JPY.code) + assertEquals("Japanese Yen", CurrencyMetadata.JPY.displayName) + assertEquals("Β₯", CurrencyMetadata.JPY.symbol) + assertEquals("JP", CurrencyMetadata.JPY.countryIso) + assertEquals("πŸ‡―πŸ‡΅", CurrencyMetadata.JPY.flag) + assertEquals(0, CurrencyMetadata.JPY.fractionDigits) + } + + @Test + fun testClpMetadata() { + assertEquals("CLP", CurrencyMetadata.CLP.code) + assertEquals(0, CurrencyMetadata.CLP.fractionDigits) + } + + @Test + fun testParseWithWhitespace() { + val result = CurrencyMetadata.parse(" USD ") + + assertTrue(result.isSuccess) + val metadata = result.getOrNull() + assertNotNull(metadata) + assertEquals("USD", metadata.code) + } + + @Test + fun testAllCurrenciesHaveUniqueCodes() { + val allCurrencies = CurrencyMetadata.getAll() + val codes = allCurrencies.map { it.code } + val uniqueCodes = codes.toSet() + + assertEquals(codes.size, uniqueCodes.size) + } + + @Test + fun testParseMultipleCurrencies() { + val testCodes = listOf("USD", "EUR", "GBP", "JPY", "CNY", "AUD", "CAD", "CHF") + + testCodes.forEach { code -> + val result = CurrencyMetadata.parse(code) + assertTrue(result.isSuccess, "Failed to parse $code") + val metadata = result.getOrNull() + assertNotNull(metadata) + assertEquals(code.uppercase(), metadata.code) + } + } + + @Test + fun testKrwMetadata() { + assertEquals("KRW", CurrencyMetadata.KRW.code) + assertEquals("South Korean Won", CurrencyMetadata.KRW.displayName) + assertEquals("β‚©", CurrencyMetadata.KRW.symbol) + assertEquals("KR", CurrencyMetadata.KRW.countryIso) + assertEquals("πŸ‡°πŸ‡·", CurrencyMetadata.KRW.flag) + assertEquals(0, CurrencyMetadata.KRW.fractionDigits) + } + + @Test + fun testKwdMetadata() { + assertEquals("KWD", CurrencyMetadata.KWD.code) + assertEquals("Kuwaiti Dinar", CurrencyMetadata.KWD.displayName) + assertEquals("Ψ―.Ωƒ", CurrencyMetadata.KWD.symbol) + assertEquals("KW", CurrencyMetadata.KWD.countryIso) + assertEquals("πŸ‡°πŸ‡Ό", CurrencyMetadata.KWD.flag) + assertEquals(3, CurrencyMetadata.KWD.fractionDigits) + } + + @Test + fun testVndMetadata() { + assertEquals("VND", CurrencyMetadata.VND.code) + assertEquals("Vietnamese Dong", CurrencyMetadata.VND.displayName) + assertEquals("β‚«", CurrencyMetadata.VND.symbol) + assertEquals(0, CurrencyMetadata.VND.fractionDigits) + } + + @Test + fun testParseNewCurrencies() { + val newCurrencies = listOf("KRW", "TWD", "VND", "ARS", "COP", "UAH", "PKR", "NGN", "KES", "QAR", "KWD", "OMR") + + newCurrencies.forEach { code -> + val result = CurrencyMetadata.parse(code) + assertTrue(result.isSuccess, "Failed to parse $code") + val metadata = result.getOrNull() + assertNotNull(metadata) + assertEquals(code.uppercase(), metadata.code) + } + } +} + diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyStateTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyStateTestInstrumented.kt new file mode 100644 index 0000000..53f7a6f --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyStateTestInstrumented.kt @@ -0,0 +1,213 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class CurrencyStateTestInstrumented { + + @Test + fun testCurrencyStateCreation() { + val state = CurrencyState("USD", "100.00") + + assertEquals("USD", state.currency.code) + assertEquals("100.00", state.amount) + } + + @Test + fun testCurrencyStateDefaultAmount() { + val state = CurrencyState("USD") + + assertEquals("USD", state.currency.code) + assertEquals("0.00", state.amount) + } + + @Test + fun testFormattedAmount() { + val state = CurrencyState("USD", "1234.56") + val formatted = state.formattedAmount + + assertNotNull(formatted) + assertTrue(formatted.isNotEmpty()) + } + + @Test + fun testFormattedAmountIso() { + val state = CurrencyState("USD", "1234.56") + val formatted = state.formattedAmountIso + + assertNotNull(formatted) + assertTrue(formatted.isNotEmpty()) + } + + @Test + fun testFormattedAmountResult() { + val state = CurrencyState("USD", "1234.56") + val result = state.formattedAmountResult + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormattedAmountIsoResult() { + val state = CurrencyState("USD", "1234.56") + val result = state.formattedAmountIsoResult + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testUpdateCurrency() { + val state = CurrencyState("USD", "100.00") + val originalCurrency = state.currency + + state.updateCurrency("EUR") + + assertEquals("EUR", state.currency.code) + assertNotEquals(originalCurrency, state.currency) + assertEquals("100.00", state.amount) + } + + @Test + fun testUpdateAmount() { + val state = CurrencyState("USD", "100.00") + + state.updateAmount("250.50") + + assertEquals("USD", state.currency.code) + assertEquals("250.50", state.amount) + } + + @Test + fun testUpdateCurrencyAndAmount() { + val state = CurrencyState("USD", "100.00") + + state.updateCurrencyAndAmount("EUR", "500.00") + + assertEquals("EUR", state.currency.code) + assertEquals("500.00", state.amount) + } + + @Test + fun testFormattedAmountAfterUpdate() { + val state = CurrencyState("USD", "100.00") + val beforeUpdate = state.formattedAmount + + state.updateAmount("200.00") + val afterUpdate = state.formattedAmount + + assertNotEquals(beforeUpdate, afterUpdate) + } + + @Test + fun testFormattedAmountAfterCurrencyUpdate() { + val state = CurrencyState("USD", "100.00") + val beforeUpdate = state.formattedAmount + + state.updateCurrency("EUR") + val afterUpdate = state.formattedAmount + + assertNotEquals(beforeUpdate, afterUpdate) + } + + @Test + fun testStateWithInvalidAmount() { + val state = CurrencyState("USD", "invalid") + val formatted = state.formattedAmount + + assertEquals("", formatted) + } + + @Test + fun testStateWithInvalidAmountResult() { + val state = CurrencyState("USD", "invalid") + val result = state.formattedAmountResult + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidAmount) + } + + @Test + fun testStateWithInvalidCurrency() { + val state = CurrencyState("INVALID", "100.00") + val formatted = state.formattedAmount + + assertEquals("", formatted) + } + + @Test + fun testStateWithZeroAmount() { + val state = CurrencyState("USD", "0.00") + val formatted = state.formattedAmount + + assertNotNull(formatted) + assertTrue(formatted.isNotEmpty()) + } + + @Test + fun testStateWithNegativeAmount() { + val state = CurrencyState("USD", "-100.50") + val formatted = state.formattedAmount + + assertNotNull(formatted) + assertTrue(formatted.isNotEmpty()) + } + + @Test + fun testMultipleSequentialUpdates() { + val state = CurrencyState("USD", "100.00") + + state.updateAmount("200.00") + assertEquals("200.00", state.amount) + + state.updateAmount("300.00") + assertEquals("300.00", state.amount) + + state.updateCurrency("EUR") + assertEquals("EUR", state.currency.code) + } + + @Test + fun testFormattedAmountDelegateCreation() { + val state = CurrencyState("USD", "100.00") + val delegate = state.formattedAmount() + + assertNotNull(delegate) + } + + @Test + fun testFormattedAmountDelegateWithStyle() { + val state = CurrencyState("USD", "100.00") + val delegate = state.formattedAmount(CurrencyStyle.Iso) + + assertNotNull(delegate) + } + + @Test + fun testStateWithDifferentCurrencies() { + val currencies = listOf("USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD") + + currencies.forEach { code -> + val state = CurrencyState(code, "100.00") + val formatted = state.formattedAmount + assertNotNull(formatted, "Failed for currency: $code") + } + } + + @Test + fun testStateImmutabilityAfterCreation() { + val state1 = CurrencyState("USD", "100.00") + val state2 = CurrencyState("USD", "100.00") + + assertEquals(state1.currency.code, state2.currency.code) + assertEquals(state1.amount, state2.amount) + } +} + diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyTestInstrumented.kt new file mode 100644 index 0000000..ab958e2 --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/CurrencyTestInstrumented.kt @@ -0,0 +1,294 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class CurrencyTestInstrumented { + + @Test + fun testCurrencyCreation() { + val currency = Currency("USD") + assertEquals("USD", currency.code) + assertEquals(2, currency.fractionDigits) + } + + @Test + fun testJapaneseCurrencyFractionDigits() { + val currency = Currency("JPY") + assertEquals("JPY", currency.code) + assertEquals(0, currency.fractionDigits) + } + + @Test + fun testFormatAmountReturnsSuccess() { + val currency = Currency("USD") + val result = currency.formatAmount("100.50") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatAmountWithDoubleReturnsSuccess() { + val currency = Currency("USD") + val result = currency.formatAmount(100.50) + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatAmountStandardStyle() { + val currency = Currency("USD") + val result = currency.formatAmount("1234.56", CurrencyStyle.Standard) + + assertTrue(result.isSuccess) + val formatted = result.getOrNull() + assertNotNull(formatted) + } + + @Test + fun testFormatAmountIsoStyle() { + val currency = Currency("USD") + val result = currency.formatAmount("1234.56", CurrencyStyle.Iso) + + assertTrue(result.isSuccess) + val formatted = result.getOrNull() + assertNotNull(formatted) + } + + @Test + fun testFormatAmountWithInvalidAmount() { + val currency = Currency("USD") + val result = currency.formatAmount("invalid") + + assertTrue(result.isFailure) + val exception = result.exceptionOrNull() + assertTrue(exception is KurrencyError.InvalidAmount) + } + + @Test + fun testFormatAmountWithEmptyString() { + val currency = Currency("USD") + val result = currency.formatAmount("") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidAmount) + } + + @Test + fun testFormatAmountWithInvalidCurrencyCode() { + val currency = Currency("INVALID") + val result = currency.formatAmount("100.00") + + assertTrue(result.isFailure) + val exception = result.exceptionOrNull() + assertTrue(exception is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testFormatAmountWithShortCurrencyCode() { + val currency = Currency("US") + val result = currency.formatAmount("100.00") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testFormatAmountWithLongCurrencyCode() { + val currency = Currency("USDD") + val result = currency.formatAmount("100.00") + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is KurrencyError.InvalidCurrencyCode) + } + + @Test + fun testFormatAmountOrEmpty() { + val currency = Currency("USD") + val formatted = currency.formatAmountOrEmpty("100.50") + + assertNotNull(formatted) + assertFalse(formatted.isEmpty()) + } + + @Test + fun testFormatAmountOrEmptyWithInvalidAmount() { + val currency = Currency("USD") + val formatted = currency.formatAmountOrEmpty("invalid") + + assertEquals("", formatted) + } + + @Test + fun testFormatAmountWithZero() { + val currency = Currency("USD") + val result = currency.formatAmount("0.00") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatAmountWithNegativeNumber() { + val currency = Currency("USD") + val result = currency.formatAmount("-100.50") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatAmountWithVeryLargeNumber() { + val currency = Currency("USD") + val result = currency.formatAmount("999999999.99") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFormatAmountWithCommaDecimalSeparator() { + val currency = Currency("EUR") + val result = currency.formatAmount("100,50") + + assertTrue(result.isSuccess) + } + + @Test + fun testPropertyDelegation() { + val currency = Currency("USD") + val delegate = currency.format("100.50") + + assertNotNull(delegate) + } + + @Test + fun testPropertyDelegationWithStyle() { + val currency = Currency("USD") + val delegate = currency.format("100.50", CurrencyStyle.Iso) + + assertNotNull(delegate) + } + + @Test + fun testEuroFormatting() { + val currency = Currency("EUR") + val result = currency.formatAmount("1234.56") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testBritishPoundFormatting() { + val currency = Currency("GBP") + val result = currency.formatAmount("1234.56") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testJapaneseYenFormatting() { + val currency = Currency("JPY") + val result = currency.formatAmount("1234") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testSwissFrancFormatting() { + val currency = Currency("CHF") + val result = currency.formatAmount("1234.56") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testCurrencyEquality() { + val currency1 = Currency("USD") + val currency2 = Currency("USD") + + assertEquals(currency1, currency2) + assertTrue(currency1 == currency2) + } + + @Test + fun testCurrencyInequality() { + val currency1 = Currency("USD") + val currency2 = Currency("EUR") + + assertFalse(currency1 == currency2) + assertTrue(currency1 != currency2) + } + + @Test + fun testCurrencyEqualityWithDifferentFractionDigits() { + val currency1 = Currency("USD", 2) + val currency2 = Currency("USD", 3) + + assertEquals(currency1, currency2) + assertTrue(currency1 == currency2) + } + + @Test + fun testCurrencyHashCode() { + val currency1 = Currency("USD") + val currency2 = Currency("USD") + + assertEquals(currency1.hashCode(), currency2.hashCode()) + } + + @Test + fun testCurrencyHashCodeDifferentCodes() { + val currency1 = Currency("USD") + val currency2 = Currency("EUR") + + assertTrue(currency1.hashCode() != currency2.hashCode()) + } + + @Test + fun testIsValidWithValidCurrency() { + assertTrue(Currency.isValid("USD")) + assertTrue(Currency.isValid("EUR")) + assertTrue(Currency.isValid("GBP")) + assertTrue(Currency.isValid("JPY")) + } + + @Test + fun testIsValidWithInvalidFormat() { + assertFalse(Currency.isValid("US")) + assertFalse(Currency.isValid("USDD")) + assertFalse(Currency.isValid("123")) + assertFalse(Currency.isValid("US$")) + } + + @Test + fun testIsValidWithInvalidCurrencyCode() { + assertFalse(Currency.isValid("INVALID")) + assertFalse(Currency.isValid("XYZ")) + } + + @Test + fun testIsValidWithEmptyString() { + assertFalse(Currency.isValid("")) + } + + @Test + fun testIsValidWithLowercase() { + assertTrue(Currency.isValid("usd")) + assertTrue(Currency.isValid("eur")) + assertTrue(Currency.isValid("jpy")) + } +} + diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/KurrencyErrorTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/KurrencyErrorTestInstrumented.kt new file mode 100644 index 0000000..66cc761 --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/KurrencyErrorTestInstrumented.kt @@ -0,0 +1,133 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class KurrencyErrorTestInstrumented { + + @Test + fun testInvalidCurrencyCodeError() { + val error = KurrencyError.InvalidCurrencyCode("XYZ") + + assertEquals("XYZ", error.code) + assertEquals("Invalid currency code: XYZ", error.errorMessage) + assertNotNull(error.message) + } + + @Test + fun testInvalidAmountError() { + val error = KurrencyError.InvalidAmount("abc") + + assertEquals("abc", error.amount) + assertEquals("Invalid amount: abc", error.errorMessage) + assertNotNull(error.message) + } + + @Test + fun testFormattingFailureError() { + val cause = RuntimeException("Test exception") + val error = KurrencyError.FormattingFailure("USD", "100.00", cause) + + assertEquals("USD", error.currencyCode) + assertEquals("100.00", error.amount) + assertEquals("Formatting failed for USD: 100.00", error.errorMessage) + assertEquals(cause, error.cause) + assertNotNull(error.message) + } + + @Test + fun testFractionDigitsFailureError() { + val cause = RuntimeException("Test exception") + val error = KurrencyError.FractionDigitsFailure("USD", cause) + + assertEquals("USD", error.currencyCode) + assertEquals("Failed to get fraction digits for USD", error.errorMessage) + assertEquals(cause, error.cause) + assertNotNull(error.message) + } + + @Test + fun testErrorIsException() { + val error = KurrencyError.InvalidCurrencyCode("XYZ") + + assertTrue(error is Exception) + assertTrue(error is Throwable) + } + + @Test + fun testErrorCanBeCaught() { + var caught = false + + try { + throw KurrencyError.InvalidAmount("test") + } catch (e: KurrencyError) { + caught = true + } + + assertTrue(caught) + } + + @Test + fun testInvalidCurrencyCodeInResult() { + val currency = Currency("INVALID") + val result = currency.formatAmount("100.00") + + assertTrue(result.isFailure) + val error = result.exceptionOrNull() + assertTrue(error is KurrencyError.InvalidCurrencyCode) + assertEquals("INVALID", (error as KurrencyError.InvalidCurrencyCode).code) + } + + @Test + fun testInvalidAmountInResult() { + val currency = Currency("USD") + val result = currency.formatAmount("invalid") + + assertTrue(result.isFailure) + val error = result.exceptionOrNull() + assertTrue(error is KurrencyError.InvalidAmount) + assertEquals("invalid", (error as KurrencyError.InvalidAmount).amount) + } + + @Test + fun testErrorMessageIsAccessible() { + val error = KurrencyError.InvalidCurrencyCode("TEST") + + assertNotNull(error.errorMessage) + assertTrue(error.errorMessage.contains("TEST")) + } + + @Test + fun testErrorWithEmptyValues() { + val error1 = KurrencyError.InvalidCurrencyCode("") + assertEquals("", error1.code) + assertTrue(error1.errorMessage.contains("Invalid currency code")) + + val error2 = KurrencyError.InvalidAmount("") + assertEquals("", error2.amount) + assertTrue(error2.errorMessage.contains("Invalid amount")) + } + + @Test + fun testErrorMessagesAreDifferent() { + val error1 = KurrencyError.InvalidCurrencyCode("USD") + val error2 = KurrencyError.InvalidAmount("100") + val error3 = KurrencyError.FormattingFailure("EUR", "200", RuntimeException()) + val error4 = KurrencyError.FractionDigitsFailure("GBP", RuntimeException()) + + val messages = setOf( + error1.errorMessage, + error2.errorMessage, + error3.errorMessage, + error4.errorMessage + ) + + assertEquals(4, messages.size) + } +} + diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTestInstrumented.kt new file mode 100644 index 0000000..f18fa70 --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTestInstrumented.kt @@ -0,0 +1,111 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class KurrencyLocaleTestInstrumented { + + @Test + fun testFromLanguageTag_validTags() { + val validTags = listOf( + "en-US", + "en-GB", + "fr-FR", + "de-DE", + "ja-JP", + "zh-CN", + "ar-SA", + "pt-BR", + "es-ES" + ) + + validTags.forEach { tag -> + val result = KurrencyLocale.fromLanguageTag(tag) + assertTrue(result.isSuccess, "Expected $tag to be valid") + assertEquals(tag, result.getOrNull()?.languageTag?.replace("_", "-")) + } + } + + @Test + fun testFromLanguageTag_invalidTags() { + val invalidTags = listOf( + "", + " ", + "invalid", + "123", + "en_", + "_US" + ) + + invalidTags.forEach { tag -> + val result = KurrencyLocale.fromLanguageTag(tag) + assertTrue(result.isFailure, "Expected '$tag' to be invalid") + } + } + + @Test + fun testSystemLocale() { + val systemLocale = KurrencyLocale.systemLocale() + assertNotNull(systemLocale) + assertTrue(systemLocale.languageTag.isNotBlank()) + } + + @Test + fun testPredefinedLocales() { + // Test that all predefined locales are valid + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.UK, + KurrencyLocale.CANADA, + KurrencyLocale.CANADA_FRENCH, + KurrencyLocale.GERMANY, + KurrencyLocale.FRANCE, + KurrencyLocale.ITALY, + KurrencyLocale.SPAIN, + KurrencyLocale.JAPAN, + KurrencyLocale.CHINA, + KurrencyLocale.KOREA, + KurrencyLocale.BRAZIL, + KurrencyLocale.RUSSIA, + KurrencyLocale.SAUDI_ARABIA, + KurrencyLocale.INDIA + ) + + locales.forEach { locale -> + assertNotNull(locale) + assertTrue(locale.languageTag.isNotBlank()) + } + } + + @Test + fun testLocaleEquality() { + val locale1 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + val locale2 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + val locale3 = KurrencyLocale.fromLanguageTag("en-GB").getOrThrow() + + assertTrue(locale1 == locale2) + assertFalse(locale1 == locale3) + } + + @Test + fun testLocaleHashCode() { + val locale1 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + val locale2 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + + assertEquals(locale1.hashCode(), locale2.hashCode()) + } + + @Test + fun testLocaleToString() { + val locale = KurrencyLocale.US + val string = locale.toString() + assertTrue(string.contains("KurrencyLocale")) + assertTrue(string.contains(locale.languageTag.replace("_", "-"))) + } +} diff --git a/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/LocaleFormattingTestInstrumented.kt b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/LocaleFormattingTestInstrumented.kt new file mode 100644 index 0000000..c63a6ec --- /dev/null +++ b/kurrency-core/src/androidTest/kotlin/org/kimplify/kurrency/LocaleFormattingTestInstrumented.kt @@ -0,0 +1,155 @@ +package org.kimplify.kurrency +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class LocaleFormattingTestInstrumented { + + @Test + fun testFormatting_withDifferentLocales() { + val amount = "1234.56" + val currencyCode = "USD" + + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.UK, + KurrencyLocale.GERMANY, + KurrencyLocale.FRANCE, + KurrencyLocale.JAPAN + ) + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.formatCurrencyStyle(amount, currencyCode) + + assertTrue( + result.isSuccess, + "Formatting should succeed for locale ${locale.languageTag}" + ) + + val formatted = result.getOrNull() + assertNotNull(formatted, "Formatted value should not be null for ${locale.languageTag}") + assertTrue(formatted.isNotBlank(), "Formatted value should not be blank") + } + } + + @Test + fun testFormatting_euroWithDifferentLocales() { + val amount = "1234.56" + val currencyCode = "EUR" + + val locales = listOf( + KurrencyLocale.GERMANY, + KurrencyLocale.FRANCE, + KurrencyLocale.ITALY, + KurrencyLocale.SPAIN + ) + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.formatCurrencyStyle(amount, currencyCode) + + assertTrue( + result.isSuccess, + "EUR formatting should succeed for locale ${locale.languageTag}" + ) + + val formatted = result.getOrNull() + assertNotNull(formatted) + assertTrue(formatted.contains("1") || formatted.contains("2")) + } + } + + @Test + fun testIsoFormatting_withDifferentLocales() { + val amount = "1234.56" + val currencyCode = "USD" + + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.JAPAN, + KurrencyLocale.GERMANY + ) + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.formatIsoCurrencyStyle(amount, currencyCode) + + assertTrue( + result.isSuccess, + "ISO formatting should succeed for locale ${locale.languageTag}" + ) + + val formatted = result.getOrNull() + assertNotNull(formatted) + // ISO format should include the currency code + assertTrue( + formatted.contains("USD") || formatted.contains("usd"), + "ISO format should contain currency code" + ) + } + } + + @Test + fun testFractionDigits_consistentAcrossLocales() { + val currencyCode = "USD" + + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.UK, + KurrencyLocale.GERMANY, + KurrencyLocale.JAPAN + ) + + val fractionDigits = mutableSetOf() + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.getFractionDigits(currencyCode) + + assertTrue(result.isSuccess, "Should get fraction digits for ${locale.languageTag}") + result.getOrNull()?.let { fractionDigits.add(it) } + } + + // USD should have consistent fraction digits (2) across all locales + assertTrue( + fractionDigits.size == 1, + "USD should have consistent fraction digits across locales" + ) + assertTrue( + fractionDigits.first() == 2, + "USD should have 2 fraction digits" + ) + } + + @Test + fun testFactoryMethod_createWithLocale() { + val formatter = CurrencyFormatter.create(KurrencyLocale.GERMANY) + val result = formatter.formatCurrencyStyle("100.50", "EUR") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFactoryMethod_createWithSystemLocale() { + val formatter = CurrencyFormatter.createWithSystemLocale() + val result = formatter.formatCurrencyStyle("100.50", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFactoryMethod_createWithNullLocale() { + val formatter = CurrencyFormatter.create(KurrencyLocale.systemLocale()) + val result = formatter.formatCurrencyStyle("100.50", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } +} diff --git a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/ComposeLocaleExtensions.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/ComposeLocaleExtensions.kt new file mode 100644 index 0000000..46493c1 --- /dev/null +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/ComposeLocaleExtensions.kt @@ -0,0 +1,46 @@ +package org.kimplify.kurrency + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.intl.Locale + +/** + * Creates a [KurrencyLocale] from a Jetpack Compose Multiplatform [Locale]. + * + * @param composeLocale The Compose locale to convert + * @return Result with KurrencyLocale on success, or failure if the conversion fails + */ +fun KurrencyLocale.Companion.fromComposeLocale(composeLocale: Locale): Result { + return fromLanguageTag(composeLocale.toLanguageTag()) +} + +/** + * Creates a [KurrencyLocale] from the current Compose system locale. + * + * This is a Composable function that will recompose when the system locale changes. + * + * @return KurrencyLocale representing the current system locale + */ +@Composable +fun KurrencyLocale.Companion.current(): KurrencyLocale { + val composeLocale = Locale.current + return remember(composeLocale) { + fromComposeLocale(composeLocale).getOrElse { + // Fallback to system locale if conversion fails + systemLocale() + } + } +} + +/** + * Extension function to convert a Compose Locale to a KurrencyLocale. + * + * Usage: + * ``` + * val composeLocale = Locale.current + * val kurrencyLocale = composeLocale.toKurrencyLocale().getOrNull() + * ``` + */ +fun Locale.toKurrencyLocale(): Result { + return KurrencyLocale.fromLanguageTag(this.toLanguageTag()) +} diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/Currency.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/Currency.kt similarity index 96% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/Currency.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/Currency.kt index 712b2c7..e66afd8 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/Currency.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/Currency.kt @@ -1,6 +1,6 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency -import com.chilinoodles.cedar.logging.Cedar +import org.kimplify.cedar.logging.Cedar import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyFormat.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormat.kt similarity index 88% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyFormat.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormat.kt index b84142a..b4b0117 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyFormat.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormat.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency interface CurrencyFormat { fun getFractionDigits(currencyCode: String): Result diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatter.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt similarity index 61% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatter.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt index caa2a87..6340643 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatter.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyFormatter.kt @@ -1,9 +1,9 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency -import com.chilinoodles.cedar.logging.Cedar -import com.chilinoodles.kurrency.extensions.replaceCommaWithDot +import org.kimplify.cedar.logging.Cedar +import org.kimplify.kurrency.extensions.replaceCommaWithDot -expect class CurrencyFormatterImpl() : CurrencyFormat { +expect class CurrencyFormatterImpl(kurrencyLocale: KurrencyLocale = KurrencyLocale.systemLocale()) : CurrencyFormat { override fun getFractionDigits(currencyCode: String): Result override fun formatCurrencyStyle(amount: String, currencyCode: String): Result override fun formatIsoCurrencyStyle(amount: String, currencyCode: String): Result @@ -13,39 +13,60 @@ expect fun isValidCurrency(currencyCode: String): Boolean object CurrencyFormatter { private const val DEFAULT_FRACTION_DIGITS = 2 - - private val formatter: CurrencyFormat by lazy { - Cedar.tag("Kurrency").d("Initializing CurrencyFormatter") - CurrencyFormatterImpl() + + private val defaultFormatter: CurrencyFormat by lazy { + Cedar.tag("Kurrency").d("Initializing default CurrencyFormatter") + CurrencyFormatterImpl() } - + + /** + * Creates a new CurrencyFormat instance with the specified locale. + * + * @param locale The locale to use for formatting. If null, uses the system default locale. + * @return A new CurrencyFormat instance + */ + fun create(locale: KurrencyLocale): CurrencyFormat { + Cedar.tag("Kurrency").d("Creating CurrencyFormatter with locale: ${locale?.languageTag ?: "system default"}") + return CurrencyFormatterImpl(locale) + } + + /** + * Creates a new CurrencyFormat instance with the system's default locale. + * + * @return A new CurrencyFormat instance using system locale + */ + fun createWithSystemLocale(): CurrencyFormat { + Cedar.tag("Kurrency").d("Creating CurrencyFormatter with system locale") + return CurrencyFormatterImpl(KurrencyLocale.systemLocale()) + } + fun getFractionDigits(currencyCode: String): Result { if (!isValidCurrencyCode(currencyCode)) { val error = KurrencyError.InvalidCurrencyCode(currencyCode) Cedar.tag("Kurrency").w(error.errorMessage) return Result.failure(error) } - + Cedar.tag("Kurrency").d("Getting fraction digits for: $currencyCode") - return formatter.getFractionDigits(currencyCode) + return defaultFormatter.getFractionDigits(currencyCode) .onFailure { throwable -> val error = KurrencyError.FractionDigitsFailure(currencyCode, throwable) Cedar.tag("Kurrency").e(throwable, error.errorMessage) } } - + fun getFractionDigitsOrDefault(currencyCode: String): Int = getFractionDigits(currencyCode).getOrDefault(DEFAULT_FRACTION_DIGITS) - + fun formatCurrencyStyle(amount: String, currencyCode: String): Result { - return formatWithValidation(amount, currencyCode) { - formatter.formatCurrencyStyle(it, currencyCode) + return formatWithValidation(amount, currencyCode) { + defaultFormatter.formatCurrencyStyle(it, currencyCode) } } - + fun formatIsoCurrencyStyle(amount: String, currencyCode: String): Result { - return formatWithValidation(amount, currencyCode) { - formatter.formatIsoCurrencyStyle(it, currencyCode) + return formatWithValidation(amount, currencyCode) { + defaultFormatter.formatIsoCurrencyStyle(it, currencyCode) } } diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyMetadata.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyMetadata.kt similarity index 98% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyMetadata.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyMetadata.kt index a9a9e14..d5de8e5 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyMetadata.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyMetadata.kt @@ -1,6 +1,6 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency -import com.chilinoodles.cedar.logging.Cedar +import org.kimplify.cedar.logging.Cedar enum class CurrencyMetadata( val code: String, diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyState.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyState.kt similarity index 96% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyState.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyState.kt index 2d5616e..ed93477 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/CurrencyState.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/CurrencyState.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -6,7 +6,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.chilinoodles.cedar.logging.Cedar +import org.kimplify.cedar.logging.Cedar import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/KurrencyError.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/KurrencyError.kt similarity index 95% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/KurrencyError.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/KurrencyError.kt index 0803d8e..993e03d 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/KurrencyError.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/KurrencyError.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency sealed class KurrencyError( val errorMessage: String, diff --git a/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt new file mode 100644 index 0000000..c8c1025 --- /dev/null +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt @@ -0,0 +1,75 @@ +package org.kimplify.kurrency + +/** + * Represents a locale for currency formatting across all platforms. + * + * This class wraps platform-specific locale implementations and provides + * a consistent API for locale handling in Kurrency. + * + * @property languageTag The BCP 47 language tag (e.g., "en-US", "de-DE", "ja-JP") + */ +expect class KurrencyLocale { + val languageTag: String + + companion object { + /** + * Creates a KurrencyLocale from a BCP 47 language tag. + * + * @param languageTag The language tag string (e.g., "en-US", "fr-FR") + * @return Result with KurrencyLocale on success, or failure if the tag is invalid + */ + fun fromLanguageTag(languageTag: String): Result + + /** + * Returns the system's current locale. + */ + fun systemLocale(): KurrencyLocale + + // Common predefined locales for convenience + + /** United States English (en-US) */ + val US: KurrencyLocale + + /** United Kingdom English (en-GB) */ + val UK: KurrencyLocale + + /** Canadian English (en-CA) */ + val CANADA: KurrencyLocale + + /** Canadian French (fr-CA) */ + val CANADA_FRENCH: KurrencyLocale + + /** German (Germany) (de-DE) */ + val GERMANY: KurrencyLocale + + /** French (France) (fr-FR) */ + val FRANCE: KurrencyLocale + + /** Italian (Italy) (it-IT) */ + val ITALY: KurrencyLocale + + /** Spanish (Spain) (es-ES) */ + val SPAIN: KurrencyLocale + + /** Japanese (Japan) (ja-JP) */ + val JAPAN: KurrencyLocale + + /** Chinese Simplified (China) (zh-CN) */ + val CHINA: KurrencyLocale + + /** Korean (South Korea) (ko-KR) */ + val KOREA: KurrencyLocale + + /** Portuguese (Brazil) (pt-BR) */ + val BRAZIL: KurrencyLocale + + /** Russian (Russia) (ru-RU) */ + val RUSSIA: KurrencyLocale + + /** Arabic (Saudi Arabia) (ar-SA) */ + val SAUDI_ARABIA: KurrencyLocale + + /** Hindi (India) (hi-IN) */ + val INDIA: KurrencyLocale + } +} diff --git a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/extensions/StringExtensions.kt b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/extensions/StringExtensions.kt similarity index 63% rename from kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/extensions/StringExtensions.kt rename to kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/extensions/StringExtensions.kt index af2e112..01ef424 100644 --- a/kurrency/src/commonMain/kotlin/com/chilinoodles/kurrency/extensions/StringExtensions.kt +++ b/kurrency-core/src/commonMain/kotlin/org/kimplify/kurrency/extensions/StringExtensions.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency.extensions +package org.kimplify.kurrency.extensions internal fun String.replaceCommaWithDot(): String = this.replace(',', '.') diff --git a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyFormatterTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyFormatterTest.kt similarity index 99% rename from kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyFormatterTest.kt rename to kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyFormatterTest.kt index 968e8d2..a971bd0 100644 --- a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyFormatterTest.kt +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyFormatterTest.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import kotlin.test.Test import kotlin.test.assertEquals diff --git a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyMetadataTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTest.kt similarity index 99% rename from kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyMetadataTest.kt rename to kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTest.kt index 12a2388..6bd4db6 100644 --- a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyMetadataTest.kt +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyMetadataTest.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import kotlin.test.Test import kotlin.test.assertEquals diff --git a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyStateTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyStateTest.kt similarity index 99% rename from kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyStateTest.kt rename to kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyStateTest.kt index 54cdd29..7766e32 100644 --- a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyStateTest.kt +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyStateTest.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import kotlin.test.Test import kotlin.test.assertEquals diff --git a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyTest.kt similarity index 99% rename from kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyTest.kt rename to kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyTest.kt index f1a70c0..d55e6b9 100644 --- a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/CurrencyTest.kt +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/CurrencyTest.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import kotlin.test.Test import kotlin.test.assertEquals diff --git a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/KurrencyErrorTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/KurrencyErrorTest.kt similarity index 99% rename from kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/KurrencyErrorTest.kt rename to kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/KurrencyErrorTest.kt index 4983426..6f342ff 100644 --- a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/KurrencyErrorTest.kt +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/KurrencyErrorTest.kt @@ -1,4 +1,4 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency import kotlin.test.Test import kotlin.test.assertEquals diff --git a/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTest.kt new file mode 100644 index 0000000..bfa2394 --- /dev/null +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/KurrencyLocaleTest.kt @@ -0,0 +1,108 @@ +package org.kimplify.kurrency + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class KurrencyLocaleTest { + + @Test + fun testFromLanguageTag_validTags() { + val validTags = listOf( + "en-US", + "en-GB", + "fr-FR", + "de-DE", + "ja-JP", + "zh-CN", + "ar-SA", + "pt-BR", + "es-ES" + ) + + validTags.forEach { tag -> + val result = KurrencyLocale.fromLanguageTag(tag) + assertTrue(result.isSuccess, "Expected $tag to be valid") + assertEquals(tag, result.getOrNull()?.languageTag?.replace("_", "-")) + } + } + + @Test + fun testFromLanguageTag_invalidTags() { + val invalidTags = listOf( + "", + " ", + "invalid", + "123", + "en_", + "_US" + ) + + invalidTags.forEach { tag -> + val result = KurrencyLocale.fromLanguageTag(tag) + assertTrue(result.isFailure, "Expected '$tag' to be invalid") + } + } + + @Test + fun testSystemLocale() { + val systemLocale = KurrencyLocale.systemLocale() + assertNotNull(systemLocale) + assertTrue(systemLocale.languageTag.isNotBlank()) + } + + @Test + fun testPredefinedLocales() { + // Test that all predefined locales are valid + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.UK, + KurrencyLocale.CANADA, + KurrencyLocale.CANADA_FRENCH, + KurrencyLocale.GERMANY, + KurrencyLocale.FRANCE, + KurrencyLocale.ITALY, + KurrencyLocale.SPAIN, + KurrencyLocale.JAPAN, + KurrencyLocale.CHINA, + KurrencyLocale.KOREA, + KurrencyLocale.BRAZIL, + KurrencyLocale.RUSSIA, + KurrencyLocale.SAUDI_ARABIA, + KurrencyLocale.INDIA + ) + + locales.forEach { locale -> + assertNotNull(locale) + assertTrue(locale.languageTag.isNotBlank()) + } + } + + @Test + fun testLocaleEquality() { + val locale1 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + val locale2 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + val locale3 = KurrencyLocale.fromLanguageTag("en-GB").getOrThrow() + + assertTrue(locale1 == locale2) + assertFalse(locale1 == locale3) + } + + @Test + fun testLocaleHashCode() { + val locale1 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + val locale2 = KurrencyLocale.fromLanguageTag("en-US").getOrThrow() + + assertEquals(locale1.hashCode(), locale2.hashCode()) + } + + @Test + fun testLocaleToString() { + val locale = KurrencyLocale.US + val string = locale.toString() + assertTrue(string.contains("KurrencyLocale")) + assertTrue(string.contains(locale.languageTag.replace("_", "-"))) + } +} diff --git a/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/LocaleFormattingTest.kt b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/LocaleFormattingTest.kt new file mode 100644 index 0000000..ca1bbcf --- /dev/null +++ b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/LocaleFormattingTest.kt @@ -0,0 +1,152 @@ +package org.kimplify.kurrency + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class LocaleFormattingTest { + + @Test + fun testFormatting_withDifferentLocales() { + val amount = "1234.56" + val currencyCode = "USD" + + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.UK, + KurrencyLocale.GERMANY, + KurrencyLocale.FRANCE, + KurrencyLocale.JAPAN + ) + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.formatCurrencyStyle(amount, currencyCode) + + assertTrue( + result.isSuccess, + "Formatting should succeed for locale ${locale.languageTag}" + ) + + val formatted = result.getOrNull() + assertNotNull(formatted, "Formatted value should not be null for ${locale.languageTag}") + assertTrue(formatted.isNotBlank(), "Formatted value should not be blank") + } + } + + @Test + fun testFormatting_euroWithDifferentLocales() { + val amount = "1234.56" + val currencyCode = "EUR" + + val locales = listOf( + KurrencyLocale.GERMANY, + KurrencyLocale.FRANCE, + KurrencyLocale.ITALY, + KurrencyLocale.SPAIN + ) + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.formatCurrencyStyle(amount, currencyCode) + + assertTrue( + result.isSuccess, + "EUR formatting should succeed for locale ${locale.languageTag}" + ) + + val formatted = result.getOrNull() + assertNotNull(formatted) + assertTrue(formatted.contains("1") || formatted.contains("2")) + } + } + + @Test + fun testIsoFormatting_withDifferentLocales() { + val amount = "1234.56" + val currencyCode = "USD" + + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.JAPAN, + KurrencyLocale.GERMANY + ) + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.formatIsoCurrencyStyle(amount, currencyCode) + + assertTrue( + result.isSuccess, + "ISO formatting should succeed for locale ${locale.languageTag}" + ) + + val formatted = result.getOrNull() + assertNotNull(formatted) + // ISO format should include the currency code + assertTrue( + formatted.contains("USD") || formatted.contains("usd"), + "ISO format should contain currency code" + ) + } + } + + @Test + fun testFractionDigits_consistentAcrossLocales() { + val currencyCode = "USD" + + val locales = listOf( + KurrencyLocale.US, + KurrencyLocale.UK, + KurrencyLocale.GERMANY, + KurrencyLocale.JAPAN + ) + + val fractionDigits = mutableSetOf() + + locales.forEach { locale -> + val formatter = CurrencyFormatterImpl(locale) + val result = formatter.getFractionDigits(currencyCode) + + assertTrue(result.isSuccess, "Should get fraction digits for ${locale.languageTag}") + result.getOrNull()?.let { fractionDigits.add(it) } + } + + // USD should have consistent fraction digits (2) across all locales + assertTrue( + fractionDigits.size == 1, + "USD should have consistent fraction digits across locales" + ) + assertTrue( + fractionDigits.first() == 2, + "USD should have 2 fraction digits" + ) + } + + @Test + fun testFactoryMethod_createWithLocale() { + val formatter = CurrencyFormatter.create(KurrencyLocale.GERMANY) + val result = formatter.formatCurrencyStyle("100.50", "EUR") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFactoryMethod_createWithSystemLocale() { + val formatter = CurrencyFormatter.createWithSystemLocale() + val result = formatter.formatCurrencyStyle("100.50", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } + + @Test + fun testFactoryMethod_createWithNullLocale() { + val formatter = CurrencyFormatter.create(KurrencyLocale.systemLocale()) + val result = formatter.formatCurrencyStyle("100.50", "USD") + + assertTrue(result.isSuccess) + assertNotNull(result.getOrNull()) + } +} diff --git a/kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/README_TESTS.md b/kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/README_TESTS.md similarity index 100% rename from kurrency/src/commonTest/kotlin/com/chilinoodles/kurrency/README_TESTS.md rename to kurrency-core/src/commonTest/kotlin/org/kimplify/kurrency/README_TESTS.md diff --git a/kurrency/src/iosMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt b/kurrency-core/src/iosMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt similarity index 65% rename from kurrency/src/iosMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt rename to kurrency-core/src/iosMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt index a3d7836..ebad9e8 100644 --- a/kurrency/src/iosMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt +++ b/kurrency-core/src/iosMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt @@ -1,16 +1,21 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency -import com.chilinoodles.kurrency.extensions.replaceCommaWithDot +import org.kimplify.kurrency.extensions.replaceCommaWithDot import platform.Foundation.NSLocale import platform.Foundation.NSNumber import platform.Foundation.NSNumberFormatter import platform.Foundation.NSNumberFormatterCurrencyISOCodeStyle import platform.Foundation.NSNumberFormatterCurrencyStyle import platform.Foundation.NSNumberFormatterStyle +import platform.Foundation.commonISOCurrencyCodes import platform.Foundation.currentLocale -actual class CurrencyFormatterImpl : CurrencyFormat { - +actual class CurrencyFormatterImpl actual constructor( + kurrencyLocale: KurrencyLocale +) : CurrencyFormat { + + private val locale: NSLocale = kurrencyLocale?.nsLocale ?: NSLocale.currentLocale + actual override fun getFractionDigits(currencyCode: String): Result { return runCatching { val formatter = NSNumberFormatter().apply { @@ -49,27 +54,13 @@ actual class CurrencyFormatterImpl : CurrencyFormat { style: NSNumberFormatterStyle ): NSNumberFormatter = NSNumberFormatter().apply { this.numberStyle = style - this.locale = NSLocale.currentLocale + this.locale = this@CurrencyFormatterImpl.locale this.currencyCode = currencyCode } } -actual fun isValidCurrency(currencyCode: String): Boolean = - runCatching { - val upperCode = currencyCode.uppercase() - val formatter = NSNumberFormatter().apply { - this.currencyCode = upperCode - this.numberStyle = NSNumberFormatterCurrencyStyle - this.locale = NSLocale.currentLocale - } - val formatted = formatter.stringFromNumber(NSNumber(1.0)) - if (formatted == null) { - return false - } - val hasNumber = formatted.contains("1") || formatted.matches(Regex(".*[0-9].*")) - val notJustCode = formatted != upperCode && - !formatted.equals(upperCode + " 1", ignoreCase = true) && - !formatted.equals("1 " + upperCode, ignoreCase = true) - hasNumber && notJustCode - }.getOrDefault(false) +actual fun isValidCurrency(currencyCode: String): Boolean { + val upperCode = currencyCode.uppercase() + return NSLocale.commonISOCurrencyCodes.contains(upperCode) +} diff --git a/kurrency-core/src/iosMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt b/kurrency-core/src/iosMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt new file mode 100644 index 0000000..f754f49 --- /dev/null +++ b/kurrency-core/src/iosMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt @@ -0,0 +1,72 @@ +package org.kimplify.kurrency + +import platform.Foundation.NSLocale +import platform.Foundation.currentLocale +import platform.Foundation.localeIdentifier + +/** + * iOS implementation of KurrencyLocale using NSLocale. + */ +actual class KurrencyLocale internal constructor(internal val nsLocale: NSLocale) { + actual val languageTag: String + get() = nsLocale.localeIdentifier.replace("_", "-") + + actual companion object { + actual fun fromLanguageTag(languageTag: String): Result { + return try { + // Validate format before creating locale + if (languageTag.isBlank()) { + return Result.failure(IllegalArgumentException("Language tag cannot be blank")) + } + + // Basic BCP 47 validation + val bcp47Pattern = Regex( + "^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?(-[0-9A-Za-z]+)*$", + RegexOption.IGNORE_CASE + ) + + if (!bcp47Pattern.matches(languageTag)) { + return Result.failure(IllegalArgumentException("Invalid language tag format: $languageTag")) + } + + // NSLocale uses underscores, so convert hyphens to underscores + val localeIdentifier = languageTag.replace("-", "_") + val locale = NSLocale(localeIdentifier = localeIdentifier) + Result.success(KurrencyLocale(locale)) + } catch (e: Exception) { + Result.failure(e) + } + } + + actual fun systemLocale(): KurrencyLocale { + return KurrencyLocale(NSLocale.currentLocale) + } + + // Predefined locales + actual val US: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "en_US")) + actual val UK: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "en_GB")) + actual val CANADA: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "en_CA")) + actual val CANADA_FRENCH: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "fr_CA")) + actual val GERMANY: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "de_DE")) + actual val FRANCE: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "fr_FR")) + actual val ITALY: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "it_IT")) + actual val SPAIN: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "es_ES")) + actual val JAPAN: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "ja_JP")) + actual val CHINA: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "zh_CN")) + actual val KOREA: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "ko_KR")) + actual val BRAZIL: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "pt_BR")) + actual val RUSSIA: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "ru_RU")) + actual val SAUDI_ARABIA: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "ar_SA")) + actual val INDIA: KurrencyLocale = KurrencyLocale(NSLocale(localeIdentifier = "hi_IN")) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KurrencyLocale) return false + return nsLocale.localeIdentifier == other.nsLocale.localeIdentifier + } + + override fun hashCode(): Int = nsLocale.localeIdentifier.hashCode() + + override fun toString(): String = "KurrencyLocale($languageTag)" +} diff --git a/kurrency/src/jvmMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt b/kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt similarity index 89% rename from kurrency/src/jvmMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt rename to kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt index 9b3e697..931248e 100644 --- a/kurrency/src/jvmMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt +++ b/kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt @@ -1,11 +1,15 @@ -package com.chilinoodles.kurrency +package org.kimplify.kurrency -import com.chilinoodles.kurrency.extensions.replaceCommaWithDot +import org.kimplify.kurrency.extensions.replaceCommaWithDot import java.text.NumberFormat import java.util.Currency import java.util.Locale -actual class CurrencyFormatterImpl : CurrencyFormat { +actual class CurrencyFormatterImpl actual constructor( + kurrencyLocale: KurrencyLocale +) : CurrencyFormat { + + private val locale: Locale = kurrencyLocale.locale actual override fun getFractionDigits(currencyCode: String): Result { return runCatching { @@ -27,7 +31,6 @@ actual class CurrencyFormatterImpl : CurrencyFormat { useIsoCode: Boolean ): Result { return runCatching { - val locale = Locale.getDefault() val value = amount.replaceCommaWithDot().toDouble() require(value.isFinite()) { "Amount must be a finite number" } @@ -59,4 +62,3 @@ actual fun isValidCurrency(currencyCode: String): Boolean = val currency = Currency.getInstance(currencyCode.uppercase()) currency != null }.getOrDefault(false) - diff --git a/kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt b/kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt new file mode 100644 index 0000000..e17e8b3 --- /dev/null +++ b/kurrency-core/src/jvmMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt @@ -0,0 +1,68 @@ +package org.kimplify.kurrency + +import java.util.Locale + +/** + * JVM implementation of KurrencyLocale using java.util.Locale. + */ +actual class KurrencyLocale internal constructor(internal val locale: Locale) { + actual val languageTag: String + get() = locale.toLanguageTag() + + actual companion object { + actual fun fromLanguageTag(languageTag: String): Result { + return try { + // Validate format before creating locale + if (languageTag.isBlank()) { + return Result.failure(IllegalArgumentException("Language tag cannot be blank")) + } + + // Basic BCP 47 validation + val bcp47Pattern = Regex( + "^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?(-[0-9A-Za-z]+)*$", + RegexOption.IGNORE_CASE + ) + + if (!bcp47Pattern.matches(languageTag)) { + return Result.failure(IllegalArgumentException("Invalid language tag format: $languageTag")) + } + + val locale = Locale.forLanguageTag(languageTag) + Result.success(KurrencyLocale(locale)) + } catch (e: Exception) { + Result.failure(e) + } + } + + actual fun systemLocale(): KurrencyLocale { + return KurrencyLocale(Locale.getDefault()) + } + + // Predefined locales + actual val US: KurrencyLocale = KurrencyLocale(Locale.US) + actual val UK: KurrencyLocale = KurrencyLocale(Locale.UK) + actual val CANADA: KurrencyLocale = KurrencyLocale(Locale.CANADA) + actual val CANADA_FRENCH: KurrencyLocale = KurrencyLocale(Locale.CANADA_FRENCH) + actual val GERMANY: KurrencyLocale = KurrencyLocale(Locale.GERMANY) + actual val FRANCE: KurrencyLocale = KurrencyLocale(Locale.FRANCE) + actual val ITALY: KurrencyLocale = KurrencyLocale(Locale.ITALY) + actual val SPAIN: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("es-ES")) + actual val JAPAN: KurrencyLocale = KurrencyLocale(Locale.JAPAN) + actual val CHINA: KurrencyLocale = KurrencyLocale(Locale.CHINA) + actual val KOREA: KurrencyLocale = KurrencyLocale(Locale.KOREA) + actual val BRAZIL: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("pt-BR")) + actual val RUSSIA: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("ru-RU")) + actual val SAUDI_ARABIA: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("ar-SA")) + actual val INDIA: KurrencyLocale = KurrencyLocale(Locale.forLanguageTag("hi-IN")) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KurrencyLocale) return false + return locale == other.locale + } + + override fun hashCode(): Int = locale.hashCode() + + override fun toString(): String = "KurrencyLocale($languageTag)" +} diff --git a/kurrency/src/webMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt b/kurrency-core/src/webMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt similarity index 60% rename from kurrency/src/webMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt rename to kurrency-core/src/webMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt index 6cfc4f7..0d28d76 100644 --- a/kurrency/src/webMain/kotlin/com/chilinoodles/kurrency/CurrencyFormatterImpl.kt +++ b/kurrency-core/src/webMain/kotlin/org/kimplify/kurrency/CurrencyFormatterImpl.kt @@ -1,21 +1,21 @@ @file:OptIn(ExperimentalWasmJsInterop::class) -package com.chilinoodles.kurrency +package org.kimplify.kurrency -import com.chilinoodles.kurrency.extensions.replaceCommaWithDot +import org.kimplify.kurrency.extensions.replaceCommaWithDot import kotlin.js.ExperimentalWasmJsInterop -@JsFun("function(cur) { return new Intl.NumberFormat(undefined, {style:'currency', currency:cur}).resolvedOptions().maximumFractionDigits }") -private external fun jsGetMaxFractionDigits(cur: String): Int +@JsFun("function(cur, loc) { return new Intl.NumberFormat(loc || undefined, {style:'currency', currency:cur}).resolvedOptions().maximumFractionDigits }") +private external fun jsGetMaxFractionDigits(cur: String, loc: String?): Int -@JsFun("function(cur) { return new Intl.NumberFormat(undefined, {style:'currency', currency:cur}).resolvedOptions().currency }") -private external fun jsGetResolvedCurrency(cur: String): String +@JsFun("function(cur, loc) { return new Intl.NumberFormat(loc || undefined, {style:'currency', currency:cur}).resolvedOptions().currency }") +private external fun jsGetResolvedCurrency(cur: String, loc: String?): String -@JsFun("function(amt, cur) { return new Intl.NumberFormat(undefined, {style:'currency', currency:cur}).format(+amt) }") -private external fun jsFormatSymbol(amt: String, cur: String): String +@JsFun("function(amt, cur, loc) { return new Intl.NumberFormat(loc || undefined, {style:'currency', currency:cur}).format(+amt) }") +private external fun jsFormatSymbol(amt: String, cur: String, loc: String?): String -@JsFun("function(amt, cur) { return new Intl.NumberFormat(undefined, {style:'currency', currency:cur, currencyDisplay:'code'}).format(+amt) }") -private external fun jsFormatIso(amt: String, cur: String): String +@JsFun("function(amt, cur, loc) { return new Intl.NumberFormat(loc || undefined, {style:'currency', currency:cur, currencyDisplay:'code'}).format(+amt) }") +private external fun jsFormatIso(amt: String, cur: String, loc: String?): String @JsFun("function(cur) { try { if (typeof Intl.supportedValuesOf === 'function') { return Intl.supportedValuesOf('currency').includes(cur); } return null; } catch(e) { return null; } }") private external fun jsIsSupportedCurrency(cur: String): Boolean? @@ -23,16 +23,20 @@ private external fun jsIsSupportedCurrency(cur: String): Boolean? @JsFun("function(cur) { try { new Intl.NumberFormat(undefined, {style:'currency', currency:cur}); return true; } catch(e) { return false; } }") private external fun jsCanCreateCurrencyFormatter(cur: String): Boolean -actual class CurrencyFormatterImpl : CurrencyFormat { +actual class CurrencyFormatterImpl actual constructor( + kurrencyLocale: KurrencyLocale +) : CurrencyFormat { + + private val locale: String? = kurrencyLocale?.languageTag actual override fun getFractionDigits(currencyCode: String): Result { return runCatching { val upperCode = currencyCode.uppercase() - val resolvedCurrency = jsGetResolvedCurrency(upperCode) - require(resolvedCurrency == upperCode) { - "Invalid currency code: $currencyCode (resolved to: $resolvedCurrency)" + val resolvedCurrency = jsGetResolvedCurrency(upperCode, locale) + require(resolvedCurrency == upperCode) { + "Invalid currency code: $currencyCode (resolved to: $resolvedCurrency)" } - val fractionDigits = jsGetMaxFractionDigits(upperCode) + val fractionDigits = jsGetMaxFractionDigits(upperCode, locale) require(fractionDigits >= 0) { "Invalid fraction digits: $fractionDigits" } fractionDigits } @@ -43,7 +47,7 @@ actual class CurrencyFormatterImpl : CurrencyFormat { val normalizedAmount = amount.replaceCommaWithDot() val doubleValue = normalizedAmount.toDouble() require(doubleValue.isFinite()) { "Amount must be a finite number" } - jsFormatSymbol(normalizedAmount, currencyCode) + jsFormatSymbol(normalizedAmount, currencyCode, locale) } } @@ -55,7 +59,7 @@ actual class CurrencyFormatterImpl : CurrencyFormat { val normalizedAmount = amount.replaceCommaWithDot() val doubleValue = normalizedAmount.toDouble() require(doubleValue.isFinite()) { "Amount must be a finite number" } - jsFormatIso(normalizedAmount, currencyCode) + jsFormatIso(normalizedAmount, currencyCode, locale) } } } @@ -73,7 +77,7 @@ actual fun isValidCurrency(currencyCode: String): Boolean = return false } - val resolvedCurrency = jsGetResolvedCurrency(upperCode) + val resolvedCurrency = jsGetResolvedCurrency(upperCode, null) resolvedCurrency == upperCode }.getOrDefault(false) diff --git a/kurrency-core/src/webMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt b/kurrency-core/src/webMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt new file mode 100644 index 0000000..ef9bf9f --- /dev/null +++ b/kurrency-core/src/webMain/kotlin/org/kimplify/kurrency/KurrencyLocale.kt @@ -0,0 +1,79 @@ +package org.kimplify.kurrency + +import kotlin.js.ExperimentalWasmJsInterop + +/** + * Gets the browser's default locale from navigator.language + */ +@OptIn(ExperimentalWasmJsInterop::class) +@JsFun("function() { return navigator.language || 'en-US'; }") +private external fun getBrowserLocale(): String + +/** + * Web (JS/WasmJs) implementation of KurrencyLocale using BCP 47 language tags. + */ +actual class KurrencyLocale internal constructor(actual val languageTag: String) { + + actual companion object { + actual fun fromLanguageTag(languageTag: String): Result { + return try { + // Basic validation for BCP 47 format + if (languageTag.isBlank()) { + Result.failure(IllegalArgumentException("Language tag cannot be blank")) + } else if (!isValidLanguageTag(languageTag)) { + Result.failure(IllegalArgumentException("Invalid language tag format: $languageTag")) + } else { + Result.success(KurrencyLocale(languageTag)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + actual fun systemLocale(): KurrencyLocale { + // Get browser's default locale + val browserLocale = getBrowserLocale() + return KurrencyLocale(browserLocale) + } + + // Predefined locales + actual val US: KurrencyLocale = KurrencyLocale("en-US") + actual val UK: KurrencyLocale = KurrencyLocale("en-GB") + actual val CANADA: KurrencyLocale = KurrencyLocale("en-CA") + actual val CANADA_FRENCH: KurrencyLocale = KurrencyLocale("fr-CA") + actual val GERMANY: KurrencyLocale = KurrencyLocale("de-DE") + actual val FRANCE: KurrencyLocale = KurrencyLocale("fr-FR") + actual val ITALY: KurrencyLocale = KurrencyLocale("it-IT") + actual val SPAIN: KurrencyLocale = KurrencyLocale("es-ES") + actual val JAPAN: KurrencyLocale = KurrencyLocale("ja-JP") + actual val CHINA: KurrencyLocale = KurrencyLocale("zh-CN") + actual val KOREA: KurrencyLocale = KurrencyLocale("ko-KR") + actual val BRAZIL: KurrencyLocale = KurrencyLocale("pt-BR") + actual val RUSSIA: KurrencyLocale = KurrencyLocale("ru-RU") + actual val SAUDI_ARABIA: KurrencyLocale = KurrencyLocale("ar-SA") + actual val INDIA: KurrencyLocale = KurrencyLocale("hi-IN") + + /** + * Basic validation for BCP 47 language tags. + * Format: language[-script][-region][-variant] + */ + private fun isValidLanguageTag(tag: String): Boolean { + // Simple regex for BCP 47: language code (2-3 letters) optionally followed by subtags + val bcp47Pattern = Regex( + "^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?(-[0-9A-Za-z]+)*$", + RegexOption.IGNORE_CASE + ) + return bcp47Pattern.matches(tag) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KurrencyLocale) return false + return languageTag == other.languageTag + } + + override fun hashCode(): Int = languageTag.hashCode() + + override fun toString(): String = "KurrencyLocale($languageTag)" +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index b79456f..2e01674 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -20,7 +20,7 @@ kotlin { iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { - export(project(":kurrency")) + export(project(":kurrency-core")) baseName = "ComposeApp" isStatic = true } @@ -56,7 +56,7 @@ kotlin { implementation(compose.material3) implementation(compose.components.uiToolingPreview) implementation(kotlin("test")) - api(project(":kurrency")) + api(project(":kurrency-core")) } androidMain.dependencies { @@ -70,14 +70,14 @@ kotlin { } android { - namespace = "io.github.chilinoodles.sample" + namespace = "org.kimplify.sample" compileSdk = 36 defaultConfig { minSdk = 24 targetSdk = 36 - applicationId = "io.github.chilinoodles.sample" + applicationId = "org.kimplify.sample" versionCode = 1 versionName = "1.0.0" } @@ -85,11 +85,11 @@ android { compose.desktop { application { - mainClass = "io.github.chilinoodles.sample.MainKt" + mainClass = "org.kimplify.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "io.github.chilinoodles.sample" + packageName = "org.kimplify.sample" packageVersion = "1.0.0" } } @@ -98,7 +98,7 @@ compose.desktop { tasks.register("runJvm") { group = "application" description = "Runs the JVM MainKt" - mainClass.set("io.github.chilinoodles.sample.MainKt") + mainClass.set("org.kimplify.MainKt") classpath = kotlin.targets .getByName("jvm") .compilations diff --git a/sample/src/androidMain/AndroidManifest.xml b/sample/src/androidMain/AndroidManifest.xml index 26403a7..3576e93 100644 --- a/sample/src/androidMain/AndroidManifest.xml +++ b/sample/src/androidMain/AndroidManifest.xml @@ -10,7 +10,7 @@ android:theme="@android:style/Theme.Material.Light.NoActionBar"> + android:name="org.kimplify.sample.MainActivity"> @@ -19,4 +19,4 @@ - \ No newline at end of file + diff --git a/sample/src/androidMain/kotlin/io/github/chilinoodles/sample/MainActivity.kt b/sample/src/androidMain/kotlin/org/kimplify/sample/MainActivity.kt similarity index 86% rename from sample/src/androidMain/kotlin/io/github/chilinoodles/sample/MainActivity.kt rename to sample/src/androidMain/kotlin/org/kimplify/sample/MainActivity.kt index 0dcd9f3..5b3d29b 100644 --- a/sample/src/androidMain/kotlin/io/github/chilinoodles/sample/MainActivity.kt +++ b/sample/src/androidMain/kotlin/org/kimplify/sample/MainActivity.kt @@ -1,4 +1,4 @@ -package io.github.chilinoodles.sample +package org.kimplify.sample import android.os.Bundle import androidx.activity.ComponentActivity @@ -6,7 +6,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import io.chilinoodles.kurrency.sample.App +import org.kimplify.kurrency.sample.App class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -23,4 +23,4 @@ class MainActivity : ComponentActivity() { @Composable fun AppAndroidPreview() { App() -} \ No newline at end of file +} diff --git a/sample/src/commonMain/kotlin/com/chilinoodles/kurrency/sample/CurrencyListScreen.kt b/sample/src/commonMain/kotlin/com/chilinoodles/kurrency/sample/CurrencyListScreen.kt deleted file mode 100644 index a476083..0000000 --- a/sample/src/commonMain/kotlin/com/chilinoodles/kurrency/sample/CurrencyListScreen.kt +++ /dev/null @@ -1,367 +0,0 @@ -package com.chilinoodles.kurrency.sample - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.chilinoodles.kurrency.CurrencyMetadata -import com.chilinoodles.kurrency.rememberCurrencyState - -@Composable -fun CurrencyListScreen() { - var searchQuery by remember { mutableStateOf("") } - val allCurrencies = remember { CurrencyMetadata.getAll() } - val filteredCurrencies = remember(searchQuery) { - if (searchQuery.isBlank()) { - allCurrencies - } else { - val query = searchQuery.uppercase().trim() - allCurrencies.filter { currency -> - currency.code.contains(query, ignoreCase = true) || - currency.displayName.contains(query, ignoreCase = true) || - currency.countryIso.contains(query, ignoreCase = true) || - currency.symbol.contains(query, ignoreCase = true) - } - } - } - - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.surfaceContainerLowest - ) { - Column( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - HeaderSection() - - SearchBar( - query = searchQuery, - onQueryChange = { searchQuery = it }, - resultCount = filteredCurrencies.size, - totalCount = allCurrencies.size - ) - - CurrencyList( - currencies = filteredCurrencies, - searchQuery = searchQuery - ) - } - } -} - -@Composable -fun EmbeddedCurrencyList() { - var searchQuery by remember { mutableStateOf("") } - val allCurrencies = remember { CurrencyMetadata.getAll() } - val filteredCurrencies = remember(searchQuery) { - if (searchQuery.isBlank()) { - allCurrencies - } else { - val query = searchQuery.uppercase().trim() - allCurrencies.filter { currency -> - currency.code.contains(query, ignoreCase = true) || - currency.displayName.contains(query, ignoreCase = true) || - currency.countryIso.contains(query, ignoreCase = true) || - currency.symbol.contains(query, ignoreCase = true) - } - } - } - - Column( - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - SearchBar( - query = searchQuery, - onQueryChange = { searchQuery = it }, - resultCount = filteredCurrencies.size, - totalCount = allCurrencies.size - ) - - Box( - modifier = Modifier.height(400.dp) - ) { - CurrencyList( - currencies = filteredCurrencies, - searchQuery = searchQuery - ) - } - } -} - -@Composable -private fun HeaderSection() { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Currency Registry", - style = MaterialTheme.typography.displaySmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = "Browse ${CurrencyMetadata.getAll().size} supported currencies", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -} - -@Composable -private fun SearchBar( - query: String, - onQueryChange: (String) -> Unit, - resultCount: Int, - totalCount: Int -) { - val focusRequester = remember { FocusRequester() } - - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = RoundedCornerShape(20.dp) - ) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = query, - onValueChange = onQueryChange, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - label = { Text("Search currencies...") }, - placeholder = { Text("Try: USD, Dollar, $, or US") }, - shape = RoundedCornerShape(16.dp), - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { focusRequester.freeFocus() } - ), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant - ) - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (query.isBlank()) { - "Showing all $totalCount currencies" - } else { - "Found $resultCount ${if (resultCount == 1) "currency" else "currencies"}" - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - if (query.isNotBlank()) { - Text( - text = "Clear", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.clickable { onQueryChange("") } - ) - } - } - } -} - -@Composable -private fun CurrencyList( - currencies: List, - searchQuery: String -) { - if (currencies.isEmpty()) { - EmptyState(searchQuery = searchQuery) - } else { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(currencies) { currency -> - CurrencyCard(currency = currency) - } - } - } -} - -@Composable -private fun CurrencyCard(currency: CurrencyMetadata) { - val currencyState = rememberCurrencyState(currency.code, "1000.00") - - Row( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(16.dp) - ) - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Box( - modifier = Modifier - .size(56.dp) - .background( - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), - shape = RoundedCornerShape(16.dp) - ), - contentAlignment = Alignment.Center - ) { - Text( - text = currency.flag, - style = MaterialTheme.typography.displaySmall - ) - } - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = currency.code, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = currency.symbol, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Text( - text = currency.displayName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = currency.countryIso, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "β€’", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${currency.fractionDigits} ${if (currency.fractionDigits == 1) "decimal" else "decimals"}", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = currencyState.formattedAmount, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.End - ) - Text( - text = currencyState.formattedAmountIso, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.End - ) - } - } -} - -@Composable -private fun EmptyState(searchQuery: String) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(20.dp) - ) - .padding(40.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "πŸ”", - style = MaterialTheme.typography.displayMedium - ) - Text( - text = "No currencies found", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "Try searching for a different currency code, name, or symbol", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } -} - diff --git a/sample/src/commonMain/kotlin/com/chilinoodles/kurrency/sample/KurrencySample.kt b/sample/src/commonMain/kotlin/com/chilinoodles/kurrency/sample/KurrencySample.kt deleted file mode 100644 index 811d3ae..0000000 --- a/sample/src/commonMain/kotlin/com/chilinoodles/kurrency/sample/KurrencySample.kt +++ /dev/null @@ -1,451 +0,0 @@ -package com.chilinoodles.kurrency.sample - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.chilinoodles.kurrency.Currency -import com.chilinoodles.kurrency.CurrencyStyle -import com.chilinoodles.kurrency.rememberCurrencyState - -@Composable -fun KurrencySampleApp() { - MaterialTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.surfaceContainerLowest - ) { - Column( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .imePadding() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - HeaderSection() - CombinedExamplesCard() - CurrencyListSection() - } - } - } -} - -@Composable -private fun HeaderSection() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Kurrency", - style = MaterialTheme.typography.displayMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = "Kotlin Multiplatform Currency Library", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -} - -@Composable -private fun CombinedExamplesCard() { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp) - ) { - Column( - modifier = Modifier.padding(20.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - Text( - text = "Examples", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - - BasicFormattingSection() - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - CurrencyStateSection() - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - MultiCurrencySection() - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - InteractiveConverterSection() - } - } -} - -@Composable -private fun BasicFormattingSection() { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - text = "Basic Formatting", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - val currency = remember { Currency("USD") } - val standardFormatted by currency.format("1234.56") - val isoFormatted by currency.format("1234.56", CurrencyStyle.Iso) - - FormatExample( - label = "Standard Style", - value = standardFormatted - ) - FormatExample( - label = "ISO Style", - value = isoFormatted - ) - FormatExample( - label = "Fraction Digits", - value = "${currency.fractionDigits} digits" - ) - } -} - -@Composable -private fun CurrencyStateSection() { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - text = "State Management", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - val currencyState = rememberCurrencyState("EUR", "999.99") - - CompactDisplayBox( - label = "Formatted Amount", - value = currencyState.formattedAmount - ) - CompactDisplayBox( - label = "ISO Format", - value = currencyState.formattedAmountIso - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - ActionButton( - text = "Update Amount", - modifier = Modifier.weight(1f), - onClick = { currencyState.updateAmount("2500.00") } - ) - ActionButton( - text = "Change to GBP", - modifier = Modifier.weight(1f), - onClick = { currencyState.updateCurrency("GBP") } - ) - } - } -} - -@Composable -private fun MultiCurrencySection() { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - text = "Multiple Currencies", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - val baseAmount = "1000.00" - val currencies = listOf( - "USD" to "US Dollar", - "EUR" to "Euro", - "GBP" to "British Pound", - "JPY" to "Japanese Yen", - "CHF" to "Swiss Franc" - ) - - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - currencies.forEach { (code, name) -> - val state = rememberCurrencyState(code, baseAmount) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = code, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = name, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = state.formattedAmount, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary - ) - } - if (code != currencies.last().first) { - HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) - } - } - } - } -} - -@Composable -private fun InteractiveConverterSection() { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Text( - text = "Interactive Converter", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - var inputAmount by remember { mutableStateOf("100.00") } - var selectedCurrency by remember { mutableStateOf("USD") } - - val currencyState = rememberCurrencyState(selectedCurrency, inputAmount) - val availableCurrencies = listOf("USD", "EUR", "GBP", "JPY", "CAD", "AUD") - - OutlinedTextField( - value = inputAmount, - onValueChange = { newValue -> - inputAmount = newValue - currencyState.updateAmount(newValue) - }, - label = { Text("Amount") }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - textStyle = MaterialTheme.typography.bodyLarge - ) - - Text( - text = "Select Currency:", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - availableCurrencies.take(3).forEach { currency -> - CurrencyChip( - code = currency, - isSelected = selectedCurrency == currency, - onClick = { - selectedCurrency = currency - currencyState.updateCurrency(currency) - }, - modifier = Modifier.weight(1f) - ) - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - availableCurrencies.drop(3).forEach { currency -> - CurrencyChip( - code = currency, - isSelected = selectedCurrency == currency, - onClick = { - selectedCurrency = currency - currencyState.updateCurrency(currency) - }, - modifier = Modifier.weight(1f) - ) - } - } - - CompactResultBox( - label = "Standard Format", - value = currencyState.formattedAmount - ) - CompactResultBox( - label = "ISO Format", - value = currencyState.formattedAmountIso - ) - } -} - - - -@Composable -private fun FormatExample(label: String, value: String) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = value, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - } -} - -@Composable -private fun CompactDisplayBox(label: String, value: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f), - shape = RoundedCornerShape(12.dp) - ) - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = value, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } -} - -@Composable -private fun CompactResultBox(label: String, value: String) { - Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Text( - text = label, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.6f), - shape = RoundedCornerShape(16.dp) - ) - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = value, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - } - } -} - -@Composable -private fun ActionButton( - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - ElevatedButton( - onClick = onClick, - modifier = modifier, - shape = RoundedCornerShape(16.dp) - ) { - Text( - text = text, - style = MaterialTheme.typography.labelLarge - ) - } -} - -@Composable -private fun CurrencyChip( - code: String, - isSelected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - FilterChip( - selected = isSelected, - onClick = onClick, - label = { - Text( - text = code, - style = MaterialTheme.typography.labelLarge, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium - ) - }, - modifier = modifier, - shape = RoundedCornerShape(16.dp), - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, - selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer - ) - ) -} - -@Composable -private fun CurrencyListSection() { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = "Currency Registry & Search", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - EmbeddedCurrencyList() - } -} - diff --git a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/App.kt b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/App.kt similarity index 78% rename from sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/App.kt rename to sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/App.kt index bd0c7a2..00c9abf 100644 --- a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/App.kt +++ b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/App.kt @@ -1,4 +1,4 @@ -package io.chilinoodles.kurrency.sample +package org.kimplify.kurrency.sample import androidx.compose.runtime.* import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/CurrencyListScreen.kt b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/CurrencyListScreen.kt similarity index 98% rename from sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/CurrencyListScreen.kt rename to sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/CurrencyListScreen.kt index 0ee0248..a2fee24 100644 --- a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/CurrencyListScreen.kt +++ b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/CurrencyListScreen.kt @@ -1,4 +1,4 @@ -package io.chilinoodles.kurrency.sample +package org.kimplify.kurrency.sample import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -37,8 +37,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.chilinoodles.kurrency.CurrencyMetadata -import com.chilinoodles.kurrency.rememberCurrencyState +import org.kimplify.kurrency.CurrencyMetadata +import org.kimplify.kurrency.rememberCurrencyState @Composable fun CurrencyListScreen() { diff --git a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/KurrencySample.kt b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/KurrencySample.kt similarity index 98% rename from sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/KurrencySample.kt rename to sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/KurrencySample.kt index 1250faf..a317b32 100644 --- a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/KurrencySample.kt +++ b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/KurrencySample.kt @@ -1,4 +1,4 @@ -package io.chilinoodles.kurrency.sample +package org.kimplify.kurrency.sample import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -33,9 +33,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.chilinoodles.kurrency.Currency -import com.chilinoodles.kurrency.CurrencyStyle -import com.chilinoodles.kurrency.rememberCurrencyState +import org.kimplify.kurrency.Currency +import org.kimplify.kurrency.CurrencyStyle +import org.kimplify.kurrency.rememberCurrencyState @Composable fun KurrencySampleApp() { diff --git a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/QuickStartSample.kt b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/QuickStartSample.kt similarity index 96% rename from sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/QuickStartSample.kt rename to sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/QuickStartSample.kt index 3a11c7a..2fcbc48 100644 --- a/sample/src/commonMain/kotlin/io/chilinoodles/kurrency/sample/QuickStartSample.kt +++ b/sample/src/commonMain/kotlin/org/kimplify/kurrency/sample/QuickStartSample.kt @@ -1,4 +1,4 @@ -package io.chilinoodles.kurrency.sample +package org.kimplify.kurrency.sample import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.chilinoodles.kurrency.rememberCurrencyState +import org.kimplify.kurrency.rememberCurrencyState @Composable fun QuickStartSample() { diff --git a/sample/src/commonTest/kotlin/io/github/chilinoodles/ComposeAppCommonTest.kt b/sample/src/commonTest/kotlin/org/kimplify/ComposeAppCommonTest.kt similarity index 82% rename from sample/src/commonTest/kotlin/io/github/chilinoodles/ComposeAppCommonTest.kt rename to sample/src/commonTest/kotlin/org/kimplify/ComposeAppCommonTest.kt index 737da2d..4e25011 100644 --- a/sample/src/commonTest/kotlin/io/github/chilinoodles/ComposeAppCommonTest.kt +++ b/sample/src/commonTest/kotlin/org/kimplify/ComposeAppCommonTest.kt @@ -1,4 +1,4 @@ -package io.github.chilinoodles +package org.kimplify import kotlin.test.Test import kotlin.test.assertEquals @@ -9,4 +9,4 @@ class ComposeAppCommonTest { fun example() { assertEquals(3, 1 + 2) } -} \ No newline at end of file +} diff --git a/sample/src/iosMain/kotlin/io/github/chilinoodles/MainViewController.kt b/sample/src/iosMain/kotlin/io/github/chilinoodles/MainViewController.kt deleted file mode 100644 index 5e74758..0000000 --- a/sample/src/iosMain/kotlin/io/github/chilinoodles/MainViewController.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.chilinoodles - -import androidx.compose.ui.window.ComposeUIViewController -<<<<<<< HEAD -import io.chilinoodles.kurrency.sample.App -======= ->>>>>>> origin/main - -fun MainViewController() = ComposeUIViewController { App() } \ No newline at end of file diff --git a/sample/src/iosMain/kotlin/org/kimplify/MainViewController.kt b/sample/src/iosMain/kotlin/org/kimplify/MainViewController.kt new file mode 100644 index 0000000..3f31c03 --- /dev/null +++ b/sample/src/iosMain/kotlin/org/kimplify/MainViewController.kt @@ -0,0 +1,6 @@ +package org.kimplify + +import androidx.compose.ui.window.ComposeUIViewController +import org.kimplify.kurrency.sample.App + +fun MainViewController() = ComposeUIViewController { App() } diff --git a/sample/src/jvmMain/kotlin/io/github/chilinoodles/main.kt b/sample/src/jvmMain/kotlin/org/kimplify/main.kt similarity index 75% rename from sample/src/jvmMain/kotlin/io/github/chilinoodles/main.kt rename to sample/src/jvmMain/kotlin/org/kimplify/main.kt index 7c8896a..292b96c 100644 --- a/sample/src/jvmMain/kotlin/io/github/chilinoodles/main.kt +++ b/sample/src/jvmMain/kotlin/org/kimplify/main.kt @@ -1,8 +1,8 @@ -package io.github.chilinoodles +package org.kimplify import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import io.chilinoodles.kurrency.sample.App +import org.kimplify.kurrency.sample.App fun main() = application { Window( @@ -11,4 +11,4 @@ fun main() = application { ) { App() } -} \ No newline at end of file +} diff --git a/sample/src/wasmJsMain/kotlin/io/github/chilinoodles/main.kt b/sample/src/wasmJsMain/kotlin/org/kimplify/main.kt similarity index 72% rename from sample/src/wasmJsMain/kotlin/io/github/chilinoodles/main.kt rename to sample/src/wasmJsMain/kotlin/org/kimplify/main.kt index 934d4f5..5bbc196 100644 --- a/sample/src/wasmJsMain/kotlin/io/github/chilinoodles/main.kt +++ b/sample/src/wasmJsMain/kotlin/org/kimplify/main.kt @@ -1,12 +1,12 @@ -package io.github.chilinoodles +package org.kimplify import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport -import io.chilinoodles.kurrency.sample.App +import org.kimplify.kurrency.sample.App @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport { App() } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0456565..5a688f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,4 +34,4 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -include(":sample", "kurrency") +include(":sample", ":kurrency-core", ":kurrency-compose")