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..2dca369 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,16 @@ [versions] agp = "8.5.2" kotlin = "2.1.0" -android-minSdk = "24" +android-minSdk = "21" 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/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 e91c346..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) @@ -60,16 +64,30 @@ 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 { 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 new file mode 100644 index 0000000..1cd2fa1 --- /dev/null +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatter.kt @@ -0,0 +1,22 @@ +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, +) : DateFormatter { + + override fun format(instant: Instant): String { + val date = instant.toNSDate() + return dateFormatter.stringFromDate(date).normalizeWhitespaces() + } + + 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..5f7cf77 --- /dev/null +++ b/library/src/appleMain/kotlin/com/bngdev/formatk/date/AppleDateFormatterFactory.kt @@ -0,0 +1,55 @@ +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 +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() + + dateFormatter.locale = localeInfo.toNSLocale() + + if (formatOptions.pattern != null) { + dateFormatter.dateFormat = formatOptions.pattern + } else { + dateFormatter.dateStyle = toNSDateFormatterStyle(formatOptions.dateStyle) + dateFormatter.timeStyle = toNSDateFormatterStyle(formatOptions.timeStyle) + } + + formatOptions.timeZone?.let { timeZone -> + dateFormatter.timeZone = timeZone.toNSTimeZone() + } + if(formatOptions.pattern == null){ + dateFormatter.dateFormat = removeQuotedWordsWithSpaceAndQuotes(dateFormatter.dateFormat) + } + + return AppleDateFormatter(dateFormatter) + } + + 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/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..69b9075 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/DateFormatterProvider.commonJs.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 JsDateFormatFactory(locale.toLanguageTag()) + } +} 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/JsDateFormatFactory.kt b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt new file mode 100644 index 0000000..162a6e9 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatFactory.kt @@ -0,0 +1,8 @@ +package com.bngdev.formatk.date; + +class JsDateFormatFactory(private val locale: String) : DateFormatterFactory { + + 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 new file mode 100644 index 0000000..e948bb3 --- /dev/null +++ b/library/src/commonJsMain/kotlin/com/bngdev/formatk/date/JsDateFormatter.kt @@ -0,0 +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, + private val settings: DateFormatterSettings +) : DateFormatter { + + override fun format(instant: Instant): String { + val dateTime = DateTime.fromMillis( + instant.toEpochMilliseconds().toDouble(), createDateTimeOptions() + ) + return if (settings.pattern != null) { + dateTime.toFormat(settings.pattern).normalizeWhitespaces() + } else { + dateTime.toLocaleString(createLocaleOptions()).normalizeWhitespaces() + } + } + + override fun parse(value: String): Instant? { + if(settings.pattern == null){ + return null + } + + val luxonDateTime = try { + DateTime.fromFormat(value, settings.pattern, createFromFormatOptions()) + } catch (e: Throwable) { + return null + } + + return try { + luxonDateTime.toMillis().let { Instant.fromEpochMilliseconds(it.toLong()) } + } catch (e: Throwable) { + null + } + } + + private fun createFromFormatOptions(): FromFormatOptions { + val fromFormatOptions = fromFormatOptionsTemplate() + fromFormatOptions.locale = locale + if (settings.timeZone != null) { + fromFormatOptions.zone = settings.timeZone.id + } + return fromFormatOptions + } + + private fun createDateTimeOptions(): DateTimeOptions { + val dateTimeOptionsTemplate = dateTimeOptionsTemplate() + dateTimeOptionsTemplate.locale = locale + getTimezone(settings)?.let { + dateTimeOptionsTemplate.zone = it + } + return dateTimeOptionsTemplate + } + + 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 + } + + private fun getTimezone(settings: DateFormatterSettings): String? { + if(settings.timeZone == null) { + return null + } + if(settings.timeZone.id == "Z"){ + return "UTC" + } + return settings.timeZone.id + } +} + +private fun FormatStyle.toLuxon(): String? { + return when (this) { + FormatStyle.SHORT -> "short" + FormatStyle.MEDIUM -> "medium" + FormatStyle.LONG -> "long" + FormatStyle.FULL -> "full" + FormatStyle.NONE -> null + } +} 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/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..7f9df15 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/JvmDateFormatter.kt @@ -0,0 +1,25 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.utils.normalizeWhitespaces +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()).normalizeWhitespaces() + } + + 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..023763a --- /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 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.SHORT // fallback, ignored usage + } + } + + private fun getDefaultPattern(settings: DateFormatterSettings, locale: Locale): DateTimeFormatter { + if (settings.pattern != null) return DateTimeFormatter.ofPattern(settings.pattern) + val dateStyle = toJavaFormatStyle(settings.dateStyle) + val timeStyle = toJavaFormatStyle(settings.timeStyle) + 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..32cc826 --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatter.kt @@ -0,0 +1,27 @@ +package com.bngdev.formatk.date + +import com.bngdev.formatk.utils.normalizeWhitespaces +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 { + val date = Date.from(instant.toJavaInstant()) + return simpleDateFormat.format(date).normalizeWhitespaces() + } + + override fun parse(value: String): Instant? { + return try { + val date = simpleDateFormat.parse(value) + 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..448a8bf --- /dev/null +++ b/library/src/commonJvmMain/kotlin/com/bngdev/formatk/date/LegacyJvmDateFormatterFactory.kt @@ -0,0 +1,54 @@ +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 pattern = getDefaultPattern(formatOptions, locale.toLocale()) + + val formatter = SimpleDateFormat(pattern, locale.toLocale()).apply { + timeZone = getZoneId(formatOptions) + } + + return LegacyJvmDateFormatter(formatter) + } + + private fun getZoneId(formatOptions: DateFormatterSettings): TimeZone? { + return formatOptions.timeZone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault() + } + + 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 usage + } + } + + private fun getDefaultPattern(settings: DateFormatterSettings, locale: Locale): String { + if (settings.pattern != null) return settings.pattern + + val dateStyle = toLegacyStyle(settings.dateStyle) + val timeStyle = toLegacyStyle(settings.timeStyle) + 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/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/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