kotlindatedatetimedatetimeformatterrome

DateTimeFormatter error for some users only


I have this weird situation where my function using DateTimeFormatter works perfectly for me (i.e. the list of rss feeds is sorted in the order that it was published), but it crashes for some users of my app. At first, I thought maybe there was a locale related problem but it seems as if all rss feeds use the locale.English format, so now I am out of ideas. I also have reports of crashes from users with American English set on their device (I use UK English). Is there something wrong with my code or another possible reason?

I use Rome parser and pubDates are parsed in this way: Sat Aug 12 12:51:34 GMT+01:00 2023

    suspend fun sortDateTimeAndSaveLatestHeadline() {
    val dataStore = AppDataStore(getApplication())
    val listOfStringDates: MutableList<String> = mutableListOf()
    val listOfHeadlines: MutableList<String> = mutableListOf()
    val listOfFeedTitles: MutableList<String> = mutableListOf()

    newsFeed.value?.forEach { feed ->
        listOfStringDates.add(feed.feedItem.pubDate)
        listOfHeadlines.add(feed.feedItem.title)
        listOfFeedTitles.add(feed.feedTitle)
    }

    val zippedLists = listOfStringDates.zip(listOfHeadlines)
        .zip(listOfFeedTitles) { (a, b), c -> Triple(a, b, c) }

    val dateTimeFormatter: DateTimeFormatter =
        DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss O yyyy",  Locale.ENGLISH)

    val result = zippedLists.sortedByDescending {
        LocalDateTime.parse(it.first, dateTimeFormatter)
    }

    if (result.isNotEmpty()) {
        dataStore.saveTileHeadline(result[0].second)
        dataStore.saveFeedTitle(result[0].third)
        println("Headline = ${result[0].second}")
    }
}

And this is the stack trace:

  Exception java.time.format.DateTimeParseException:
  at java.time.format.DateTimeFormatter.parseResolved0 (DateTimeFormatter.java:1949)
  at java.time.format.DateTimeFormatter.parse (DateTimeFormatter.java:1851)
  at java.time.LocalDateTime.parse (LocalDateTime.java:486)
  at java.util.TimSort.countRunAndMakeAscending (TimSort.java:355)
  at java.util.TimSort.sort (TimSort.java:220)
  at java.util.Arrays.sort (Arrays.java:1424)
  at kotlin.collections.CollectionsKt___CollectionsKt.sortedWith (CollectionsKt___Collections.kt)
  at com.strangerweather.news.presentation.screens.screens.FeedItemsScreenKt$FeedItemsScreen$7$1.invoke (FeedItemsScreen.kt)
  at androidx.wear.compose.foundation.lazy.ScalingLazyColumnKt$ScalingLazyColumn$1$1$2$1.invoke (ScalingLazyColumn.kt)
  at androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent.<init> (LazyLayoutIntervalContent.java)
  at androidx.compose.foundation.lazy.LazyListIntervalContent.<init> (LazyListIntervalContent.java)
  at androidx.compose.runtime.snapshots.Snapshot$Companion.observe (Snapshot.java)
  at androidx.compose.runtime.DerivedSnapshotState.currentRecord (DerivedSnapshotState.java)
  at androidx.compose.runtime.snapshots.SnapshotKt.getLock (Snapshot.kt)
  at androidx.compose.runtime.DerivedSnapshotState$ResultRecord.readableHash (DerivedSnapshotState.java)
  at androidx.compose.runtime.snapshots.SnapshotKt.getLock (Snapshot.kt)
  at androidx.compose.runtime.DerivedSnapshotState$ResultRecord.isValid (DerivedSnapshotState.java)
  at androidx.compose.runtime.DerivedSnapshotState.currentRecord (DerivedSnapshotState.java)
  at androidx.compose.runtime.DerivedSnapshotState.getCurrentRecord (DerivedSnapshotState.java)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.recordInvalidation (SnapshotStateObserver.java)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver.drainChanges (SnapshotStateObserver.java)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver.access$drainChanges (SnapshotStateObserver.java)
  at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot (Snapshot.kt)
  at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot (Snapshot.kt)
  at androidx.compose.runtime.snapshots.SnapshotKt.access$advanceGlobalSnapshot (Snapshot.kt)
  at androidx.compose.runtime.snapshots.GlobalSnapshot.notifyObjectsInitialized$runtime_release (GlobalSnapshot.java)
  at androidx.compose.runtime.DerivedSnapshotState.currentRecord (DerivedSnapshotState.java)
  at androidx.compose.runtime.snapshots.Snapshot$Companion.getCurrent (Snapshot.java)
  at androidx.compose.runtime.DerivedSnapshotState.getValue (DerivedSnapshotState.java)
  at androidx.wear.compose.foundation.lazy.ScalingLazyListState.getLayoutInfo (ScalingLazyListState.java)
  at androidx.wear.compose.foundation.lazy.ScalingLazyListState.scrollToItem$compose_foundation_release (ScalingLazyListState.java)
  at androidx.wear.compose.foundation.lazy.ScalingLazyListState.scrollToInitialItem$compose_foundation_release (ScalingLazyListState.java)
  at androidx.wear.compose.foundation.lazy.ScalingLazyColumnKt$ScalingLazyColumn$1$1$3$1.invokeSuspend (ScalingLazyColumn.kt)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (BaseContinuationImpl.java)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.java)
  at androidx.compose.ui.platform.AndroidUiDispatcher.nextTask (AndroidUiDispatcher.java)
  at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch (AndroidUiDispatcher.java)
  at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch (AndroidUiDispatcher.java)
  at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run (AndroidUiDispatcher.java)
  at android.os.Handler.handleCallback (Handler.java:938)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:246)
  at android.app.ActivityThread.main (ActivityThread.java:7690)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:593)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:995)

Solution

  • Update:

    Thanks to Ole V.V. for this update.

    DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ROOT) parses both of the example strings in the original answer just fine.

    Online Demo

    Original answer:

    From the description you have provided in the code and in the comment, it looks like your application is getting timezone information in different forms e.g. GMT+01:00, EDT etc. Therefore, you should use multiple optional patterns for time zones.

    Demo:

    public class Main {
        public static void main(String[] args) {
            DateTimeFormatter dtf = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss [O][z] uuuu", Locale.ENGLISH);
    
            // Test
            Stream.of(
                    "Sat Aug 12 12:51:34 GMT+01:00 2023",
                    "Fri Aug 18 12:34:50 EDT 2023"
            ).forEach(s -> System.out.println(ZonedDateTime.parse(s, dtf)));
        }
    }
    

    Output:

    2023-08-12T12:51:34+01:00
    2023-08-18T12:34:50-04:00[America/New_York]
    

    Online Demo

    Notes:

    1. I have used only two optional patterns for the demo. You can extend it further as per your requirement e.g. "EEE MMM dd HH:mm:ss [O][z][VV] uuuu" has three optional patterns.
    2. Here, you can use y instead of u but I prefer u to y.