From 9cdfe0c5d6ad2730c2751cf2e7ef4c21cb360d12 Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 9 Jun 2025 13:30:30 -0500 Subject: [PATCH] PM-22502: Format dates and times correctly for locale (#5333) --- .../search/util/SearchTypeDataExtensions.kt | 11 +- .../util/FlightRecorderDataSetExtensions.kt | 14 +- .../accountsecurity/AccountSecurityScreen.kt | 10 +- .../loginapproval/LoginApprovalViewModel.kt | 8 +- .../PendingRequestsViewModel.kt | 8 +- .../util/FlightRecorderDataSetExtensions.kt | 13 +- .../feature/settings/other/OtherViewModel.kt | 17 +- .../PasswordHistoryViewModel.kt | 8 +- .../AddEditSendCustomDateChooser.kt | 11 +- .../feature/send/util/SendDataExtensions.kt | 10 +- .../send/viewsend/util/SendViewExtensions.kt | 11 +- .../addedit/util/CipherViewExtensions.kt | 11 +- .../feature/item/util/CipherViewExtensions.kt | 27 +- .../util/VaultItemListingDataExtensions.kt | 11 +- .../util/FlightRecorderDataSetExtensions.kt | 8 +- .../LoginApprovalViewModelTest.kt | 4 +- .../PendingRequestsViewModelTest.kt | 16 +- .../settings/other/OtherViewModelTest.kt | 4 +- .../PasswordHistoryViewModelTest.kt | 10 +- .../viewsend/util/SendViewExtensionsTest.kt | 4 +- .../feature/item/util/VaultItemTestUtil.kt | 6 +- .../data/util/TemporalAccessorExtensions.kt | 85 +++++- .../util/TemporalAccessorExtensionsTest.kt | 253 ++++++++++++++++-- 23 files changed, 445 insertions(+), 115 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt index a4d9807942..477f2b7c7c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt @@ -4,7 +4,7 @@ package com.x8bit.bitwarden.ui.platform.feature.search.util import androidx.annotation.DrawableRes import com.bitwarden.core.data.repository.util.SpecialCharWithPrecedenceComparator -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.send.SendType import com.bitwarden.send.SendView import com.bitwarden.ui.platform.base.util.removeDiacritics @@ -27,8 +27,7 @@ import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData import java.time.Clock - -private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a" +import java.time.format.FormatStyle /** * Updates a [SearchTypeData] with the given data if necessary. @@ -353,7 +352,11 @@ private fun SendView.toDisplayItem( id = id.orEmpty(), title = name, titleTestTag = "SendNameLabel", - subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock), + subtitle = deletionDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, + clock = clock, + ), subtitleTestTag = "SendDateLabel", iconData = IconData.Local( iconRes = when (type) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/about/util/FlightRecorderDataSetExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/about/util/FlightRecorderDataSetExtensions.kt index db871f6aef..2c0badec99 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/about/util/FlightRecorderDataSetExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/about/util/FlightRecorderDataSetExtensions.kt @@ -1,12 +1,14 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.about.util -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateStyle +import com.bitwarden.core.data.util.toFormattedTimeStyle import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet import java.time.Clock import java.time.Instant +import java.time.format.FormatStyle /** * Creates a properly formatted string indicating when the logging will stop for the active log or @@ -23,7 +25,13 @@ private fun FlightRecorderDataSet.FlightRecorderData.getStopsLoggingString( clock: Clock, ): Text { val completionInstant = Instant.ofEpochMilli(this.startTimeMs + this.durationMs) - val completionDate = completionInstant.toFormattedPattern(pattern = "M/d/yy", clock = clock) - val completionTime = completionInstant.toFormattedPattern(pattern = "h:mm a", clock = clock) + val completionDate = completionInstant.toFormattedDateStyle( + dateStyle = FormatStyle.SHORT, + clock = clock, + ) + val completionTime = completionInstant.toFormattedTimeStyle( + timeStyle = FormatStyle.SHORT, + clock = clock, + ) return R.string.stops_logging_on.asText(completionDate, completionTime) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index fe1c5e8db3..2aaae614a6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -572,13 +572,11 @@ private fun SessionCustomTimeoutRow( cardStyle = CardStyle.Middle(), modifier = modifier, ) { - val formattedTime = LocalTime - .ofSecondOfDay( - vaultTimeoutInMinutes * MINUTES_PER_HOUR.toLong(), - ) - .toFormattedPattern("HH:mm") + Text( - text = formattedTime, + text = LocalTime + .ofSecondOfDay(vaultTimeoutInMinutes * MINUTES_PER_HOUR.toLong()) + .toFormattedPattern(pattern = "HH:mm"), style = BitwardenTheme.typography.labelSmall, color = BitwardenTheme.colorScheme.text.primary, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt index b2b4d7f8c1..d42dca5dc4 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt @@ -5,7 +5,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginap import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import java.time.Clock +import java.time.format.FormatStyle import javax.inject.Inject private const val KEY_STATE = "state" @@ -214,8 +215,9 @@ class LoginApprovalViewModel @Inject constructor( email = email, fingerprint = result.authRequest.fingerprint, ipAddress = result.authRequest.ipAddress, - time = result.authRequest.creationDate.toFormattedPattern( - pattern = "M/d/yy hh:mm a", + time = result.authRequest.creationDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, clock = clock, ), ), diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt index 89b7daf88e..32efced556 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModel.kt @@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pending import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.util.Text import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import java.time.Clock +import java.time.format.FormatStyle import javax.inject.Inject private const val KEY_STATE = "state" @@ -144,8 +145,9 @@ class PendingRequestsViewModel @Inject constructor( PendingRequestsState.ViewState.Content.PendingLoginRequest( fingerprintPhrase = request.fingerprint, platform = request.platform, - timestamp = request.creationDate.toFormattedPattern( - pattern = "M/d/yy hh:mm a", + timestamp = request.creationDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, clock = clock, ), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/util/FlightRecorderDataSetExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/util/FlightRecorderDataSetExtensions.kt index a4c20a8a4b..c1d9faf3fa 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/util/FlightRecorderDataSetExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/util/FlightRecorderDataSetExtensions.kt @@ -1,6 +1,8 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.recordedLogs.util +import com.bitwarden.core.data.util.toFormattedDateStyle import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedTimeStyle import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R @@ -11,6 +13,7 @@ import com.x8bit.bitwarden.ui.platform.util.formatBytes import kotlinx.collections.immutable.toImmutableList import java.time.Clock import java.time.Instant +import java.time.format.FormatStyle import java.time.temporal.ChronoUnit /** @@ -67,14 +70,20 @@ private fun FlightRecorderDataSet.FlightRecorderData.expiresIn(clock: Clock): Te R.string.expired.asText() } else if (now.isAfter(expirationTime.minus(1, ChronoUnit.DAYS))) { // We are within 24 hours of expiration, so show the specific time. - val expirationTime = expirationTime.toFormattedPattern(pattern = "h:mm a", clock = clock) + val expirationTime = expirationTime.toFormattedTimeStyle( + timeStyle = FormatStyle.SHORT, + clock = clock, + ) R.string.expires_at.asText(expirationTime) } else if (dayBeforeExpiration.dayOfYear == now.atZone(clock.zone).dayOfYear) { // We expire tomorrow based on the day of year. R.string.expires_tomorrow.asText() } else { // Let them know the date it expires. - val expirationDate = expirationTime.toFormattedPattern(pattern = "M/d/yy", clock = clock) + val expirationDate = expirationTime.toFormattedDateStyle( + dateStyle = FormatStyle.SHORT, + clock = clock, + ) R.string.expires_on.asText(expirationDate) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt index 6f5d39d1d7..3ba6944e9b 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt @@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -21,12 +21,11 @@ import kotlinx.coroutines.flow.update import kotlinx.parcelize.Parcelize import java.time.Clock import java.time.Instant +import java.time.format.FormatStyle import javax.inject.Inject private const val KEY_STATE = "state" -private const val VAULT_LAST_SYNC_TIME_PATTERN: String = "M/d/yyyy h:mm a" - /** * View model for the other screen. */ @@ -46,7 +45,11 @@ class OtherViewModel @Inject constructor( clearClipboardFrequency = settingsRepo.clearClipboardFrequency, lastSyncTime = settingsRepo .vaultLastSync - ?.toFormattedPattern(VAULT_LAST_SYNC_TIME_PATTERN, clock) + ?.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, + clock = clock, + ) .orEmpty(), dialogState = null, ), @@ -137,7 +140,11 @@ class OtherViewModel @Inject constructor( it.copy( lastSyncTime = action .vaultLastSyncTime - ?.toFormattedPattern(VAULT_LAST_SYNC_TIME_PATTERN, clock) + ?.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, + clock = clock, + ) .orEmpty(), dialogState = null, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt index 435d732cf3..0de1b84d2d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt @@ -4,7 +4,7 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.core.data.repository.model.DataState -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import java.time.Clock +import java.time.format.FormatStyle import javax.inject.Inject private const val KEY_STATE = "state" @@ -141,8 +142,9 @@ class PasswordHistoryViewModel @Inject constructor( val passwords = this?.map { passwordHistoryView -> GeneratedPassword( password = passwordHistoryView.password, - date = passwordHistoryView.lastUsedDate.toFormattedPattern( - pattern = "MM/dd/yy h:mm a", + date = passwordHistoryView.lastUsedDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, clock = clock, ), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt index a2688d9e2a..a3ea5be334 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -22,6 +22,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.parcelize.Parcelize import java.time.Clock import java.time.ZonedDateTime +import java.time.format.FormatStyle import java.time.temporal.ChronoUnit import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -94,7 +95,13 @@ private sealed class CustomDeletionOption : Parcelable { override fun getText( clock: Clock, - ): Text = time.toFormattedPattern(pattern = "d MMM, yyyy, h:mma", clock = clock).asText() + ): Text = time + .toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, + clock = clock, + ) + .asText() } @Parcelize diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt index 3c35f84d92..b4f8cbbe2f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/util/SendDataExtensions.kt @@ -1,13 +1,12 @@ package com.x8bit.bitwarden.ui.tools.feature.send.util -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.send.SendType import com.bitwarden.send.SendView import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.ui.tools.feature.send.SendState import java.time.Clock - -private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a" +import java.time.format.FormatStyle /** * Transforms [SendData] into [SendState.ViewState]. @@ -34,8 +33,9 @@ private fun List.toSendContent( SendState.ViewState.Content.SendItem( id = requireNotNull(sendView.id), name = sendView.name, - deletionDate = sendView.deletionDate.toFormattedPattern( - pattern = DELETION_DATE_PATTERN, + deletionDate = sendView.deletionDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, clock = clock, ), type = when (sendView.type) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensions.kt index 0628869dfa..1d86538260 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensions.kt @@ -1,6 +1,6 @@ package com.x8bit.bitwarden.ui.tools.feature.send.viewsend.util -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.send.SendFileView import com.bitwarden.send.SendTextView import com.bitwarden.send.SendType @@ -8,6 +8,7 @@ import com.bitwarden.send.SendView import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendState import java.time.Clock +import java.time.format.FormatStyle /** * Transforms the given [SendView] to a [ViewSendState.ViewState.Content]. @@ -23,9 +24,11 @@ fun SendView.toViewSendViewStateContent( }, shareLink = this.toSendUrl(baseWebSendUrl = baseWebSendUrl), sendName = this.name, - deletionDate = this - .deletionDate - .toFormattedPattern(pattern = "d MMM, yyyy, h:mma", clock = clock), + deletionDate = this.deletionDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, + clock = clock, + ), maxAccessCount = this.maxAccessCount?.toInt(), currentAccessCount = this.accessCount.toInt(), notes = this.notes, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index f5736194e8..e3a64f6ba5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -2,7 +2,8 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit.util -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateStyle +import com.bitwarden.core.data.util.toFormattedTimeStyle import com.bitwarden.ui.util.asText import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherType @@ -27,11 +28,9 @@ import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType.Companion.fromId import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull import java.time.Clock +import java.time.format.FormatStyle import java.util.UUID -private const val PASSKEY_CREATION_DATE_PATTERN: String = "M/d/yy" -private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a" - /** * Transforms [CipherView] into [VaultAddEditState.ViewState]. */ @@ -332,6 +331,6 @@ private fun List?.getPrimaryFido2CredentialOrNull( * "M/d/yy, hh:mm a". */ private fun Fido2Credential.getCreationDateTime(clock: Clock) = R.string.created_xy.asText( - creationDate.toFormattedPattern(pattern = PASSKEY_CREATION_DATE_PATTERN, clock = clock), - creationDate.toFormattedPattern(pattern = PASSKEY_CREATION_TIME_PATTERN, clock = clock), + creationDate.toFormattedDateStyle(dateStyle = FormatStyle.SHORT, clock = clock), + creationDate.toFormattedTimeStyle(timeStyle = FormatStyle.SHORT, clock = clock), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index 2d9e131e85..7b1c3c798a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -1,7 +1,9 @@ package com.x8bit.bitwarden.ui.vault.feature.item.util import androidx.annotation.DrawableRes -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateStyle +import com.bitwarden.core.data.util.toFormattedDateTimeStyle +import com.bitwarden.core.data.util.toFormattedTimeStyle import com.bitwarden.ui.platform.base.util.nullIfAllEqual import com.bitwarden.ui.platform.base.util.orNullIfBlank import com.bitwarden.ui.platform.base.util.orZeroWidthSpace @@ -27,12 +29,9 @@ import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull import kotlinx.collections.immutable.ImmutableList import java.time.Clock +import java.time.format.FormatStyle import java.util.Locale -private const val LAST_UPDATED_DATE_TIME_PATTERN: String = "M/d/yy hh:mm a" -private const val FIDO2_CREDENTIAL_CREATION_DATE_PATTERN: String = "M/d/yy" -private const val FIDO2_CREDENTIAL_CREATION_TIME_PATTERN: String = "h:mm a" - /** * Transforms [VaultData] into [VaultItemState.ViewState]. */ @@ -62,8 +61,9 @@ fun CipherView.toViewState( ?.find { it.id == fieldView.hashCode().toString() }, ) }, - lastUpdated = revisionDate.toFormattedPattern( - pattern = LAST_UPDATED_DATE_TIME_PATTERN, + lastUpdated = revisionDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, clock = clock, ), notes = notes, @@ -124,8 +124,9 @@ fun CipherView.toViewState( uris = loginValues.uris.orEmpty().map { it.toUriData() }, passwordRevisionDate = loginValues .passwordRevisionDate - ?.toFormattedPattern( - pattern = LAST_UPDATED_DATE_TIME_PATTERN, + ?.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, clock = clock, ), isPremiumUser = isPremiumUser, @@ -248,12 +249,12 @@ private fun LoginUriView.toUriData() = private fun Fido2Credential?.getCreationDateText(clock: Clock): Text? = this?.let { R.string.created_xy.asText( - creationDate.toFormattedPattern( - pattern = FIDO2_CREDENTIAL_CREATION_DATE_PATTERN, + creationDate.toFormattedDateStyle( + dateStyle = FormatStyle.SHORT, clock = clock, ), - creationDate.toFormattedPattern( - pattern = FIDO2_CREDENTIAL_CREATION_TIME_PATTERN, + creationDate.toFormattedTimeStyle( + timeStyle = FormatStyle.SHORT, clock = clock, ), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt index dc235912d6..638abc43cb 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt @@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util import androidx.annotation.DrawableRes -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.send.SendType import com.bitwarden.send.SendView @@ -35,8 +35,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData import com.x8bit.bitwarden.ui.vault.model.TotpData import java.time.Clock - -private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a" +import java.time.format.FormatStyle /** * Determines a predicate to filter a list of [CipherView] based on the @@ -461,7 +460,11 @@ private fun SendView.toDisplayItem( titleTestTag = "SendNameLabel", secondSubtitle = null, secondSubtitleTestTag = null, - subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock), + subtitle = deletionDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.SHORT, + clock = clock, + ), subtitleTestTag = "SendDateLabel", iconData = IconData.Local( iconRes = when (type) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/FlightRecorderDataSetExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/FlightRecorderDataSetExtensions.kt index 03f8efbe10..3bb2a5c5c6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/FlightRecorderDataSetExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/util/FlightRecorderDataSetExtensions.kt @@ -1,12 +1,14 @@ package com.x8bit.bitwarden.ui.vault.feature.vault.util -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateStyle +import com.bitwarden.core.data.util.toFormattedTimeStyle import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData import java.time.Clock import java.time.Instant +import java.time.format.FormatStyle /** * Helper function to create a [BitwardenSnackbarData] representing the active flight recorder. @@ -21,8 +23,8 @@ fun FlightRecorderDataSet.toSnackbarData( ?: return null return BitwardenSnackbarData( message = R.string.flight_recorder_banner_message.asText( - expirationTime.toFormattedPattern(pattern = "M/d/yy", clock = clock), - expirationTime.toFormattedPattern(pattern = "h:mm a", clock = clock), + expirationTime.toFormattedDateStyle(dateStyle = FormatStyle.SHORT, clock = clock), + expirationTime.toFormattedTimeStyle(timeStyle = FormatStyle.SHORT, clock = clock), ), messageHeader = R.string.flight_recorder_banner_title.asText(), actionLabel = R.string.go_to_settings.asText(), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt index e922b18ed4..f81a303aa7 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt @@ -183,7 +183,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() { email = EMAIL, fingerprint = AUTH_REQUEST.fingerprint, ipAddress = AUTH_REQUEST.ipAddress, - time = "9/13/24 12:00 AM", + time = "9/13/24, 12:00 AM", ), ) val viewModel = createViewModel() @@ -455,7 +455,7 @@ private val DEFAULT_STATE: LoginApprovalState = LoginApprovalState( email = EMAIL, fingerprint = FINGERPRINT, ipAddress = "1.0.0.1", - time = "9/13/24 12:00 AM", + time = "9/13/24, 12:00 AM", ), ) private const val USER_ID = "userID" diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt index f073c95fe3..f16b749c87 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsViewModelTest.kt @@ -56,7 +56,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() { @Test fun `getPendingResults success with content should update state with some requests filtered`() { val dateTimeFormatter = DateTimeFormatter - .ofPattern("M/d/yy hh:mm a") + .ofPattern("M/d/yy, hh:mm a") .withZone(fixedClock.zone) val nowZonedDateTime = ZonedDateTime.now(fixedClock) val requestList = listOf( @@ -284,15 +284,11 @@ class PendingRequestsViewModelTest : BaseViewModelTest() { } } - @Suppress("LongMethod") @Test fun `on LifecycleResume should update state`() = runTest { - val dateTimeFormatter = DateTimeFormatter - .ofPattern("M/d/yy hh:mm a") - .withZone(fixedClock.zone) - val nowZonedDateTime = ZonedDateTime.now() - val fiveMinZonedDateTime = ZonedDateTime.now().minusMinutes(5) - val sixMinZonedDateTime = ZonedDateTime.now().minusMinutes(6) + val nowZonedDateTime = ZonedDateTime.now(fixedClock) + val fiveMinZonedDateTime = ZonedDateTime.now(fixedClock).minusMinutes(5) + val sixMinZonedDateTime = ZonedDateTime.now(fixedClock).minusMinutes(6) val requestList = listOf( AuthRequest( id = "1", @@ -353,12 +349,12 @@ class PendingRequestsViewModelTest : BaseViewModelTest() { PendingRequestsState.ViewState.Content.PendingLoginRequest( fingerprintPhrase = "pantry-overdue-survive-sleep-jab", platform = "Android", - timestamp = nowZonedDateTime.format(dateTimeFormatter), + timestamp = "10/27/23, 12:00 PM", ), PendingRequestsState.ViewState.Content.PendingLoginRequest( fingerprintPhrase = "erupt-anew-matchbook-disk-student", platform = "iOS", - timestamp = fiveMinZonedDateTime.format(dateTimeFormatter), + timestamp = "10/27/23, 11:55 AM", ), ), ), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt index e4ebaaffdc..c08f95309a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt @@ -157,7 +157,7 @@ class OtherViewModelTest : BaseViewModelTest() { mutableVaultLastSyncStateFlow.tryEmit(newSyncTime) assertEquals( DEFAULT_STATE.copy( - lastSyncTime = "10/27/2023 12:00 PM", + lastSyncTime = "Oct 27, 2023, 12:00 PM", dialogState = null, ), awaitItem(), @@ -236,6 +236,6 @@ private val DEFAULT_STATE = OtherState( allowScreenCapture = false, allowSyncOnRefresh = false, clearClipboardFrequency = ClearClipboardFrequency.NEVER, - lastSyncTime = "10/26/2023 12:00 PM", + lastSyncTime = "Oct 26, 2023, 12:00 PM", dialogState = null, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt index 97e82a4489..a6947c41be 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModelTest.kt @@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.data.repository.model.DataState -import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test import java.time.Clock import java.time.Instant import java.time.ZoneOffset +import java.time.format.FormatStyle class PasswordHistoryViewModelTest : BaseViewModelTest() { @@ -171,7 +172,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() { passwords = listOf( PasswordHistoryState.GeneratedPassword( password = "mockPassword-1", - date = "10/27/23 12:00 PM", + date = "10/27/23, 12:00 PM", ), ), ), @@ -194,8 +195,9 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() { passwords = listOf( PasswordHistoryState.GeneratedPassword( password = "password", - date = passwordHistoryView.lastUsedDate.toFormattedPattern( - pattern = "MM/dd/yy h:mm a", + date = passwordHistoryView.lastUsedDate.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, clock = fixedClock, ), ), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensionsTest.kt index afc1e11033..023fc99e17 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/util/SendViewExtensionsTest.kt @@ -48,7 +48,7 @@ class SendViewExtensionsTest { ), shareLink = sendUrl, sendName = "mockName-1", - deletionDate = "27 Oct, 2023, 12:00PM", + deletionDate = "Oct 27, 2023, 12:00 PM", maxAccessCount = 1, currentAccessCount = 1, notes = "mockNotes-1", @@ -76,7 +76,7 @@ class SendViewExtensionsTest { ), shareLink = sendUrl, sendName = "mockName-2", - deletionDate = "27 Oct, 2023, 12:00PM", + deletionDate = "Oct 27, 2023, 12:00 PM", maxAccessCount = 1, currentAccessCount = 1, notes = "mockNotes-2", diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index b8b7ad19be..a39ef2a519 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -169,7 +169,7 @@ fun createCommonContent( if (isEmpty) { VaultItemState.ViewState.Content.Common( name = "mockName", - lastUpdated = "1/1/70 12:16 AM", + lastUpdated = "1/1/70, 12:16 AM", notes = null, customFields = emptyList(), requiresCloneConfirmation = false, @@ -186,7 +186,7 @@ fun createCommonContent( } else { VaultItemState.ViewState.Content.Common( name = "mockName", - lastUpdated = "1/1/70 12:16 AM", + lastUpdated = "1/1/70, 12:16 AM", notes = "Lots of notes", customFields = listOf( FieldView( @@ -267,7 +267,7 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT ), ) }, - passwordRevisionDate = "1/1/70 12:16 AM".takeUnless { isEmpty }, + passwordRevisionDate = "1/1/70, 12:16 AM".takeUnless { isEmpty }, isPremiumUser = true, totpCodeItemData = TotpCodeItemData( periodSeconds = 30, diff --git a/core/src/main/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensions.kt b/core/src/main/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensions.kt index de1f06c12c..a372010435 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensions.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensions.kt @@ -1,17 +1,12 @@ package com.bitwarden.core.data.util import java.time.Clock -import java.time.ZoneId +import java.time.chrono.IsoChronology import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.format.FormatStyle import java.time.temporal.TemporalAccessor - -/** - * Converts the [TemporalAccessor] to a formatted string based on the provided pattern and timezone. - */ -fun TemporalAccessor.toFormattedPattern( - pattern: String, - zone: ZoneId, -): String = DateTimeFormatter.ofPattern(pattern).withZone(zone).format(this) +import java.util.Locale /** * Converts the [TemporalAccessor] to a formatted string based on the provided pattern and timezone. @@ -19,4 +14,74 @@ fun TemporalAccessor.toFormattedPattern( fun TemporalAccessor.toFormattedPattern( pattern: String, clock: Clock = Clock.systemDefaultZone(), -): String = toFormattedPattern(pattern = pattern, zone = clock.zone) +): String = DateTimeFormatter.ofPattern(pattern).withZone(clock.zone).format(this) + +/** + * Converts the [TemporalAccessor] to a formatted date string based on the provided style, locale, + * and clock. + * + * In US English, the output string will be as follows for the given [dateStyle]: + * * [FormatStyle.SHORT]: 6/6/25 + * * [FormatStyle.MEDIUM]: Jun 6, 2025 + * * [FormatStyle.LONG]: June 6, 2025 + * * [FormatStyle.FULL]: Friday, June 6, 2025 + */ +fun TemporalAccessor.toFormattedDateStyle( + dateStyle: FormatStyle, + locale: Locale = Locale.getDefault(), + clock: Clock = Clock.systemDefaultZone(), +): String = + toFormattedPattern( + pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern( + dateStyle, + null, + IsoChronology.INSTANCE, + locale, + ), + clock = clock, + ) + +/** + * Converts the [TemporalAccessor] to a formatted time string based on the provided style, locale, + * and clock. + * + * In US English, the output string will be as follows for the given [timeStyle]: + * * [FormatStyle.SHORT]: 4:15 PM + * * [FormatStyle.MEDIUM]: 4:15:21 PM + * * [FormatStyle.LONG]: 4:15:21 PM CDT + * * [FormatStyle.FULL]: 4:51:03 PM Central Daylight Time + */ +fun TemporalAccessor.toFormattedTimeStyle( + timeStyle: FormatStyle, + locale: Locale = Locale.getDefault(), + clock: Clock = Clock.systemDefaultZone(), +): String = + toFormattedPattern( + pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern( + null, + timeStyle, + IsoChronology.INSTANCE, + locale, + ), + clock = clock, + ) + +/** + * Converts the [TemporalAccessor] to a formatted string based on the provided style, locale, and + * clock. + */ +fun TemporalAccessor.toFormattedDateTimeStyle( + dateStyle: FormatStyle, + timeStyle: FormatStyle, + locale: Locale = Locale.getDefault(), + clock: Clock = Clock.systemDefaultZone(), +): String = + toFormattedPattern( + pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern( + dateStyle, + timeStyle, + IsoChronology.INSTANCE, + locale, + ), + clock = clock, + ) diff --git a/core/src/test/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensionsTest.kt b/core/src/test/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensionsTest.kt index 48d300aa90..507c1c99d2 100644 --- a/core/src/test/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensionsTest.kt +++ b/core/src/test/kotlin/com/bitwarden/core/data/util/TemporalAccessorExtensionsTest.kt @@ -4,33 +4,254 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.time.Clock import java.time.Instant -import java.time.ZoneId import java.time.ZoneOffset +import java.time.format.FormatStyle +import java.util.Locale class TemporalAccessorExtensionsTest { @Test - fun `toFormattedPattern should return correctly formatted string with timezone`() { + fun `toFormattedPattern should return correctly formatted string`() { val instant = Instant.parse("2023-12-10T15:30:00Z") - val pattern = "MM/dd/yyyy hh:mm a" - val zone = ZoneId.of("UTC") - val expectedFormattedString = "12/10/2023 03:30 PM" - val formattedString = instant.toFormattedPattern(pattern, zone) - assertEquals(expectedFormattedString, formattedString) + assertEquals( + "12/10/2023 03:30 PM", + instant.toFormattedPattern( + pattern = "MM/dd/yyyy hh:mm a", + clock = FIXED_CLOCK, + ), + ) } @Test - fun `toFormattedPattern should return correctly formatted string with clock`() { + fun `toFormattedDateStyle should return correctly formatted string with with locale`() { val instant = Instant.parse("2023-12-10T15:30:00Z") - val pattern = "MM/dd/yyyy hh:mm a" - val clock: Clock = Clock.fixed( - Instant.parse("2023-10-27T12:00:00Z"), - ZoneOffset.UTC, - ) - val expectedFormattedString = "12/10/2023 03:30 PM" - val formattedString = instant.toFormattedPattern(pattern, clock) - assertEquals(expectedFormattedString, formattedString) + // US locale + assertEquals( + "12/10/23", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.SHORT, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "Dec 10, 2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.MEDIUM, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "December 10, 2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.LONG, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "Sunday, December 10, 2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.FULL, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + + // UK locale + assertEquals( + "10/12/2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.SHORT, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "10 Dec 2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.MEDIUM, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "10 December 2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.LONG, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "Sunday, 10 December 2023", + instant.toFormattedDateStyle( + dateStyle = FormatStyle.FULL, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + } + + @Test + fun `toFormattedTimeStyle should return correctly formatted string with with locale`() { + val instant = Instant.parse("2023-12-10T15:30:00Z") + + // US locale + assertEquals( + "3:30 PM", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.SHORT, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "3:30:00 PM", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.MEDIUM, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "3:30:00 PM Z", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.LONG, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "3:30:00 PM Z", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.FULL, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + + // UK locale + assertEquals( + "15:30", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.SHORT, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "15:30:00", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.MEDIUM, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "15:30:00 Z", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.LONG, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "15:30:00 Z", + instant.toFormattedTimeStyle( + timeStyle = FormatStyle.FULL, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + } + + @Test + fun `toFormattedDateTimeStyle should return correctly formatted string with with locale`() { + val instant = Instant.parse("2023-12-10T15:30:00Z") + + // US locale + assertEquals( + "12/10/23, 3:30 PM", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "Dec 10, 2023, 3:30:00 PM", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.MEDIUM, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "December 10, 2023 at 3:30:00 PM Z", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.LONG, + timeStyle = FormatStyle.LONG, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "Sunday, December 10, 2023 at 3:30:00 PM Z", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.FULL, + timeStyle = FormatStyle.FULL, + locale = Locale.US, + clock = FIXED_CLOCK, + ), + ) + + // UK locale + assertEquals( + "10/12/2023, 15:30", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.SHORT, + timeStyle = FormatStyle.SHORT, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "10 Dec 2023, 15:30:00", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.MEDIUM, + timeStyle = FormatStyle.MEDIUM, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "10 December 2023 at 15:30:00 Z", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.LONG, + timeStyle = FormatStyle.LONG, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) + assertEquals( + "Sunday, 10 December 2023 at 15:30:00 Z", + instant.toFormattedDateTimeStyle( + dateStyle = FormatStyle.FULL, + timeStyle = FormatStyle.FULL, + locale = Locale.UK, + clock = FIXED_CLOCK, + ), + ) } } + +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, +)