diff --git a/core/base/src/androidMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.android.kt b/core/base/src/androidMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.android.kt index 66b310e60..617413e75 100644 --- a/core/base/src/androidMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.android.kt +++ b/core/base/src/androidMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.android.kt @@ -22,32 +22,13 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField import java.time.temporal.UnsupportedTemporalTypeException +import java.util.Locale import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toJavaZoneId import kotlinx.datetime.toLocalDateTime -private val dateFormatters = - listOf( - DateTimeFormatter.ofPattern("E, d MMM yyyy HH:mm:ss O"), - DateTimeFormatter.ofPattern("E, d MMM yyyy HH:mm:ss Z"), - DateTimeFormatter.ofPattern("E, d MMM yyyy HH:mm:ss z"), - DateTimeFormatter.ofPattern("E, d MMM yyyy HH:mm Z"), - DateTimeFormatter.ofPattern("E, d MMM yy HH:mm:ss Z"), - DateTimeFormatter.ofPattern("E, dd MMM yyyy"), - DateTimeFormatter.ofPattern("d MMM yyyy HH:mm:ss z"), - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssz"), - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"), - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"), - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"), - DateTimeFormatter.ofPattern("yyyy-MM-dd"), - DateTimeFormatter.ofPattern("MM-dd HH:mm:ss"), - DateTimeFormatter.ofPattern("E, d MMM yyyy HH:mm:ss zzzz"), - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), - ) - @Throws(DateTimeFormatException::class) actual fun String?.dateStringToEpochMillis(clock: Clock): Long? { if (this.isNullOrBlank()) return null @@ -55,13 +36,15 @@ actual fun String?.dateStringToEpochMillis(clock: Clock): Long? { val currentDate = clock.now().toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() - for (dateFormatter in dateFormatters) { + for (pattern in dateFormatterPatterns) { + val dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.US) + try { - val parsedValue = parseToInstant(dateFormatter, this) + val parsedValue = parseToInstant(dateTimeFormatter, this) return parsedValue.toEpochMilli() } catch (e: Exception) { try { - val parsedValue = fallbackParseToInstant(currentDate, dateFormatter, this) + val parsedValue = fallbackParseToInstant(currentDate, dateTimeFormatter, this) return parsedValue.toEpochMilli() } catch (e: Exception) { // no-op diff --git a/core/base/src/commonMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.kt b/core/base/src/commonMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.kt index 9ff9c8523..833c7a1c8 100644 --- a/core/base/src/commonMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.kt +++ b/core/base/src/commonMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.kt @@ -18,6 +18,30 @@ package dev.sasikanth.rss.reader.util import kotlinx.datetime.Clock +internal val dateFormatterPatterns = + setOf( + // Keep the two character year before parsing the four + // character year of similar pattern. Not sure why, + // but unlike JVM, iOS is not keep it strict? + "E, d MMM yy HH:mm:ss Z", + "E, d MMM yyyy HH:mm:ss O", + "E, d MMM yyyy HH:mm:ss Z", + "E, d MMM yyyy HH:mm:ss z", + "E, d MMM yyyy HH:mm Z", + "E, dd MMM yyyy", + "d MMM yyyy HH:mm:ss z", + "dd MMM yyyy HH:mm Z", + "yyyy-MM-dd'T'HH:mm:ssz", + "yyyy-MM-dd'T'HH:mm:ssZ", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss z", + "yyyy-MM-dd", + "MM-dd HH:mm:ss", + "E, d MMM yyyy HH:mm:ss zzzz", + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + ) + expect fun String?.dateStringToEpochMillis(clock: Clock = Clock.System): Long? data class DateTimeFormatException(val exception: Exception) : Exception() diff --git a/core/base/src/commonTest/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormattersTest.kt b/core/base/src/commonTest/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormattersTest.kt index b20f05fad..ccd2938b9 100644 --- a/core/base/src/commonTest/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormattersTest.kt +++ b/core/base/src/commonTest/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormattersTest.kt @@ -43,7 +43,9 @@ class DateTimeFormattersTest { "Fri, 24 Nov 2023 23:13:00 GMT", "Tue, 05 Dec 2023 15:55:50 PST", "2023-12-12T11:20:18", - "2023-12-10T06:11:00.000-08:00" + "2023-12-10T06:11:00.000-08:00", + "01 Jun 2024 12:00 +0000", + "Thu, 26 Sep 2024 14:30:00 +0200", ) val expectedEpochMillis = @@ -65,7 +67,9 @@ class DateTimeFormattersTest { 1700867580000, 1701820550000, 1702380018000, - 1702217460000 + 1702217460000, + 1717243200000, + 1727353800000 ) // when diff --git a/core/base/src/iosMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.ios.kt b/core/base/src/iosMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.ios.kt index 02a3827ec..e6aa4e192 100644 --- a/core/base/src/iosMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.ios.kt +++ b/core/base/src/iosMain/kotlin/dev/sasikanth/rss/reader/util/DateTimeFormatters.ios.kt @@ -33,49 +33,26 @@ import platform.Foundation.NSDateFormatter import platform.Foundation.NSLocale import platform.Foundation.timeIntervalSince1970 -private fun createDateFormatter( - pattern: String, - timeZone: TimeZone? = null, -): NSDateFormatter { - return NSDateFormatter().apply { - dateFormat = pattern - locale = NSLocale("en_US_POSIX") - - timeZone?.let { this.timeZone = it.toNSTimeZone() } - } -} - -private val dateFormatters = - listOf( - // Keep the two character year before parsing the four - // character year of similar pattern. Not sure why, - // but unlike JVM, iOS is not keep it strict? - createDateFormatter("E, d MMM yy HH:mm:ss Z"), - createDateFormatter("E, d MMM yyyy HH:mm:ss O"), - createDateFormatter("E, d MMM yyyy HH:mm:ss Z"), - createDateFormatter("E, d MMM yyyy HH:mm:ss z"), - createDateFormatter("E, d MMM yyyy HH:mm Z"), - createDateFormatter("E, dd MMM yyyy", TimeZone.UTC), - createDateFormatter("d MMM yyyy HH:mm:ss z"), - createDateFormatter("MM-dd HH:mm:ss", TimeZone.UTC), - createDateFormatter("yyyy-MM-dd'T'HH:mm:ssz"), - createDateFormatter("yyyy-MM-dd'T'HH:mm:ssZ"), - createDateFormatter("yyyy-MM-dd'T'HH:mm:ss", TimeZone.UTC), - createDateFormatter("yyyy-MM-dd HH:mm:ss", TimeZone.UTC), - createDateFormatter("yyyy-MM-dd HH:mm:ss z"), - createDateFormatter("yyyy-MM-dd", TimeZone.UTC), - createDateFormatter("E, d MMM yyyy HH:mm:ss zzzz"), - createDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), - ) - @Throws(DateTimeFormatException::class) actual fun String?.dateStringToEpochMillis(clock: Clock): Long? { if (this.isNullOrBlank()) return null try { val date = - dateFormatters.firstNotNullOfOrNull { dateFormatter -> - dateFormatter.dateFromString(this.trim()) + dateFormatterPatterns.firstNotNullOfOrNull { pattern -> + val timeZone = + if (hasTimeZonePattern(pattern)) { + null + } else { + TimeZone.UTC + } + val dateTimeFormatter = + createDateFormatter( + pattern = pattern, + timeZone = timeZone, + ) + + dateTimeFormatter.dateFromString(this.trim()) } if (date != null) { @@ -114,3 +91,20 @@ actual fun String?.dateStringToEpochMillis(clock: Clock): Long? { return null } + +private fun hasTimeZonePattern(pattern: String) = + pattern.contains("Z", ignoreCase = true) || + pattern.contains("O", ignoreCase = true) || + pattern.contains("X", ignoreCase = true) + +private fun createDateFormatter( + pattern: String, + timeZone: TimeZone? = null, +): NSDateFormatter { + return NSDateFormatter().apply { + dateFormat = pattern + locale = NSLocale("en_US_POSIX") + + timeZone?.let { this.timeZone = it.toNSTimeZone() } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fe58ceba..70d9aa711 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.0.20" -android_gradle_plugin = "8.6.1" +android_gradle_plugin = "8.7.0" compose = "1.7.0-rc01" android_sdk_compile = "34" diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt index 4522fff8d..d5738313d 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/home/HomePresenter.kt @@ -45,9 +45,11 @@ import dev.sasikanth.rss.reader.util.DispatchersProvider import dev.sasikanth.rss.reader.utils.NTuple4 import dev.sasikanth.rss.reader.utils.getLast24HourStart import dev.sasikanth.rss.reader.utils.getTodayStartInstant +import kotlin.time.Duration.Companion.milliseconds import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.* @@ -377,6 +379,7 @@ class HomePresenter( .launchIn(coroutineScope) } + @OptIn(FlowPreview::class) private fun loadFeaturedPostsItems( activeSource: Source?, unreadOnly: Boolean?, @@ -400,6 +403,7 @@ class HomePresenter( ) } } + .debounce(500.milliseconds) private fun feedsSheetStateChanged(feedsSheetState: SheetValue) { _state.update {