From a13f6012a94c44405b9fabfe8a277535b1085f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=96=D0=B8=D1=80=D0=BA=D0=B5=D0=B2=D0=B8=D1=87?= Date: Thu, 23 May 2024 11:55:30 +0300 Subject: [PATCH 1/3] datetime 0.6.0, safe parsing --- .../material3/KotlinxDatetimeCalendarModel.kt | 4 +- .../KotlinxDatetimeCalendarModelTest.kt | 9 +++ .../material3/PlatformDateFormat.jsWasm.kt | 80 ++++++++----------- gradle/libs.versions.toml | 2 +- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt index 1320bb15abe51..72cd5a6792eb7 100644 --- a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt +++ b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt @@ -123,7 +123,9 @@ internal class KotlinxDatetimeCalendarModel(locale: CalendarLocale) : CalendarMo } override fun parse(date: String, pattern: String): CalendarDate? { - return platformDateFormat.parse(date, pattern) + return kotlin.runCatching { + platformDateFormat.parse(date, pattern) + }.getOrNull() } private fun Instant.toCalendarMonth( diff --git a/compose/material3/material3/src/skikoTest/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModelTest.kt b/compose/material3/material3/src/skikoTest/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModelTest.kt index 4fe15cf4a31ed..815748e4de73f 100644 --- a/compose/material3/material3/src/skikoTest/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModelTest.kt +++ b/compose/material3/material3/src/skikoTest/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModelTest.kt @@ -413,6 +413,15 @@ internal class KotlinxDatetimeCalendarModelTest { assertThat(model.getDateInputFormat(locale).patternWithoutDelimiters).isEqualTo("ddMMyyyy") assertThat(model.getDateInputFormat(locale).delimiter).isEqualTo('-') } + + @Test + fun illegalDateParsingDoesNotThrowException(){ + val model = KotlinxDatetimeCalendarModel(calendarLocale("en","US")) + + assertThat(model.parse("50-50-2000","MM-dd-yyyy")).isEqualTo(null) + assertThat(model.parse("50-50-2000","")).isEqualTo(null) + assertThat(model.parse("","MM-dd-yyyy")).isEqualTo(null) + } } internal const val January2022Millis = 1640995200000 diff --git a/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt b/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt index 5d75e35b165da..c39f8d9a2ee1d 100644 --- a/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt +++ b/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt @@ -19,8 +19,12 @@ package androidx.compose.material3 import androidx.compose.ui.text.intl.Locale import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.atTime +import kotlinx.datetime.format +import kotlinx.datetime.format.FormatStringsInDatetimeFormats +import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime @@ -44,7 +48,8 @@ internal actual class PlatformDateFormat actual constructor(private val locale: listOf("AE", "AG", "AL", "AS", "AU", "BB", "BD", "BH", "BM", "BN", "BS", "BT", "CA", "CN", "CO", "CY", "DJ", "DM", "DO", "DZ", "EG", "EH", "ER", "ET", "FJ", "FM", "GD", "GH", "GM", "GR", "GU", "GY", "HK", "IN", "IQ", "JM", "JO", "KH", "KI", "KN", "KP", "KR", "KW", "KY", "LB", "LC", "LR", "LS", "LY", "MH", "MO", "MP", "MR", "MW", "MY", "NA", "NZ", "OM", "PA", "PG", "PH", "PK", "PR", "PS", "PW", "QA", "SA", "SB", "SD", "SG", "SL", "SO", "SS", "SY", "SZ", "TC", "TD", "TN", "TO", "TT", "TW", "UM", "US", "VC", "VE", "VG", "VI", "VU", "WS", "YE", "ZM") } - //TODO: replace formatting with kotlinx datetime when supported (see https://github.com/Kotlin/kotlinx-datetime/pull/251) + //TODO: replace manual locale-aware formatting with kotlinx-datetime when supported + @OptIn(FormatStringsInDatetimeFormats::class) actual fun formatWithPattern( utcTimeMillis: Long, pattern: String, @@ -56,33 +61,31 @@ internal actual class PlatformDateFormat actual constructor(private val locale: val jsDate = Date(utcTimeMillis.toDouble()) - val monthShort = jsDate.toLocaleDateString( - locales = locale.toLanguageTag(), - options = dateLocaleOptions { - month = SHORT - }) + val (monthShort, monthLong) = listOf(SHORT, LONG).map { + jsDate.toLocaleDateString( + locales = locale.toLanguageTag(), + options = dateLocaleOptions { + month = it + } + ) + } - val monthLong = jsDate.toLocaleDateString( - locales = locale.toLanguageTag(), - options = dateLocaleOptions { - month = LONG - }) + val (wdNarrow, wdShort, wdLong) = listOf(NARROW, SHORT, LONG).map { + jsDate.toLocaleDateString( + locales = locale.toLanguageTag(), + options = dateLocaleOptions { + weekday = it + } + ) + } - return pattern - .replace("yyyy", date.year.toString(), ignoreCase = true) - .replace("yy", date.year.toString().takeLast(2), ignoreCase = true) - .replace("MMMM", monthLong) + return date.format( + LocalDateTime.Format { byUnicodePattern(pattern) } + ).replace("MMMM", monthLong) .replace("MMM", monthShort) - .replace("MM", date.monthNumber.toStringWithLeadingZero()) - .replace("M", date.monthNumber.toString()) - .replace("dd", date.dayOfMonth.toStringWithLeadingZero(), ignoreCase = true) - .replace("d", date.dayOfMonth.toString(), ignoreCase = true) - .replace("hh", date.hour.toStringWithLeadingZero(), ignoreCase = true) - .replace("h", date.hour.toString(), ignoreCase = true) - .replace("ii", date.minute.toStringWithLeadingZero(), ignoreCase = true) - .replace("i", date.minute.toString(), ignoreCase = true) - .replace("ss", date.second.toStringWithLeadingZero(), ignoreCase = true) - .replace("s", date.second.toString(), ignoreCase = true) + .replace("EEEE", wdLong) + .replace("EEE", wdShort) + .replace("E", wdNarrow) } @@ -137,37 +140,22 @@ internal actual class PlatformDateFormat actual constructor(private val locale: ) } + @OptIn(FormatStringsInDatetimeFormats::class) actual fun parse( date: String, pattern: String ): CalendarDate? { - val year = parseSegment(date, pattern, "yyyy") - ?: return null - val month = parseSegment(date, pattern, "mm") - ?: parseSegment(date, pattern, "m") - ?: return null - - val day = parseSegment(date, pattern, "dd") - ?: parseSegment(date, pattern, "d") - ?: 1 - - return LocalDate( - year, month, day + return LocalDate.parse( + input = date, + format = LocalDate.Format { + byUnicodePattern(pattern) + } ).atTime(Midnight) .toInstant(TimeZone.UTC) .toCalendarDate(TimeZone.UTC) } - private fun parseSegment(date: String, pattern: String, segmentPattern: String): Int? { - val index = pattern - .indexOf(segmentPattern, ignoreCase = true) - .takeIf { it >= 0 } ?: return null - - return date.substring(index, index + segmentPattern.length) - .toIntOrNull() - } - actual fun getDateInputFormat(): DateInputFormat { val date = Date(year = 2000, month = 10, day = 23) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5950f83149755..0d60db54f975c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ byteBuddy = "1.12.10" asm = "9.3" cmake = "3.22.1" dagger = "2.44" -datetime="0.5.0" +datetime="0.6.0" dexmaker = "2.28.3" dokka = "1.8.10-dev-203" espresso = "3.6.0-alpha01" From b82b195ad1bd6caaa346b182b491defc681e76c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=96=D0=B8=D1=80=D0=BA=D0=B5=D0=B2=D0=B8=D1=87?= Date: Thu, 23 May 2024 14:02:10 +0300 Subject: [PATCH 2/3] do not replace narrow weekday --- .../compose/material3/PlatformDateFormat.jsWasm.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt b/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt index c39f8d9a2ee1d..0416eae5066b1 100644 --- a/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt +++ b/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt @@ -70,7 +70,7 @@ internal actual class PlatformDateFormat actual constructor(private val locale: ) } - val (wdNarrow, wdShort, wdLong) = listOf(NARROW, SHORT, LONG).map { + val (wdShort, wdLong) = listOf(SHORT, LONG).map { jsDate.toLocaleDateString( locales = locale.toLanguageTag(), options = dateLocaleOptions { @@ -79,16 +79,14 @@ internal actual class PlatformDateFormat actual constructor(private val locale: ) } - return date.format( - LocalDateTime.Format { byUnicodePattern(pattern) } - ).replace("MMMM", monthLong) + return date + .format(LocalDateTime.Format { byUnicodePattern(pattern) }) + .replace("MMMM", monthLong) .replace("MMM", monthShort) .replace("EEEE", wdLong) .replace("EEE", wdShort) - .replace("E", wdNarrow) } - actual fun formatWithSkeleton( utcTimeMillis: Long, skeleton: String, From 0a5c0b129b7d0d7bcfef9bd1fc215faa21d95f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=96=D0=B8=D1=80=D0=BA=D0=B5=D0=B2=D0=B8=D1=87?= Date: Thu, 23 May 2024 16:39:23 +0300 Subject: [PATCH 3/3] move exception check to web --- .../material3/KotlinxDatetimeCalendarModel.kt | 4 +--- .../material3/PlatformDateFormat.jsWasm.kt | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt index 72cd5a6792eb7..1320bb15abe51 100644 --- a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt +++ b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/KotlinxDatetimeCalendarModel.kt @@ -123,9 +123,7 @@ internal class KotlinxDatetimeCalendarModel(locale: CalendarLocale) : CalendarMo } override fun parse(date: String, pattern: String): CalendarDate? { - return kotlin.runCatching { - platformDateFormat.parse(date, pattern) - }.getOrNull() + return platformDateFormat.parse(date, pattern) } private fun Instant.toCalendarMonth( diff --git a/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt b/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt index 0416eae5066b1..b6d18b3eddb70 100644 --- a/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt +++ b/compose/material3/material3/src/webCommonW3C/kotlin/androidx/compose/material3/PlatformDateFormat.jsWasm.kt @@ -143,15 +143,18 @@ internal actual class PlatformDateFormat actual constructor(private val locale: date: String, pattern: String ): CalendarDate? { - - return LocalDate.parse( - input = date, - format = LocalDate.Format { - byUnicodePattern(pattern) - } - ).atTime(Midnight) - .toInstant(TimeZone.UTC) - .toCalendarDate(TimeZone.UTC) + return try { + LocalDate.parse( + input = date, + format = LocalDate.Format { + byUnicodePattern(pattern) + } + ).atTime(Midnight) + .toInstant(TimeZone.UTC) + .toCalendarDate(TimeZone.UTC) + } catch (e: Throwable) { + null + } } actual fun getDateInputFormat(): DateInputFormat {