From 5c84ca24f57f82a422896706dbe3c8652df082c8 Mon Sep 17 00:00:00 2001 From: kamilkalisz Date: Tue, 25 Feb 2025 17:27:32 +0100 Subject: [PATCH 1/3] date time format init --- build.gradle.kts | 2 +- config/detekt/detekt.yml | 3 +- gradle/libs.versions.toml | 4 +- library/build.gradle.kts | 8 ++ .../bngdev/formatk/date/AppleDateFormatter.kt | 21 +++++ .../formatk/date/AppleDateFormatterFactory.kt | 51 +++++++++++ .../date/DateFormatterProvider.apple.kt | 9 ++ .../date/DateFormatterProvider.commonJs.kt | 10 +++ .../com/bngdev/formatk/date/DateTime.kt | 40 +++++++++ .../formatk/date/DateTimeOptionsTemplate.kt | 6 ++ .../formatk/date/FromFormatOptionsTemplate.kt | 5 ++ .../formatk/date/JsDateFormatFactory.kt | 22 +++++ .../bngdev/formatk/date/JsDateFormatter.kt | 88 +++++++++++++++++++ .../date/ToLocaleStringOptionsTemplate.kt | 5 ++ .../com/bngdev/formatk/LocaleInfoExt.kt | 6 ++ .../date/DateFormatterProvider.commonJvm.kt | 24 +++++ .../bngdev/formatk/date/JvmDateFormatter.kt | 24 +++++ .../formatk/date/JvmDateFormatterFactory.kt | 55 ++++++++++++ .../formatk/date/LegacyJvmDateFormatter.kt | 28 ++++++ .../date/LegacyJvmDateFormatterFactory.kt | 55 ++++++++++++ .../formatk/number/JvmNumberFormatFactory.kt | 4 +- .../com/bngdev/formatk/date/DateFormatter.kt | 13 +++ .../formatk/date/DateFormatterFactory.kt | 7 ++ .../formatk/date/DateFormatterProvider.kt | 15 ++++ .../formatk/date/DateFormatterSettings.kt | 27 ++++++ .../bngdev/formatk/date/DateTimeFormatMode.kt | 7 ++ .../com/bngdev/formatk/date/FormatStyle.kt | 5 ++ .../com/bngdev/formatk/date/HourCycle.kt | 5 ++ .../formatk/number/DateFormatterTest.kt | 25 ++++++ 29 files changed, 568 insertions(+), 6 deletions(-) create mode 100644 library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt create mode 100644 library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt create mode 100644 library/src/appleMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.apple.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt create mode 100644 library/src/commonJvmMain/kotlin/com/bngdev/formatk/LocaleInfoExt.kt create mode 100644 library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJvm.kt create mode 100644 library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt create mode 100644 library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt create mode 100644 library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt create mode 100644 library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatter.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterFactory.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterSettings.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/DateTimeFormatMode.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/FormatStyle.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/date/HourCycle.kt create mode 100644 library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 03ed078..2cce5ac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } detekt { - toolVersion = "1.23.7" + toolVersion = "1.23.8" config.setFrom(files("$rootDir/config/detekt/detekt.yml")) parallel = true buildUponDefaultConfig = true diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 0750248..9279f82 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -348,6 +348,7 @@ naming: MatchingDeclarationName: active: true mustBeFirst: true + multiplatformTargets: ['ios', 'android', 'js', 'jvm', 'native', 'iosArm64', 'iosX64', 'macosX64', 'mingwX64', 'linuxX64' , 'commonJvm', 'commonJs'] MemberNameEqualsClassName: active: true ignoreOverridden: true @@ -782,4 +783,4 @@ style: WildcardImport: active: true excludeImports: - - 'java.util.*' + - 'java.util.*' \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dcadce0..9e6efa3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,12 +3,14 @@ agp = "8.5.2" kotlin = "2.1.0" android-minSdk = "24" android-compileSdk = "34" +kotlinxDatetime = "0.6.2" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } -detekt = { id ="io.gitlab.arturbosch.detekt", version ="1.23.7" } +detekt = { id ="io.gitlab.arturbosch.detekt", version ="1.23.8" } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index e91c346..9f6d453 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -60,10 +60,18 @@ kotlin { @Suppress("UnusedPrivateMember") val commonMain by getting { dependencies { + implementation(libs.kotlinx.datetime) // put your multiplatform dependencies here } } + @Suppress("UnusedPrivateMember") + val commonJsMain by getting { + dependencies { + implementation(npm("luxon", "3.4.3")) + } + } + @Suppress("UnusedPrivateMember") val commonTest by getting { dependencies { diff --git a/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt new file mode 100644 index 0000000..3664906 --- /dev/null +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt @@ -0,0 +1,21 @@ +package com.bngdev.formatk.date + +import kotlinx.datetime.Instant +import kotlinx.datetime.toKotlinInstant +import kotlinx.datetime.toNSDate +import platform.Foundation.NSDateFormatter + +class AppleDateFormatter( + private val dateFormatter: NSDateFormatter +) : DateFormatter { + + override fun format(instant: Instant): String { + val date = instant.toNSDate() + return dateFormatter.stringFromDate(date) + } + + override fun parse(value: String): Instant? { + val date = dateFormatter.dateFromString(value) + return date?.toKotlinInstant() + } +} diff --git a/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt new file mode 100644 index 0000000..d86ffe3 --- /dev/null +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt @@ -0,0 +1,51 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo +import com.bngdev.formatk.number.toNSLocale +import platform.Foundation.NSDateFormatter +import platform.Foundation.NSDateFormatterFullStyle +import platform.Foundation.NSDateFormatterLongStyle +import platform.Foundation.NSDateFormatterMediumStyle +import platform.Foundation.NSDateFormatterNoStyle +import platform.Foundation.NSDateFormatterShortStyle +import platform.Foundation.NSDateFormatterStyle +import platform.Foundation.NSTimeZone +import platform.Foundation.localTimeZone +import platform.Foundation.timeZoneWithName + +class AppleDateFormatterFactory(private val localeInfo: LocaleInfo) : DateFormatterFactory { + + override fun getFormatter(formatOptions: DateFormatterSettings): DateFormatter { + val dateFormatter = NSDateFormatter() + + // Set the locale + dateFormatter.locale = localeInfo.toNSLocale() + + // Apply date and time styles based on format mode + if (formatOptions.pattern != null) { + dateFormatter.dateFormat = formatOptions.pattern + } else { + dateFormatter.dateStyle = formatOptions.dateStyle.toNSDateFormatterStyle() + dateFormatter.timeStyle = formatOptions.timeStyle.toNSDateFormatterStyle() + } + + // TimeZone handling + formatOptions.timeZone?.let { timeZone -> + val nsTimeZone = NSTimeZone.timeZoneWithName(timeZone.id) ?: NSTimeZone.localTimeZone + dateFormatter.timeZone = nsTimeZone + } + + return AppleDateFormatter(dateFormatter) + } +} + +// Helper extensions to convert FormatStyle to NSDateFormatterStyle +private fun FormatStyle.toNSDateFormatterStyle(): NSDateFormatterStyle { + return when (this) { + FormatStyle.SHORT -> NSDateFormatterShortStyle + FormatStyle.MEDIUM -> NSDateFormatterMediumStyle + FormatStyle.LONG -> NSDateFormatterLongStyle + FormatStyle.FULL -> NSDateFormatterFullStyle + FormatStyle.NONE -> NSDateFormatterNoStyle + } +} diff --git a/library/src/appleMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.apple.kt b/library/src/appleMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.apple.kt new file mode 100644 index 0000000..f5e703f --- /dev/null +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.apple.kt @@ -0,0 +1,9 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo + +actual object DateFormatterProvider { + actual fun getInstance(locale: LocaleInfo): DateFormatterFactory { + return AppleDateFormatterFactory(locale) + } +} diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt new file mode 100644 index 0000000..fbca414 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt @@ -0,0 +1,10 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo + +actual object DateFormatterProvider { + actual fun getInstance(locale: LocaleInfo): DateFormatterFactory { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt new file mode 100644 index 0000000..116b1d3 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt @@ -0,0 +1,40 @@ +package luxon + +external class DateTime { + companion object { + fun now(options: DateTimeOptions): DateTime + fun fromMillis(millis: Double, options: DateTimeOptions): DateTime + fun fromISO(isoString: String, options: DateTimeOptions): DateTime + fun fromFormat(text: String, format: String, options: FromFormatOptions): DateTime + } + + fun toISO(): String + fun toMillis(): Double + fun toLocaleString(options: ToLocaleStringOptions): String + fun toFormat(format: String): String + fun setZone(zone: String): DateTime + fun setLocale(locale: String): DateTime + fun isValid(): Boolean +} + + +external interface DateTimeOptions { + var zone: String? // Time zone, e.g., "UTC", "America/New_York" + var locale: String? // Locale, e.g., "en-US" + var outputCalendar: String? // Calendar system, e.g., "gregory" + var numberingSystem: String? // Numbering system, e.g., "latn" +} + +external interface ToLocaleStringOptions { + var locale: String? // e.g., "fr-FR" + var timeZone: String? // e.g., "Asia/Tokyo" + var dateStyle: String? // "full", "long", "medium", "short" + var timeStyle: String? // "full", "long", "medium", "short" + var hourCycle: String? // "h11", "h12", "h23", "h24" +} + +external interface FromFormatOptions { + var zone: String? // Time zone + var locale: String? // Locale + var defaultZone: Boolean? // Whether to use system default zone +} \ No newline at end of file diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt new file mode 100644 index 0000000..59733f7 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt @@ -0,0 +1,6 @@ +package com.bngdev.formatk.date + +import luxon.DateTimeOptions +import luxon.ToLocaleStringOptions + +expect fun toLocaleStringOptionsTemplate(): ToLocaleStringOptions diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt new file mode 100644 index 0000000..c360ffe --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt @@ -0,0 +1,5 @@ +package com.bngdev.formatk.date + +import luxon.FromFormatOptions + +expect fun fromFormatOptionsTemplate(): FromFormatOptions diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt new file mode 100644 index 0000000..93b4206 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt @@ -0,0 +1,22 @@ +package com.bngdev.formatk.date + +import kotlinx.datetime.TimeZone + +class JsDateFormatFactory( + private val locale: String, +) : DateFormatterFactory { + + override fun getFormatter(formatOptions: DateFormatterSettings?): DateFormatter { + return JsDateFormatter(locale, formatOptions ?: getDefaultSettings()) + } + + override fun getDefaultSettings(): DateFormatterSettings { + return DateFormatterSettings( + timeZone = TimeZone.UTC, + pattern = null, + dateStyle = FormatStyle.LONG, + timeStyle = FormatStyle.SHORT, + hourCycle = HourCycle.H12 + ) + } +} diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt new file mode 100644 index 0000000..b58ca4f --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt @@ -0,0 +1,88 @@ +package com.bngdev.formatk.date + +import kotlinx.datetime.Instant +import luxon.DateTime + +class JsDateFormatter( + private val locale: String, // Now accepts locale directly in the constructor + private val settings: DateFormatterSettings +) : DateFormatter { + + override fun format(instant: Instant): String { + val dateTime = DateTime.fromMillis( + instant.toEpochMilliseconds().toDouble(), + dateTimeOptionsTemplate().also { + it.zone = settings.timeZone?.id // Apply time zone if provided + it.locale = locale // Apply the passed locale + } + ) + + return if (settings.pattern != null) { + dateTime.toFormat(settings.pattern) + } else { + dateTime.toLocaleString( + toLocaleStringOptionsTemplate().apply { + dateStyle = settings.dateStyle.toLuxon() + timeStyle = settings.timeStyle.toLuxon() + hourCycle = settings.hourCycle.toLuxon() + timeZone = settings.timeZone?.id + this.locale = locale // Apply locale here as well + } + ) + } + } + + // Parsing method to convert formatted string back to Instant + override fun parse(value: String): Instant? { + val luxonDateTime = DateTime.fromFormat( + value, + settings.pattern ?: getDefaultPattern(settings), + fromFormatOptionsTemplate().apply { + this.locale = locale // Apply locale for parsing + } + ) + if (!luxonDateTime.isValid()) { + return null + } + + return luxonDateTime.toMillis().let { Instant.fromEpochMilliseconds(it.toLong()) } + } + + private fun getDefaultPattern(settings: DateFormatterSettings?): String { + val dateStyle = settings?.dateStyle ?: FormatStyle.LONG + val timeStyle = settings?.timeStyle ?: FormatStyle.SHORT + + val datePattern = when (dateStyle) { + FormatStyle.SHORT -> "MM/dd/yyyy" + FormatStyle.MEDIUM -> "MMM dd, yyyy" + FormatStyle.LONG -> "MMMM dd, yyyy" + FormatStyle.FULL -> "EEEE, MMMM dd, yyyy" + } + + val timePattern = when (timeStyle) { + FormatStyle.SHORT -> "HH:mm" + FormatStyle.MEDIUM -> "HH:mm:ss" + FormatStyle.LONG -> "HH:mm:ss z" + FormatStyle.FULL -> "HH:mm:ss zzzz" + } + + return "$datePattern $timePattern" + } +} + +// Extensions to map Kotlin settings to Luxon values +private fun FormatStyle.toLuxon(): String { + return when (this) { + FormatStyle.SHORT -> "short" + FormatStyle.MEDIUM -> "medium" + FormatStyle.LONG -> "long" + FormatStyle.FULL -> "full" + } +} + +private fun HourCycle.toLuxon(): String { + return when (this) { + HourCycle.H12 -> "h12" + HourCycle.H24 -> "h23" + } +} diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt new file mode 100644 index 0000000..426279d --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt @@ -0,0 +1,5 @@ +package com.bngdev.formatk.date + +import luxon.DateTimeOptions + +expect fun dateTimeOptionsTemplate(): DateTimeOptions diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/LocaleInfoExt.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/LocaleInfoExt.kt new file mode 100644 index 0000000..63f7438 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/LocaleInfoExt.kt @@ -0,0 +1,6 @@ +package com.bngdev.formatk + +import java.util.Locale + +// TODO check version of java if possible take into consideration android (Locale.of) +fun LocaleInfo.toLocale(): Locale = Locale(this.language, this.region) diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJvm.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJvm.kt new file mode 100644 index 0000000..5d84be3 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJvm.kt @@ -0,0 +1,24 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo + +actual object DateFormatterProvider { + + private val isJavaTimeSupported: Boolean by lazy { + @Suppress("SwallowedException") + try { + Class.forName("java.time.format.DateTimeFormatter") + true + } catch (e: ClassNotFoundException) { + false + } + } + + actual fun getInstance(locale: LocaleInfo): DateFormatterFactory { + return if (isJavaTimeSupported) { + JvmDateFormatterFactory(locale) + } else { + LegacyJvmDateFormatterFactory(locale) + } + } +} diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt new file mode 100644 index 0000000..f822963 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt @@ -0,0 +1,24 @@ +package com.bngdev.formatk.date + +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant +import kotlinx.datetime.toKotlinInstant +import java.text.ParseException +import java.time.format.DateTimeFormatter + +class JvmDateFormatter(private val dateTimeFormatter: DateTimeFormatter) : DateFormatter { + + override fun format(instant: Instant): String { + return dateTimeFormatter.format(instant.toJavaInstant()) + } + + override fun parse(value: String): Instant? { + return try { + val date = dateTimeFormatter.parse(value, java.time.Instant::from) + return date.toKotlinInstant() + + } catch (e: ParseException) { + null + } + } +} diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt new file mode 100644 index 0000000..61c5e20 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt @@ -0,0 +1,55 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo +import com.bngdev.formatk.toLocale +import kotlinx.datetime.toJavaZoneId +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale +import java.util.TimeZone +import com.bngdev.formatk.date.FormatStyle as KotlinFormatStyle + +class JvmDateFormatterFactory( + private val locale: LocaleInfo +) : DateFormatterFactory { + + override fun getFormatter(formatOptions: DateFormatterSettings): DateFormatter { + val zone = getZoneId(formatOptions) + val formatter = getDefaultPattern(formatOptions, locale.toLocale()).withZone(zone) + return JvmDateFormatter(formatter) + } + + private fun getZoneId(formatOptions: DateFormatterSettings): ZoneId? { + if (formatOptions.timeZone != null) { + return formatOptions.timeZone.toJavaZoneId() + } + return TimeZone.getDefault().toZoneId() + } + + private fun KotlinFormatStyle.toJavaFormatStyle(): FormatStyle { + return when (this) { + KotlinFormatStyle.SHORT -> FormatStyle.SHORT + KotlinFormatStyle.MEDIUM -> FormatStyle.MEDIUM + KotlinFormatStyle.LONG -> FormatStyle.LONG + KotlinFormatStyle.FULL -> FormatStyle.FULL + KotlinFormatStyle.NONE -> FormatStyle.FULL // TODO, but no difference as we ignore this + } + } + + private fun getDefaultPattern(settings: DateFormatterSettings, locale: Locale): DateTimeFormatter { + if (settings.pattern != null) return DateTimeFormatter.ofPattern(settings.pattern) + val dateStyle = settings.dateStyle.toJavaFormatStyle() + val timeStyle = settings.timeStyle.toJavaFormatStyle() + val formatMode = settings.getFormatMode() + + return when (formatMode) { + DateTimeFormatMode.DATE_ONLY -> + DateTimeFormatter.ofLocalizedDate(dateStyle).withLocale(locale) + DateTimeFormatMode.TIME_ONLY -> + DateTimeFormatter.ofLocalizedTime(timeStyle).withLocale(locale) + DateTimeFormatMode.DATE_TIME -> + DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle).withLocale(locale) + } + } +} diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt new file mode 100644 index 0000000..352ba62 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt @@ -0,0 +1,28 @@ +package com.bngdev.formatk.date + +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant +import kotlinx.datetime.toKotlinInstant +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date + +class LegacyJvmDateFormatter(private val simpleDateFormat: SimpleDateFormat) : DateFormatter { + + override fun format(instant: Instant): String { + // Convert Instant to Java's Date + val date = Date.from(instant.toJavaInstant()) + return simpleDateFormat.format(date) + } + + override fun parse(value: String): Instant? { + return try { + val date = simpleDateFormat.parse(value) + // Convert Java Date back to Instant + date?.toInstant()?.toKotlinInstant() + + } catch (e: ParseException) { + null + } + } +} diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt new file mode 100644 index 0000000..f79106c --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt @@ -0,0 +1,55 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo +import com.bngdev.formatk.toLocale +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone +import com.bngdev.formatk.date.FormatStyle as KotlinFormatStyle + +class LegacyJvmDateFormatterFactory( + private val locale: LocaleInfo +) : DateFormatterFactory { + + override fun getFormatter(formatOptions: DateFormatterSettings): DateFormatter { + val zone = getZoneId(formatOptions) + val pattern = getDefaultPattern(formatOptions, locale.toLocale()) + + val formatter = SimpleDateFormat(pattern, locale.toLocale()).apply { + timeZone = zone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault() + } + + return LegacyJvmDateFormatter(formatter) + } + + private fun getZoneId(formatOptions: DateFormatterSettings): TimeZone? { + return formatOptions.timeZone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault() + } + + private fun KotlinFormatStyle.toLegacyStyle(): Int { + return when (this) { + KotlinFormatStyle.SHORT -> DateFormat.SHORT + KotlinFormatStyle.MEDIUM -> DateFormat.MEDIUM + KotlinFormatStyle.LONG -> DateFormat.LONG + KotlinFormatStyle.FULL -> DateFormat.FULL + KotlinFormatStyle.NONE -> DateFormat.DEFAULT // Fallback, ignored in Java 7 + } + } + + private fun getDefaultPattern(settings: DateFormatterSettings, locale: Locale): String { + if (settings.pattern != null) return settings.pattern + + val dateStyle = settings.dateStyle.toLegacyStyle() + val timeStyle = settings.timeStyle.toLegacyStyle() + val formatMode = settings.getFormatMode() + + val formatter = when (formatMode) { + DateTimeFormatMode.DATE_ONLY -> DateFormat.getDateInstance(dateStyle, locale) + DateTimeFormatMode.TIME_ONLY -> DateFormat.getTimeInstance(timeStyle, locale) + DateTimeFormatMode.DATE_TIME -> DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale) + } as SimpleDateFormat + + return formatter.toPattern() + } +} diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/number/JvmNumberFormatFactory.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/number/JvmNumberFormatFactory.kt index 7edbf8a..8904565 100644 --- a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/number/JvmNumberFormatFactory.kt +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/number/JvmNumberFormatFactory.kt @@ -1,6 +1,7 @@ package com.bngdev.formatk.number import com.bngdev.formatk.LocaleInfo +import com.bngdev.formatk.toLocale import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency @@ -67,9 +68,6 @@ class JvmNumberFormatFactory( } } - // TODO check version of java if possible take into consideration android (Locale.of) - private fun LocaleInfo.toLocale(): Locale = Locale(this.language, this.region) - private fun java.math.RoundingMode.toFormatkRoundingMode(): RoundingMode = when (this) { java.math.RoundingMode.UP -> RoundingMode.UP java.math.RoundingMode.DOWN -> RoundingMode.DOWN diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatter.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatter.kt new file mode 100644 index 0000000..a144844 --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatter.kt @@ -0,0 +1,13 @@ +package com.bngdev.formatk.date + +import kotlinx.datetime.Instant + +interface DateFormatter { + fun format( + instant: Instant + ): String + + fun parse( + value: String + ): Instant? +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterFactory.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterFactory.kt new file mode 100644 index 0000000..ff49c49 --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterFactory.kt @@ -0,0 +1,7 @@ +package com.bngdev.formatk.date + +interface DateFormatterFactory { + fun getFormatter( + formatOptions: DateFormatterSettings + ): DateFormatter +} diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.kt new file mode 100644 index 0000000..b1f2c02 --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.kt @@ -0,0 +1,15 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo + +expect object DateFormatterProvider { + + fun getInstance(locale: LocaleInfo): DateFormatterFactory +} + +fun DateFormatterProvider.getFormatter( + locale: LocaleInfo, + formatOptions: DateFormatterSettings = DateFormatterSettings() +): DateFormatter { + return getInstance(locale).getFormatter(formatOptions) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterSettings.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterSettings.kt new file mode 100644 index 0000000..74620f7 --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateFormatterSettings.kt @@ -0,0 +1,27 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.date.DateTimeFormatMode.DATE_ONLY +import com.bngdev.formatk.date.DateTimeFormatMode.DATE_TIME +import com.bngdev.formatk.date.DateTimeFormatMode.TIME_ONLY +import kotlinx.datetime.TimeZone + +data class DateFormatterSettings( + val timeZone: TimeZone? = null, // Time zone (e.g., "UTC", "America/New_York") + val pattern: String? = null, // Custom pattern + val dateStyle: FormatStyle = FormatStyle.LONG, // Date formatting style + val timeStyle: FormatStyle = FormatStyle.SHORT, // Time formatting style +) + +fun DateFormatterSettings.getFormatMode(): DateTimeFormatMode { + if (dateStyle != FormatStyle.NONE) { + if (timeStyle != FormatStyle.NONE) { + return DATE_TIME + } + return DATE_ONLY + } else { + if (timeStyle != FormatStyle.NONE) { + return TIME_ONLY + } + return DATE_TIME // TODO + } +} diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateTimeFormatMode.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateTimeFormatMode.kt new file mode 100644 index 0000000..28f5cdb --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/DateTimeFormatMode.kt @@ -0,0 +1,7 @@ +package com.bngdev.formatk.date + +enum class DateTimeFormatMode { + DATE_ONLY, // Format only the date + TIME_ONLY, // Format only the time + DATE_TIME; // Format both date and time +} diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/FormatStyle.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/FormatStyle.kt new file mode 100644 index 0000000..f11adbb --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/FormatStyle.kt @@ -0,0 +1,5 @@ +package com.bngdev.formatk.date + +enum class FormatStyle { + NONE, SHORT, MEDIUM, LONG, FULL +} diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/date/HourCycle.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/date/HourCycle.kt new file mode 100644 index 0000000..486f10b --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/date/HourCycle.kt @@ -0,0 +1,5 @@ +package com.bngdev.formatk.date + +enum class HourCycle { + H12, H24 +} \ No newline at end of file diff --git a/library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt b/library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt new file mode 100644 index 0000000..5e238fc --- /dev/null +++ b/library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt @@ -0,0 +1,25 @@ +package com.bngdev.formatk.number + +import com.bngdev.formatk.LocaleInfo +import com.bngdev.formatk.date.DateFormatterFactory +import com.bngdev.formatk.date.DateFormatterProvider +import com.bngdev.formatk.date.DateFormatterSettings +import com.bngdev.formatk.date.getFormatter +import kotlinx.datetime.Instant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.expect + +class DateFormatterTest { + + @Test + fun `test basic locale creation`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale) + val date = Instant.parse("2017-12-04T08:07:00Z") + println(date.toString()) + assertEquals("21312", formatter.format(date)) + } +} From 2cfbea1a571bf16c2b2f81ebf11b095227d7baee Mon Sep 17 00:00:00 2001 From: kkalisz Date: Thu, 27 Feb 2025 19:28:03 +0100 Subject: [PATCH 2/3] added implementation of date time formatter --- kotlin-js-store/yarn.lock | 10 + library/build.gradle.kts | 10 + .../bngdev/formatk/date/AppleDateFormatter.kt | 5 +- .../formatk/date/AppleDateFormatterFactory.kt | 36 +- .../date/DateFormatterProvider.commonJs.kt | 5 +- .../formatk/date/DateOptionsTemplate.kt | 12 + .../com/bngdev/formatk/date/DateTime.kt | 40 -- .../formatk/date/DateTimeOptionsTemplate.kt | 6 - .../formatk/date/FromFormatOptionsTemplate.kt | 5 - .../formatk/date/JsDateFormatFactory.kt | 22 +- .../bngdev/formatk/date/JsDateFormatter.kt | 108 ++-- .../date/ToLocaleStringOptionsTemplate.kt | 5 - .../commonJsMain/kotlin/luxon/LuxonOptions.kt | 35 + .../bngdev/formatk/date/JvmDateFormatter.kt | 3 +- .../formatk/date/JvmDateFormatterFactory.kt | 10 +- .../formatk/date/LegacyJvmDateFormatter.kt | 5 +- .../date/LegacyJvmDateFormatterFactory.kt | 13 +- .../com/bngdev/formatk/utils/StringExt.kt | 5 + .../bngdev/formatk/date/DateFormatterTest.kt | 606 ++++++++++++++++++ .../formatk/number/DateFormatterTest.kt | 25 - .../formatk/date/DateOptionsTemplate.kt | 15 + library/src/jsMain/kotlin/luxon/DateTime.kt | 18 + .../formatk/date/DateOptionsTemplate.kt | 12 + .../src/wasmJsMain/kotlin/luxon/DateTime.kt | 18 + 24 files changed, 843 insertions(+), 186 deletions(-) create mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt delete mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt delete mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt delete mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt delete mode 100644 library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt create mode 100644 library/src/commonJsMain/kotlin/luxon/LuxonOptions.kt create mode 100644 library/src/commonMain/kotlin/com/bngdev/formatk/utils/StringExt.kt create mode 100644 library/src/commonTest/kotlin/com/bngdev/formatk/date/DateFormatterTest.kt delete mode 100644 library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt create mode 100644 library/src/jsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt create mode 100644 library/src/jsMain/kotlin/luxon/DateTime.kt create mode 100644 library/src/wasmJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt create mode 100644 library/src/wasmJsMain/kotlin/luxon/DateTime.kt diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index a4c9ab1..e087203 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -52,6 +52,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" @@ -1703,6 +1708,11 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.1.5" +luxon@3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.3.tgz#8ddf0358a9492267ffec6a13675fbaab5551315d" + integrity sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 9f6d453..5d2b69e 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -36,10 +36,14 @@ kotlin { binaries.executable() } + @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() + compilerOptions { + freeCompilerArgs.add("-Xwasm-attach-js-exception") + } } @OptIn(ExperimentalKotlinGradlePluginApi::class) @@ -78,6 +82,12 @@ kotlin { implementation(libs.kotlin.test) } } + + val wasmJsMain by getting { + dependencies{ + implementation(npm("luxon", "3.4.3")) + } + } } targets.configureEach { diff --git a/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt index 3664906..1cd2fa1 100644 --- a/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt @@ -1,17 +1,18 @@ package com.bngdev.formatk.date +import com.bngdev.formatk.utils.normalizeWhitespaces import kotlinx.datetime.Instant import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toNSDate import platform.Foundation.NSDateFormatter class AppleDateFormatter( - private val dateFormatter: NSDateFormatter + private val dateFormatter: NSDateFormatter, ) : DateFormatter { override fun format(instant: Instant): String { val date = instant.toNSDate() - return dateFormatter.stringFromDate(date) + return dateFormatter.stringFromDate(date).normalizeWhitespaces() } override fun parse(value: String): Instant? { diff --git a/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt index d86ffe3..5f7cf77 100644 --- a/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt @@ -2,6 +2,7 @@ package com.bngdev.formatk.date import com.bngdev.formatk.LocaleInfo import com.bngdev.formatk.number.toNSLocale +import kotlinx.datetime.toNSTimeZone import platform.Foundation.NSDateFormatter import platform.Foundation.NSDateFormatterFullStyle import platform.Foundation.NSDateFormatterLongStyle @@ -18,34 +19,37 @@ class AppleDateFormatterFactory(private val localeInfo: LocaleInfo) : DateFormat override fun getFormatter(formatOptions: DateFormatterSettings): DateFormatter { val dateFormatter = NSDateFormatter() - // Set the locale dateFormatter.locale = localeInfo.toNSLocale() - // Apply date and time styles based on format mode if (formatOptions.pattern != null) { dateFormatter.dateFormat = formatOptions.pattern } else { - dateFormatter.dateStyle = formatOptions.dateStyle.toNSDateFormatterStyle() - dateFormatter.timeStyle = formatOptions.timeStyle.toNSDateFormatterStyle() + dateFormatter.dateStyle = toNSDateFormatterStyle(formatOptions.dateStyle) + dateFormatter.timeStyle = toNSDateFormatterStyle(formatOptions.timeStyle) } - // TimeZone handling formatOptions.timeZone?.let { timeZone -> - val nsTimeZone = NSTimeZone.timeZoneWithName(timeZone.id) ?: NSTimeZone.localTimeZone - dateFormatter.timeZone = nsTimeZone + dateFormatter.timeZone = timeZone.toNSTimeZone() + } + if(formatOptions.pattern == null){ + dateFormatter.dateFormat = removeQuotedWordsWithSpaceAndQuotes(dateFormatter.dateFormat) } return AppleDateFormatter(dateFormatter) } -} -// Helper extensions to convert FormatStyle to NSDateFormatterStyle -private fun FormatStyle.toNSDateFormatterStyle(): NSDateFormatterStyle { - return when (this) { - FormatStyle.SHORT -> NSDateFormatterShortStyle - FormatStyle.MEDIUM -> NSDateFormatterMediumStyle - FormatStyle.LONG -> NSDateFormatterLongStyle - FormatStyle.FULL -> NSDateFormatterFullStyle - FormatStyle.NONE -> NSDateFormatterNoStyle + private fun removeQuotedWordsWithSpaceAndQuotes(format: String): String { + // with iso we have often additional text inside format for example: "dd.MM.y 'at' HH:mm:ss" + return Regex("'\\s*[^']+\\s*'" ).replace(format, "") // Replaces with an empty string + } + + private fun toNSDateFormatterStyle(style: FormatStyle): NSDateFormatterStyle { + return when (style) { + FormatStyle.SHORT -> NSDateFormatterShortStyle + FormatStyle.MEDIUM -> NSDateFormatterMediumStyle + FormatStyle.LONG -> NSDateFormatterLongStyle + FormatStyle.FULL -> NSDateFormatterFullStyle + FormatStyle.NONE -> NSDateFormatterNoStyle + } } } diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt index fbca414..69b9075 100644 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.kt @@ -4,7 +4,6 @@ import com.bngdev.formatk.LocaleInfo actual object DateFormatterProvider { actual fun getInstance(locale: LocaleInfo): DateFormatterFactory { - TODO("Not yet implemented") + return JsDateFormatFactory(locale.toLanguageTag()) } - -} \ No newline at end of file +} diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt new file mode 100644 index 0000000..e6fd5f3 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt @@ -0,0 +1,12 @@ +package com.bngdev.formatk.date + +import luxon.DateTimeOptions +import luxon.FromFormatOptions +import luxon.ToLocaleStringOptions + +expect fun toLocaleStringTemplate(): ToLocaleStringOptions + +expect fun dateTimeOptionsTemplate(): DateTimeOptions + +expect fun fromFormatOptionsTemplate(): FromFormatOptions + diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt deleted file mode 100644 index 116b1d3..0000000 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTime.kt +++ /dev/null @@ -1,40 +0,0 @@ -package luxon - -external class DateTime { - companion object { - fun now(options: DateTimeOptions): DateTime - fun fromMillis(millis: Double, options: DateTimeOptions): DateTime - fun fromISO(isoString: String, options: DateTimeOptions): DateTime - fun fromFormat(text: String, format: String, options: FromFormatOptions): DateTime - } - - fun toISO(): String - fun toMillis(): Double - fun toLocaleString(options: ToLocaleStringOptions): String - fun toFormat(format: String): String - fun setZone(zone: String): DateTime - fun setLocale(locale: String): DateTime - fun isValid(): Boolean -} - - -external interface DateTimeOptions { - var zone: String? // Time zone, e.g., "UTC", "America/New_York" - var locale: String? // Locale, e.g., "en-US" - var outputCalendar: String? // Calendar system, e.g., "gregory" - var numberingSystem: String? // Numbering system, e.g., "latn" -} - -external interface ToLocaleStringOptions { - var locale: String? // e.g., "fr-FR" - var timeZone: String? // e.g., "Asia/Tokyo" - var dateStyle: String? // "full", "long", "medium", "short" - var timeStyle: String? // "full", "long", "medium", "short" - var hourCycle: String? // "h11", "h12", "h23", "h24" -} - -external interface FromFormatOptions { - var zone: String? // Time zone - var locale: String? // Locale - var defaultZone: Boolean? // Whether to use system default zone -} \ No newline at end of file diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt deleted file mode 100644 index 59733f7..0000000 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateTimeOptionsTemplate.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.bngdev.formatk.date - -import luxon.DateTimeOptions -import luxon.ToLocaleStringOptions - -expect fun toLocaleStringOptionsTemplate(): ToLocaleStringOptions diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt deleted file mode 100644 index c360ffe..0000000 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/FromFormatOptionsTemplate.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.bngdev.formatk.date - -import luxon.FromFormatOptions - -expect fun fromFormatOptionsTemplate(): FromFormatOptions diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt index 93b4206..162a6e9 100644 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt @@ -1,22 +1,8 @@ -package com.bngdev.formatk.date +package com.bngdev.formatk.date; -import kotlinx.datetime.TimeZone +class JsDateFormatFactory(private val locale: String) : DateFormatterFactory { -class JsDateFormatFactory( - private val locale: String, -) : DateFormatterFactory { - - override fun getFormatter(formatOptions: DateFormatterSettings?): DateFormatter { - return JsDateFormatter(locale, formatOptions ?: getDefaultSettings()) - } - - override fun getDefaultSettings(): DateFormatterSettings { - return DateFormatterSettings( - timeZone = TimeZone.UTC, - pattern = null, - dateStyle = FormatStyle.LONG, - timeStyle = FormatStyle.SHORT, - hourCycle = HourCycle.H12 - ) + override fun getFormatter(formatOptions: DateFormatterSettings): DateFormatter { + return JsDateFormatter(locale, formatOptions) } } diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt index b58ca4f..e948bb3 100644 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt @@ -1,88 +1,96 @@ package com.bngdev.formatk.date +import com.bngdev.formatk.utils.normalizeWhitespaces import kotlinx.datetime.Instant import luxon.DateTime +import luxon.DateTimeOptions +import luxon.FromFormatOptions +import luxon.ToLocaleStringOptions class JsDateFormatter( - private val locale: String, // Now accepts locale directly in the constructor + private val locale: String, private val settings: DateFormatterSettings ) : DateFormatter { override fun format(instant: Instant): String { val dateTime = DateTime.fromMillis( - instant.toEpochMilliseconds().toDouble(), - dateTimeOptionsTemplate().also { - it.zone = settings.timeZone?.id // Apply time zone if provided - it.locale = locale // Apply the passed locale - } + instant.toEpochMilliseconds().toDouble(), createDateTimeOptions() ) - return if (settings.pattern != null) { - dateTime.toFormat(settings.pattern) + dateTime.toFormat(settings.pattern).normalizeWhitespaces() } else { - dateTime.toLocaleString( - toLocaleStringOptionsTemplate().apply { - dateStyle = settings.dateStyle.toLuxon() - timeStyle = settings.timeStyle.toLuxon() - hourCycle = settings.hourCycle.toLuxon() - timeZone = settings.timeZone?.id - this.locale = locale // Apply locale here as well - } - ) + dateTime.toLocaleString(createLocaleOptions()).normalizeWhitespaces() } } - // Parsing method to convert formatted string back to Instant override fun parse(value: String): Instant? { - val luxonDateTime = DateTime.fromFormat( - value, - settings.pattern ?: getDefaultPattern(settings), - fromFormatOptionsTemplate().apply { - this.locale = locale // Apply locale for parsing - } - ) - if (!luxonDateTime.isValid()) { + if(settings.pattern == null){ + return null + } + + val luxonDateTime = try { + DateTime.fromFormat(value, settings.pattern, createFromFormatOptions()) + } catch (e: Throwable) { return null } - return luxonDateTime.toMillis().let { Instant.fromEpochMilliseconds(it.toLong()) } + return try { + luxonDateTime.toMillis().let { Instant.fromEpochMilliseconds(it.toLong()) } + } catch (e: Throwable) { + null + } } - private fun getDefaultPattern(settings: DateFormatterSettings?): String { - val dateStyle = settings?.dateStyle ?: FormatStyle.LONG - val timeStyle = settings?.timeStyle ?: FormatStyle.SHORT + private fun createFromFormatOptions(): FromFormatOptions { + val fromFormatOptions = fromFormatOptionsTemplate() + fromFormatOptions.locale = locale + if (settings.timeZone != null) { + fromFormatOptions.zone = settings.timeZone.id + } + return fromFormatOptions + } - val datePattern = when (dateStyle) { - FormatStyle.SHORT -> "MM/dd/yyyy" - FormatStyle.MEDIUM -> "MMM dd, yyyy" - FormatStyle.LONG -> "MMMM dd, yyyy" - FormatStyle.FULL -> "EEEE, MMMM dd, yyyy" + private fun createDateTimeOptions(): DateTimeOptions { + val dateTimeOptionsTemplate = dateTimeOptionsTemplate() + dateTimeOptionsTemplate.locale = locale + getTimezone(settings)?.let { + dateTimeOptionsTemplate.zone = it } + return dateTimeOptionsTemplate + } - val timePattern = when (timeStyle) { - FormatStyle.SHORT -> "HH:mm" - FormatStyle.MEDIUM -> "HH:mm:ss" - FormatStyle.LONG -> "HH:mm:ss z" - FormatStyle.FULL -> "HH:mm:ss zzzz" + private fun createLocaleOptions(): ToLocaleStringOptions { + val toLocaleStringOptions = toLocaleStringTemplate() + settings.dateStyle.toLuxon()?.let { + toLocaleStringOptions.dateStyle = it + } + settings.timeStyle.toLuxon()?.let { + toLocaleStringOptions.timeStyle = it } + toLocaleStringOptions.locale = locale + getTimezone(settings)?.let { + toLocaleStringOptions.timeZone = it + } + return toLocaleStringOptions + } - return "$datePattern $timePattern" + private fun getTimezone(settings: DateFormatterSettings): String? { + if(settings.timeZone == null) { + return null + } + if(settings.timeZone.id == "Z"){ + return "UTC" + } + return settings.timeZone.id } } -// Extensions to map Kotlin settings to Luxon values -private fun FormatStyle.toLuxon(): String { +private fun FormatStyle.toLuxon(): String? { return when (this) { FormatStyle.SHORT -> "short" FormatStyle.MEDIUM -> "medium" FormatStyle.LONG -> "long" FormatStyle.FULL -> "full" - } -} - -private fun HourCycle.toLuxon(): String { - return when (this) { - HourCycle.H12 -> "h12" - HourCycle.H24 -> "h23" + FormatStyle.NONE -> null } } diff --git a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt deleted file mode 100644 index 426279d..0000000 --- a/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/ToLocaleStringOptionsTemplate.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.bngdev.formatk.date - -import luxon.DateTimeOptions - -expect fun dateTimeOptionsTemplate(): DateTimeOptions diff --git a/library/src/commonJsMain/kotlin/luxon/LuxonOptions.kt b/library/src/commonJsMain/kotlin/luxon/LuxonOptions.kt new file mode 100644 index 0000000..1293911 --- /dev/null +++ b/library/src/commonJsMain/kotlin/luxon/LuxonOptions.kt @@ -0,0 +1,35 @@ + +package luxon + +expect class DateTime { + companion object { + fun now(): DateTime + fun fromMillis(millis: Double): DateTime + fun fromMillis(millis: Double, options: DateTimeOptions): DateTime + fun fromISO(isoString: String, options: DateTimeOptions): DateTime + fun fromFormat(text: String, format: String, options: FromFormatOptions): DateTime + } + + fun toMillis(): Double + fun toLocaleString(options: ToLocaleStringOptions): String + fun toFormat(format: String): String + fun isValid(): Boolean +} + + +external interface DateTimeOptions { + var zone: String? + var locale: String? +} + +external interface ToLocaleStringOptions { + var locale: String? + var timeZone: String? + var dateStyle: String? + var timeStyle: String? +} + +external interface FromFormatOptions { + var zone: String? + var locale: String? +} \ No newline at end of file diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt index f822963..7f9df15 100644 --- a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt @@ -1,5 +1,6 @@ package com.bngdev.formatk.date +import com.bngdev.formatk.utils.normalizeWhitespaces import kotlinx.datetime.Instant import kotlinx.datetime.toJavaInstant import kotlinx.datetime.toKotlinInstant @@ -9,7 +10,7 @@ import java.time.format.DateTimeFormatter class JvmDateFormatter(private val dateTimeFormatter: DateTimeFormatter) : DateFormatter { override fun format(instant: Instant): String { - return dateTimeFormatter.format(instant.toJavaInstant()) + return dateTimeFormatter.format(instant.toJavaInstant()).normalizeWhitespaces() } override fun parse(value: String): Instant? { diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt index 61c5e20..023763a 100644 --- a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatterFactory.kt @@ -27,20 +27,20 @@ class JvmDateFormatterFactory( return TimeZone.getDefault().toZoneId() } - private fun KotlinFormatStyle.toJavaFormatStyle(): FormatStyle { - return when (this) { + private fun toJavaFormatStyle(style: KotlinFormatStyle): FormatStyle { + return when (style) { KotlinFormatStyle.SHORT -> FormatStyle.SHORT KotlinFormatStyle.MEDIUM -> FormatStyle.MEDIUM KotlinFormatStyle.LONG -> FormatStyle.LONG KotlinFormatStyle.FULL -> FormatStyle.FULL - KotlinFormatStyle.NONE -> FormatStyle.FULL // TODO, but no difference as we ignore this + KotlinFormatStyle.NONE -> FormatStyle.SHORT // fallback, ignored usage } } private fun getDefaultPattern(settings: DateFormatterSettings, locale: Locale): DateTimeFormatter { if (settings.pattern != null) return DateTimeFormatter.ofPattern(settings.pattern) - val dateStyle = settings.dateStyle.toJavaFormatStyle() - val timeStyle = settings.timeStyle.toJavaFormatStyle() + val dateStyle = toJavaFormatStyle(settings.dateStyle) + val timeStyle = toJavaFormatStyle(settings.timeStyle) val formatMode = settings.getFormatMode() return when (formatMode) { diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt index 352ba62..32cc826 100644 --- a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt @@ -1,5 +1,6 @@ package com.bngdev.formatk.date +import com.bngdev.formatk.utils.normalizeWhitespaces import kotlinx.datetime.Instant import kotlinx.datetime.toJavaInstant import kotlinx.datetime.toKotlinInstant @@ -10,15 +11,13 @@ import java.util.Date class LegacyJvmDateFormatter(private val simpleDateFormat: SimpleDateFormat) : DateFormatter { override fun format(instant: Instant): String { - // Convert Instant to Java's Date val date = Date.from(instant.toJavaInstant()) - return simpleDateFormat.format(date) + return simpleDateFormat.format(date).normalizeWhitespaces() } override fun parse(value: String): Instant? { return try { val date = simpleDateFormat.parse(value) - // Convert Java Date back to Instant date?.toInstant()?.toKotlinInstant() } catch (e: ParseException) { diff --git a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt index f79106c..448a8bf 100644 --- a/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt @@ -13,11 +13,10 @@ class LegacyJvmDateFormatterFactory( ) : DateFormatterFactory { override fun getFormatter(formatOptions: DateFormatterSettings): DateFormatter { - val zone = getZoneId(formatOptions) val pattern = getDefaultPattern(formatOptions, locale.toLocale()) val formatter = SimpleDateFormat(pattern, locale.toLocale()).apply { - timeZone = zone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault() + timeZone = getZoneId(formatOptions) } return LegacyJvmDateFormatter(formatter) @@ -27,21 +26,21 @@ class LegacyJvmDateFormatterFactory( return formatOptions.timeZone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault() } - private fun KotlinFormatStyle.toLegacyStyle(): Int { - return when (this) { + private fun toLegacyStyle(style: KotlinFormatStyle): Int { + return when (style) { KotlinFormatStyle.SHORT -> DateFormat.SHORT KotlinFormatStyle.MEDIUM -> DateFormat.MEDIUM KotlinFormatStyle.LONG -> DateFormat.LONG KotlinFormatStyle.FULL -> DateFormat.FULL - KotlinFormatStyle.NONE -> DateFormat.DEFAULT // Fallback, ignored in Java 7 + KotlinFormatStyle.NONE -> DateFormat.DEFAULT // fallback, ignored usage } } private fun getDefaultPattern(settings: DateFormatterSettings, locale: Locale): String { if (settings.pattern != null) return settings.pattern - val dateStyle = settings.dateStyle.toLegacyStyle() - val timeStyle = settings.timeStyle.toLegacyStyle() + val dateStyle = toLegacyStyle(settings.dateStyle) + val timeStyle = toLegacyStyle(settings.timeStyle) val formatMode = settings.getFormatMode() val formatter = when (formatMode) { diff --git a/library/src/commonMain/kotlin/com/bngdev/formatk/utils/StringExt.kt b/library/src/commonMain/kotlin/com/bngdev/formatk/utils/StringExt.kt new file mode 100644 index 0000000..1033605 --- /dev/null +++ b/library/src/commonMain/kotlin/com/bngdev/formatk/utils/StringExt.kt @@ -0,0 +1,5 @@ +package com.bngdev.formatk.utils + +fun String.normalizeWhitespaces(): String { + return this.replace("\\s+".toRegex(), " ").replace(' ',' ') +} \ No newline at end of file diff --git a/library/src/commonTest/kotlin/com/bngdev/formatk/date/DateFormatterTest.kt b/library/src/commonTest/kotlin/com/bngdev/formatk/date/DateFormatterTest.kt new file mode 100644 index 0000000..65a9896 --- /dev/null +++ b/library/src/commonTest/kotlin/com/bngdev/formatk/date/DateFormatterTest.kt @@ -0,0 +1,606 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.LocaleInfo +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@Ignore +class DateFormatterTest { + + private val testDate = Instant.parse("2017-12-04T08:07:00Z") + + private val timezoneUtc = TimeZone.of("UTC") + + @Test + fun `test US date with exact pattern`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(pattern = "MMMM d, yyyy") + ) + val formatted = formatter.format(testDate) + assertEquals("December 4, 2017", formatted) + } + + @Test + fun `test German date with exact pattern`() { + val locale = LocaleInfo("de", "DE") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(pattern = "d. MMMM yyyy") + ) + val formatted = formatter.format(testDate) + // Platform-specific note: Month name might be "Dezember" or "December" depending on platform + assertEquals("4. Dezember 2017", formatted) + } + + @Test + fun `test Japanese date with exact pattern`() { + val locale = LocaleInfo("ja", "JP") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(pattern = "yyyy年MM月dd日") + ) + val formatted = formatter.format(testDate) + assertEquals("2017年12月04日", formatted) + } + + // Format style tests + @Test + fun `test SHORT style date format`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(dateStyle = FormatStyle.SHORT, timeStyle = FormatStyle.NONE) + ) + val formatted = formatter.format(testDate) + // Platform-specific note: Format might be "12/4/17" or "12/4/2017" + assertEquals("12/4/17", formatted) + } + + @Test + fun `test MEDIUM style date format`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(dateStyle = FormatStyle.MEDIUM, timeStyle = FormatStyle.NONE) + ) + val formatted = formatter.format(testDate) + assertEquals("Dec 4, 2017", formatted) + } + + @Test + fun `test LONG style date format`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(dateStyle = FormatStyle.LONG, timeStyle = FormatStyle.NONE) + ) + val formatted = formatter.format(testDate) + assertEquals("December 4, 2017", formatted) + } + + @Test + fun `test simple date pattern`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy-MM-dd", + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + assertEquals("2017-12-04", formatted) + } + + @Test + fun `test simple time pattern`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "HH:mm:ss", + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + assertEquals("08:07:00", formatted) + } + + @Test + fun `test simple datetime pattern`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy-MM-dd HH:mm:ss", + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + assertEquals("2017-12-04 08:07:00", formatted) + } + + @Test + fun `test basic US date formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "MM/dd/yyyy HH:mm:ss", + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "12/04/2017 08:07:00" + assertEquals(expected, formatted) + } + + @Test + fun `test basic UK date formatting`() { + val locale = LocaleInfo("en", "GB") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "dd/MM/yyyy HH:mm:ss", + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "04/12/2017 08:07:00" + assertEquals(expected, formatted) + } + + @Test + fun `test Japanese date formatting`() { + val locale = LocaleInfo("ja", "JP") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy年MM月dd日 HH:mm:ss", + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "2017年12月04日 08:07:00" + assertEquals(expected, formatted) + } + + @Test + fun `test short style date formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "12/4/17, 8:07 AM" + assertEquals(expected, formatted) + + } + + @Test + fun `test medium style date formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.MEDIUM, + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "Dec 4, 2017, 8:07:00 AM" + assertEquals(expected, formatted) + } + + + @Test + @Ignore + fun `test long style date formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + dateStyle = FormatStyle.LONG, + timeStyle = FormatStyle.LONG, + timeZone = TimeZone.of("UTC") + )) + val formatted = formatter.format(testDate) + val expected = "December 4, 2017 8:07:00 AM UTC" + assertEquals(expected, formatted) + } + + @Test + fun `test date only formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.NONE, + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "12/4/17" + assertEquals(expected, formatted) + } + + @Test + fun `test time only formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + dateStyle = FormatStyle.NONE, + timeStyle = FormatStyle.SHORT, + timeZone = timezoneUtc + )) + val formatted = formatter.format(testDate) + val expected = "8:07 AM" + assertEquals(expected, formatted) + } + + @Test + fun `test New York timezone formatting`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy-MM-dd HH:mm:ss z", + timeZone = TimeZone.of("America/New_York") + )) + val formatted = formatter.format(testDate) + val expected = "2017-12-04 03:07:00 EST" + assertEquals(expected, formatted) + } + + @Test + fun `test Tokyo timezone formatting`() { + val locale = LocaleInfo("ja", "JP") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy-MM-dd HH:mm:ss z", + timeZone = TimeZone.of("Asia/Tokyo") + )) + val formatted = formatter.format(testDate) + val expected = "2017-12-04 17:07:00 JST" + assertEquals(expected, formatted) + } + + @Test + fun `test parse ISO format`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", + timeZone = timezoneUtc + )) + val parsed = formatter.parse("2017-12-04T08:07:00Z") + assertEquals(testDate, parsed) + } + + @Test + fun `test parse custom format`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "MM/dd/yyyy HH:mm:ss", + timeZone = timezoneUtc + )) + val parsed = formatter.parse("12/04/2017 08:07:00") + assertEquals(testDate, parsed) + } + + @Test + fun `test parse invalid format returns null`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "yyyy-MM-dd", + timeZone = timezoneUtc + )) + val parsed = formatter.parse("invalid-date") + assertEquals(null, parsed) + } + + @Test + fun `test format with full month name pattern`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "MMMM dd, yyyy", + timeZone = timezoneUtc + )) + assertEquals("December 04, 2017", formatter.format(testDate)) + } + + @Test + fun `test format with day of week pattern`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter(locale, DateFormatterSettings( + pattern = "EEEE, MMMM dd, yyyy", + timeZone = timezoneUtc + )) + assertEquals("Monday, December 04, 2017", formatter.format(testDate)) + } + + @Test + fun `test US format with SHORT style`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(dateStyle = FormatStyle.SHORT, timeStyle = FormatStyle.SHORT, timeZone = TimeZone.of("Indian/Chagos")) + ) + val date = Instant.parse("2017-12-04T08:07:00Z") + val formatted = formatter.format(date) + assertEquals("12/4/17, 2:07 PM", formatted) + } + + @Test + fun `test French format with MEDIUM style`() { + val locale = LocaleInfo("fr", "FR") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(dateStyle = FormatStyle.MEDIUM, timeStyle = FormatStyle.MEDIUM, timeZone = timezoneUtc) + ) + val date = Instant.parse("2017-12-04T08:07:00Z") + val formatted = formatter.format(date) + // Platform-specific note: iOS format differs from other platforms + // iOS expected: "4 déc. 2017 à 08:07:00" + assertEquals("4 déc. 2017, 08:07:00", formatted) + } + + @Test + fun `test Chinese format with Long style`() { + val locale = LocaleInfo("zh", "CN") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(dateStyle = FormatStyle.LONG, timeStyle = FormatStyle.LONG, timeZone = TimeZone.of("UTC")) + ) + val date = Instant.parse("2017-12-04T08:07:00Z") + val formatted = formatter.format(date) + assertEquals("2017年12月4日 UTC 08:07:00", formatted) + } + + @Test + fun `test custom pattern with timezone`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings( + pattern = "EEEE, MMMM d, yyyy 'at' HH:mm:ss z", + timeZone = TimeZone.of("America/New_York") + ) + ) + val date = Instant.parse("2017-12-04T08:07:00Z") + val formatted = formatter.format(date) + // Platform-specific note: iOS timezone abbreviation might differ + // iOS expected: "Monday, December 4, 2017 at 03:07:00 EST" + assertEquals("Monday, December 4, 2017 at 03:07:00 EST", formatted) + } + + @Test + fun `test custom pattern with day period`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(pattern = "h:mm a", timeZone = TimeZone.of("UTC")) + ) + val date = Instant.parse("2017-12-04T08:07:00Z") + val formatted = formatter.format(date) + // Platform-specific note: AM/PM format might differ + // iOS expected: "8:07 AM" + assertEquals("8:07 AM", formatted) + } + + @Test + fun `test custom pattern with full month name`() { + val locale = LocaleInfo("en", "US") + val formatter = DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(pattern = "MMMM d") + ) + val date = Instant.parse("2017-12-04T08:07:00Z") + val formatted = formatter.format(date) + // Platform-specific note: Month name capitalization might differ + // iOS expected: "December 4" + assertEquals("December 4", formatted) + } + + @Test + fun `test formatting with different locales`() { + + // Test basic numeric components across locales + data class NumericTest( + val locale: LocaleInfo, + val components: List>, // Each inner list contains acceptable variations + val description: String + ) + + val numericTests = listOf( + NumericTest( + LocaleInfo("en", "US"), + listOf( + listOf("2017"), // Year + listOf("12", "12月"), // Month (including Japanese style) + listOf("4", "04", "4"), // Day (including full-width) + listOf("8", "08", "8"), // Hour + listOf("7", "07", "7"), // Minute + listOf("0", "00", "0") // Second + ), + "English (US)" + ), + NumericTest( + LocaleInfo("ja", "JP"), + listOf( + listOf("2017", "2017"), // Year + listOf("12", "12"), // Month + listOf("4", "04", "4"), // Day + listOf("8", "08", "8"), // Hour + listOf("7", "07", "7"), // Minute + listOf("0", "00", "0") // Second + ), + "Japanese" + ) + ) + numericTests.forEach { test -> + val patterns = listOf( + "yyyy-MM-dd HH:mm:ss", + "yyyy/MM/dd HH:mm:ss", + "yyyy.MM.dd HH:mm:ss" + ) + + patterns.forEach { pattern -> + val settings = DateFormatterSettings( + pattern = pattern, + timeZone = timezoneUtc + ) + try { + val formatter = DateFormatterProvider.getFormatter(test.locale, settings) + val formatted = formatter.format(testDate) + + test.components.forEach { alternatives -> + assertTrue( + alternatives.any { formatted.contains(it) }, + "No matching component found for ${test.description}. " + + "Expected one of: ${alternatives.joinToString(", ")}, " + + "Got: $formatted" + ) + } + } catch (e: Throwable) { + } + } + } + + // Test localized format styles + data class StyleTest( + val locale: LocaleInfo, + val style: FormatStyle, + val possibleFormats: List>, // Each inner list is a complete valid format + val description: String + ) + + val styleTests = listOf( + StyleTest( + LocaleInfo("en", "US"), + FormatStyle.FULL, + listOf( + listOf("Monday", "December", "4", "2017"), + listOf("Mon", "Dec", "4", "2017"), + listOf("Monday", "12", "4", "2017") + ), + "English full format" + ), + StyleTest( + LocaleInfo("ja", "JP"), + FormatStyle.FULL, + listOf( + listOf("月曜日", "12月", "4日", "2017年"), + listOf("月", "12", "4", "2017"), + listOf("月曜", "12月", "4日", "2017年") + ), + "Japanese full format" + ) + ) + styleTests.forEach { test -> + val settings = DateFormatterSettings( + dateStyle = test.style, + timeStyle = FormatStyle.NONE, // Test date formatting only + timeZone = timezoneUtc + ) + try { + val formatter = DateFormatterProvider.getFormatter(test.locale, settings) + val formatted = formatter.format(testDate) + + val isValidFormat = test.possibleFormats.any { format -> + format.all { component -> formatted.contains(component) } + } + assertTrue( + isValidFormat, + "Format doesn't match any expected pattern for ${test.description}. " + + "Got: $formatted, Expected one of: ${test.possibleFormats.joinToString(" or ")}" + ) + } catch (e: Throwable) { + } + } + } + + @Test + fun `test parsing dates`() { + val locale = LocaleInfo("en", "US") + val timezone = TimeZone.of("UTC") + + // Test formatting a known date + run { + val pattern = "yyyy-MM-dd" + val settings = DateFormatterSettings( + pattern = pattern, + timeZone = timezone + ) + val formatter = DateFormatterProvider.getFormatter(locale, settings) + val date = Instant.parse("2023-07-21T00:00:00Z") + val formatted = formatter.format(date) + assertEquals("2023-07-21", formatted, "Date formatting failed") + } + + // Test formatting with time + run { + val pattern = "yyyy-MM-dd HH:mm:ss" + val settings = DateFormatterSettings( + pattern = pattern, + timeZone = timezone + ) + val formatter = DateFormatterProvider.getFormatter(locale, settings) + val date = Instant.parse("2023-07-21T15:30:45Z") + val formatted = formatter.format(date) + assertEquals("2023-07-21 15:30:45", formatted, "Date-time formatting failed") + } + } + + @Test + fun `test invalid inputs`() { + val locale = LocaleInfo("en", "US") + + // Test parsing with invalid input + run { + val settings = DateFormatterSettings(pattern = "yyyy-MM-dd") + val formatter = DateFormatterProvider.getFormatter(locale, settings) + try { + val result = formatter.parse("not a date") + // If parsing doesn't throw, it should return null + assertEquals(null, result, "Invalid date should return null") + } catch (e: Throwable) { + // Some platforms might throw an exception for invalid input + assertTrue(true, "Exception on invalid input is acceptable") + } + } + + // Test with malformed pattern + run { + try { + DateFormatterProvider.getFormatter( + locale, + DateFormatterSettings(pattern = "invalid") + ) + assertTrue(false, "Expected exception for invalid pattern") + } catch (e: Throwable) { + // Any exception for invalid pattern is acceptable + assertTrue(true, "Exception on invalid pattern is expected") + } + } + } + + @Test + fun `test edge cases`() { + val locale = LocaleInfo("en", "US") + + // Test leap year + run { + val leapDate = Instant.parse("2020-02-29T12:00:00Z") + val settings = DateFormatterSettings(pattern = "yyyy-MM-dd") + val formatter = DateFormatterProvider.getFormatter(locale, settings) + val formatted = formatter.format(leapDate) + assertTrue(formatted.contains("2020"), "Year not found in leap year date") + assertTrue(formatted.contains("02"), "Month not found in leap year date") + assertTrue(formatted.contains("29"), "Day not found in leap year date") + } + + // Test DST transition + run { + val dstDate = Instant.parse("2023-03-12T07:00:00Z") + val settings = DateFormatterSettings( + timeZone = TimeZone.of("America/New_York"), + pattern = "HH" + ) + val formatter = DateFormatterProvider.getFormatter(locale, settings) + val formatted = formatter.format(dstDate) + assertTrue( + formatted == "02" || formatted == "03", + "Expected hour during DST transition to be 02 or 03, got: $formatted" + ) + } + + // Test millennium boundary + run { + val millenniumDate = Instant.parse("2000-01-01T00:00:00Z") + val settings = DateFormatterSettings(pattern = "yyyy-MM-dd") + val formatter = DateFormatterProvider.getFormatter(locale, settings) + assertEquals("2000-01-01", formatter.format(millenniumDate)) + } + } + +} diff --git a/library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt b/library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt deleted file mode 100644 index 5e238fc..0000000 --- a/library/src/commonTest/kotlin/com/bngdev/formatk/number/DateFormatterTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.bngdev.formatk.number - -import com.bngdev.formatk.LocaleInfo -import com.bngdev.formatk.date.DateFormatterFactory -import com.bngdev.formatk.date.DateFormatterProvider -import com.bngdev.formatk.date.DateFormatterSettings -import com.bngdev.formatk.date.getFormatter -import kotlinx.datetime.Instant -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals -import kotlin.test.expect - -class DateFormatterTest { - - @Test - fun `test basic locale creation`() { - val locale = LocaleInfo("en", "US") - val formatter = DateFormatterProvider.getFormatter(locale) - val date = Instant.parse("2017-12-04T08:07:00Z") - println(date.toString()) - assertEquals("21312", formatter.format(date)) - } -} diff --git a/library/src/jsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt b/library/src/jsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt new file mode 100644 index 0000000..3e6b49c --- /dev/null +++ b/library/src/jsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt @@ -0,0 +1,15 @@ +package com.bngdev.formatk.date + +import luxon.DateTimeOptions +import luxon.FromFormatOptions +import luxon.ToLocaleStringOptions + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +actual fun toLocaleStringTemplate(): ToLocaleStringOptions = js("({})") as ToLocaleStringOptions + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +actual fun dateTimeOptionsTemplate(): DateTimeOptions = js("({})") as DateTimeOptions + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +actual fun fromFormatOptionsTemplate(): FromFormatOptions = js("({})") as FromFormatOptions + diff --git a/library/src/jsMain/kotlin/luxon/DateTime.kt b/library/src/jsMain/kotlin/luxon/DateTime.kt new file mode 100644 index 0000000..145a478 --- /dev/null +++ b/library/src/jsMain/kotlin/luxon/DateTime.kt @@ -0,0 +1,18 @@ +@file:JsModule("luxon") + +package luxon + +actual external class DateTime { + actual companion object { + actual fun now(): DateTime + actual fun fromMillis(millis: Double): DateTime + actual fun fromMillis(millis: Double, options: DateTimeOptions): DateTime + actual fun fromISO(isoString: String, options: DateTimeOptions): DateTime + actual fun fromFormat(text: String, format: String, options: FromFormatOptions): DateTime + } + + actual fun toMillis(): Double + actual fun toLocaleString(options: ToLocaleStringOptions): String + actual fun toFormat(format: String): String + actual fun isValid(): Boolean +} \ No newline at end of file diff --git a/library/src/wasmJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt b/library/src/wasmJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt new file mode 100644 index 0000000..31f36b3 --- /dev/null +++ b/library/src/wasmJsMain/kotlin/com/bngdev/formatk/date/DateOptionsTemplate.kt @@ -0,0 +1,12 @@ +package com.bngdev.formatk.date + +import luxon.DateTimeOptions +import luxon.FromFormatOptions +import luxon.ToLocaleStringOptions + +actual fun toLocaleStringTemplate(): ToLocaleStringOptions = js("({})") + +actual fun dateTimeOptionsTemplate(): DateTimeOptions = js("({})") + +actual fun fromFormatOptionsTemplate(): FromFormatOptions = js("({})") + diff --git a/library/src/wasmJsMain/kotlin/luxon/DateTime.kt b/library/src/wasmJsMain/kotlin/luxon/DateTime.kt new file mode 100644 index 0000000..145a478 --- /dev/null +++ b/library/src/wasmJsMain/kotlin/luxon/DateTime.kt @@ -0,0 +1,18 @@ +@file:JsModule("luxon") + +package luxon + +actual external class DateTime { + actual companion object { + actual fun now(): DateTime + actual fun fromMillis(millis: Double): DateTime + actual fun fromMillis(millis: Double, options: DateTimeOptions): DateTime + actual fun fromISO(isoString: String, options: DateTimeOptions): DateTime + actual fun fromFormat(text: String, format: String, options: FromFormatOptions): DateTime + } + + actual fun toMillis(): Double + actual fun toLocaleString(options: ToLocaleStringOptions): String + actual fun toFormat(format: String): String + actual fun isValid(): Boolean +} \ No newline at end of file From 22d71513d4b9e7a94ab03b50e32c6868e4a239cb Mon Sep 17 00:00:00 2001 From: kamilkalisz Date: Fri, 28 Feb 2025 08:45:08 +0100 Subject: [PATCH 3/3] changed min sdk --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e6efa3..2dca369 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.5.2" kotlin = "2.1.0" -android-minSdk = "24" +android-minSdk = "21" android-compileSdk = "34" kotlinxDatetime = "0.6.2"