From 00974e5d3dd9ba51f675999b6a0a2ecf2ccf1982 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 09:28:22 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../core/ui/utils/DateTimeFormatters.kt | 19 ++- .../core/ui/utils/DateTimeFormattersTest.kt | 143 ++++++++++++++++++ .../feed/ui/utils/CreatedTimeFormatter.kt | 11 +- 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 95ffc567c2..f460a6f129 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -103,9 +103,9 @@ object DateTimeFormatters { */ val localFullDate: DateTimeFormatter by lazy { val locale = Locale.getDefault() - val datePattern = DateFormat.getBestDateTimePattern(locale, "d MMMM") + val datePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, "d MMMM")) val timeSkeleton = if (is12HourFormat) "h:mm a" else "HH:mm" - val timePattern = DateFormat.getBestDateTimePattern(locale, timeSkeleton) + val timePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, timeSkeleton)) val fullPattern = "$datePattern, $timePattern" DateTimeFormatterBuilder() .appendPattern(fullPattern) @@ -128,13 +128,26 @@ object DateTimeFormatters { */ fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter { val skeletonWithLocale = skeleton.replaceHourLetters() + val icuPattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale) + val jodaPattern = icuPatternToJodaPattern(icuPattern) return DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale)) + .appendPattern(jodaPattern) .toFormatter() .withLocale(Locale.getDefault()) } + /** + * Converts ICU date/time pattern (from [DateFormat.getBestDateTimePattern]) to Joda-Time compatible pattern. + */ + internal fun icuPatternToJodaPattern(icuPattern: String): String { + return icuPattern + .replace("LLLL", "MMMM") + .replace("LLL", "MMM") + .replace("LL", "MM") + .replace("L", "M") + } + private fun String.replaceHourLetters(): String { return if (is12HourFormat) { this.replace('H', 'h').replace('k', 'K') diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt new file mode 100644 index 0000000000..7fa8bcd9ce --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.utils + +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** + * Unit tests for [DateTimeFormatters], in particular for conversion of ICU date/time patterns + * to Joda-Time compatible patterns. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DateTimeFormattersTest { + + @Test + fun `converts LLLL to MMMM - full standalone month pattern that crashes on Chinese locale`() { + // Arrange + val icuPattern = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `converts LLL to MMM - short standalone month`() { + // Arrange + val icuPattern = "dd LLL yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd MMM yyyy") + } + + @Test + fun `converts LL to MM - numeric standalone month`() { + // Arrange + val icuPattern = "yyyy-MM-LL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("yyyy-MM-MM") + } + + @Test + fun `converts single L to M`() { + // Arrange + val icuPattern = "d/L/yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d/M/yyyy") + } + + @Test + fun `leaves pattern without L unchanged`() { + // Arrange + val icuPattern = "dd.MM.yyyy HH:mm" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd.MM.yyyy HH:mm") + } + + @Test + fun `leaves pattern with M unchanged`() { + // Arrange + val icuPattern = "d MMMM yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM yyyy") + } + + @Test + fun `handles mixed ICU pattern as returned for Chinese locale - d MMMM`() { + // Arrange + val icuPatternWithStandaloneMonth = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPatternWithStandaloneMonth) + + // Assert — Joda-Time can parse and format this without IllegalArgumentException + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `handles empty string`() { + // Arrange + val icuPattern = "" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `handles pattern with only literal characters`() { + // Arrange + val icuPattern = " 'at' " + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo(" 'at' ") + } + + @Test + fun `getBestFormatterBySkeleton with d MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("d MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } + + @Test + fun `getBestFormatterBySkeleton with dd MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt index b1052994f0..ef92d71b3c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt @@ -10,10 +10,13 @@ import com.tangem.utils.StringsSigns import org.joda.time.DateTime internal fun mapFormattedDate(createdAt: String): TextReference { - val formattedDate = getFormattedDate( - createdAt = createdAt, - now = DateTime.now(), - ) + val formattedDate = runCatching { + getFormattedDate( + createdAt = createdAt, + now = DateTime.now(), + ) + }.getOrElse { FormattedDate.FullDate("") } + return when (formattedDate) { is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date) is FormattedDate.HoursAgo -> TextReference.PluralRes(