PM-22502: Format dates and times correctly for locale (#5333)

This commit is contained in:
David Perez 2025-06-09 13:30:30 -05:00 committed by GitHub
parent d822be62e1
commit 9cdfe0c5d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 445 additions and 115 deletions

View File

@ -4,7 +4,7 @@ package com.x8bit.bitwarden.ui.platform.feature.search.util
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.bitwarden.core.data.repository.util.SpecialCharWithPrecedenceComparator 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.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.ui.platform.base.util.removeDiacritics 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.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a"
/** /**
* Updates a [SearchTypeData] with the given data if necessary. * Updates a [SearchTypeData] with the given data if necessary.
@ -353,7 +352,11 @@ private fun SendView.toDisplayItem(
id = id.orEmpty(), id = id.orEmpty(),
title = name, title = name,
titleTestTag = "SendNameLabel", titleTestTag = "SendNameLabel",
subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock), subtitle = deletionDate.toFormattedDateTimeStyle(
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
),
subtitleTestTag = "SendDateLabel", subtitleTestTag = "SendDateLabel",
iconData = IconData.Local( iconData = IconData.Local(
iconRes = when (type) { iconRes = when (type) {

View File

@ -1,12 +1,14 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.about.util 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.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import java.time.Clock import java.time.Clock
import java.time.Instant 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 * 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, clock: Clock,
): Text { ): Text {
val completionInstant = Instant.ofEpochMilli(this.startTimeMs + this.durationMs) val completionInstant = Instant.ofEpochMilli(this.startTimeMs + this.durationMs)
val completionDate = completionInstant.toFormattedPattern(pattern = "M/d/yy", clock = clock) val completionDate = completionInstant.toFormattedDateStyle(
val completionTime = completionInstant.toFormattedPattern(pattern = "h:mm a", clock = clock) dateStyle = FormatStyle.SHORT,
clock = clock,
)
val completionTime = completionInstant.toFormattedTimeStyle(
timeStyle = FormatStyle.SHORT,
clock = clock,
)
return R.string.stops_logging_on.asText(completionDate, completionTime) return R.string.stops_logging_on.asText(completionDate, completionTime)
} }

View File

@ -572,13 +572,11 @@ private fun SessionCustomTimeoutRow(
cardStyle = CardStyle.Middle(), cardStyle = CardStyle.Middle(),
modifier = modifier, modifier = modifier,
) { ) {
val formattedTime = LocalTime
.ofSecondOfDay(
vaultTimeoutInMinutes * MINUTES_PER_HOUR.toLong(),
)
.toFormattedPattern("HH:mm")
Text( Text(
text = formattedTime, text = LocalTime
.ofSecondOfDay(vaultTimeoutInMinutes * MINUTES_PER_HOUR.toLong())
.toFormattedPattern(pattern = "HH:mm"),
style = BitwardenTheme.typography.labelSmall, style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary, color = BitwardenTheme.colorScheme.text.primary,
) )

View File

@ -5,7 +5,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginap
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope 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.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
@ -214,8 +215,9 @@ class LoginApprovalViewModel @Inject constructor(
email = email, email = email,
fingerprint = result.authRequest.fingerprint, fingerprint = result.authRequest.fingerprint,
ipAddress = result.authRequest.ipAddress, ipAddress = result.authRequest.ipAddress,
time = result.authRequest.creationDate.toFormattedPattern( time = result.authRequest.creationDate.toFormattedDateTimeStyle(
pattern = "M/d/yy hh:mm a", dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
), ),

View File

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pending
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope 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.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
@ -144,8 +145,9 @@ class PendingRequestsViewModel @Inject constructor(
PendingRequestsState.ViewState.Content.PendingLoginRequest( PendingRequestsState.ViewState.Content.PendingLoginRequest(
fingerprintPhrase = request.fingerprint, fingerprintPhrase = request.fingerprint,
platform = request.platform, platform = request.platform,
timestamp = request.creationDate.toFormattedPattern( timestamp = request.creationDate.toFormattedDateTimeStyle(
pattern = "M/d/yy hh:mm a", dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
) )

View File

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.recordedLogs.util 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.toFormattedPattern
import com.bitwarden.core.data.util.toFormattedTimeStyle
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
@ -11,6 +13,7 @@ import com.x8bit.bitwarden.ui.platform.util.formatBytes
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
/** /**
@ -67,14 +70,20 @@ private fun FlightRecorderDataSet.FlightRecorderData.expiresIn(clock: Clock): Te
R.string.expired.asText() R.string.expired.asText()
} else if (now.isAfter(expirationTime.minus(1, ChronoUnit.DAYS))) { } else if (now.isAfter(expirationTime.minus(1, ChronoUnit.DAYS))) {
// We are within 24 hours of expiration, so show the specific time. // 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) R.string.expires_at.asText(expirationTime)
} else if (dayBeforeExpiration.dayOfYear == now.atZone(clock.zone).dayOfYear) { } else if (dayBeforeExpiration.dayOfYear == now.atZone(clock.zone).dayOfYear) {
// We expire tomorrow based on the day of year. // We expire tomorrow based on the day of year.
R.string.expires_tomorrow.asText() R.string.expires_tomorrow.asText()
} else { } else {
// Let them know the date it expires. // 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) R.string.expires_on.asText(expirationDate)
} }
} }

View File

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope 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.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -21,12 +21,11 @@ import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.format.FormatStyle
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state" 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. * View model for the other screen.
*/ */
@ -46,7 +45,11 @@ class OtherViewModel @Inject constructor(
clearClipboardFrequency = settingsRepo.clearClipboardFrequency, clearClipboardFrequency = settingsRepo.clearClipboardFrequency,
lastSyncTime = settingsRepo lastSyncTime = settingsRepo
.vaultLastSync .vaultLastSync
?.toFormattedPattern(VAULT_LAST_SYNC_TIME_PATTERN, clock) ?.toFormattedDateTimeStyle(
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
)
.orEmpty(), .orEmpty(),
dialogState = null, dialogState = null,
), ),
@ -137,7 +140,11 @@ class OtherViewModel @Inject constructor(
it.copy( it.copy(
lastSyncTime = action lastSyncTime = action
.vaultLastSyncTime .vaultLastSyncTime
?.toFormattedPattern(VAULT_LAST_SYNC_TIME_PATTERN, clock) ?.toFormattedDateTimeStyle(
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
)
.orEmpty(), .orEmpty(),
dialogState = null, dialogState = null,
) )

View File

@ -4,7 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.repository.model.DataState 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.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
@ -141,8 +142,9 @@ class PasswordHistoryViewModel @Inject constructor(
val passwords = this?.map { passwordHistoryView -> val passwords = this?.map { passwordHistoryView ->
GeneratedPassword( GeneratedPassword(
password = passwordHistoryView.password, password = passwordHistoryView.password,
date = passwordHistoryView.lastUsedDate.toFormattedPattern( date = passwordHistoryView.lastUsedDate.toFormattedDateTimeStyle(
pattern = "MM/dd/yy h:mm a", dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
) )

View File

@ -10,7 +10,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.platform.components.model.CardStyle
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -22,6 +22,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock import java.time.Clock
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.FormatStyle
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
@ -94,7 +95,13 @@ private sealed class CustomDeletionOption : Parcelable {
override fun getText( override fun getText(
clock: Clock, 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 @Parcelize

View File

@ -1,13 +1,12 @@
package com.x8bit.bitwarden.ui.tools.feature.send.util 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.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.tools.feature.send.SendState import com.x8bit.bitwarden.ui.tools.feature.send.SendState
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a"
/** /**
* Transforms [SendData] into [SendState.ViewState]. * Transforms [SendData] into [SendState.ViewState].
@ -34,8 +33,9 @@ private fun List<SendView>.toSendContent(
SendState.ViewState.Content.SendItem( SendState.ViewState.Content.SendItem(
id = requireNotNull(sendView.id), id = requireNotNull(sendView.id),
name = sendView.name, name = sendView.name,
deletionDate = sendView.deletionDate.toFormattedPattern( deletionDate = sendView.deletionDate.toFormattedDateTimeStyle(
pattern = DELETION_DATE_PATTERN, dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
type = when (sendView.type) { type = when (sendView.type) {

View File

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.ui.tools.feature.send.viewsend.util 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.SendFileView
import com.bitwarden.send.SendTextView import com.bitwarden.send.SendTextView
import com.bitwarden.send.SendType 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.util.toSendUrl
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendState import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendState
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
/** /**
* Transforms the given [SendView] to a [ViewSendState.ViewState.Content]. * Transforms the given [SendView] to a [ViewSendState.ViewState.Content].
@ -23,9 +24,11 @@ fun SendView.toViewSendViewStateContent(
}, },
shareLink = this.toSendUrl(baseWebSendUrl = baseWebSendUrl), shareLink = this.toSendUrl(baseWebSendUrl = baseWebSendUrl),
sendName = this.name, sendName = this.name,
deletionDate = this deletionDate = this.deletionDate.toFormattedDateTimeStyle(
.deletionDate dateStyle = FormatStyle.MEDIUM,
.toFormattedPattern(pattern = "d MMM, yyyy, h:mma", clock = clock), timeStyle = FormatStyle.SHORT,
clock = clock,
),
maxAccessCount = this.maxAccessCount?.toInt(), maxAccessCount = this.maxAccessCount?.toInt(),
currentAccessCount = this.accessCount.toInt(), currentAccessCount = this.accessCount.toInt(),
notes = this.notes, notes = this.notes,

View File

@ -2,7 +2,8 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util 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.ui.util.asText
import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherType 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.VaultLinkedFieldType.Companion.fromId
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
import java.util.UUID 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]. * Transforms [CipherView] into [VaultAddEditState.ViewState].
*/ */
@ -332,6 +331,6 @@ private fun List<Fido2Credential>?.getPrimaryFido2CredentialOrNull(
* "M/d/yy, hh:mm a". * "M/d/yy, hh:mm a".
*/ */
private fun Fido2Credential.getCreationDateTime(clock: Clock) = R.string.created_xy.asText( private fun Fido2Credential.getCreationDateTime(clock: Clock) = R.string.created_xy.asText(
creationDate.toFormattedPattern(pattern = PASSKEY_CREATION_DATE_PATTERN, clock = clock), creationDate.toFormattedDateStyle(dateStyle = FormatStyle.SHORT, clock = clock),
creationDate.toFormattedPattern(pattern = PASSKEY_CREATION_TIME_PATTERN, clock = clock), creationDate.toFormattedTimeStyle(timeStyle = FormatStyle.SHORT, clock = clock),
) )

View File

@ -1,7 +1,9 @@
package com.x8bit.bitwarden.ui.vault.feature.item.util package com.x8bit.bitwarden.ui.vault.feature.item.util
import androidx.annotation.DrawableRes 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.nullIfAllEqual
import com.bitwarden.ui.platform.base.util.orNullIfBlank import com.bitwarden.ui.platform.base.util.orNullIfBlank
import com.bitwarden.ui.platform.base.util.orZeroWidthSpace 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 com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
import java.util.Locale 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]. * Transforms [VaultData] into [VaultItemState.ViewState].
*/ */
@ -62,8 +61,9 @@ fun CipherView.toViewState(
?.find { it.id == fieldView.hashCode().toString() }, ?.find { it.id == fieldView.hashCode().toString() },
) )
}, },
lastUpdated = revisionDate.toFormattedPattern( lastUpdated = revisionDate.toFormattedDateTimeStyle(
pattern = LAST_UPDATED_DATE_TIME_PATTERN, dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
notes = notes, notes = notes,
@ -124,8 +124,9 @@ fun CipherView.toViewState(
uris = loginValues.uris.orEmpty().map { it.toUriData() }, uris = loginValues.uris.orEmpty().map { it.toUriData() },
passwordRevisionDate = loginValues passwordRevisionDate = loginValues
.passwordRevisionDate .passwordRevisionDate
?.toFormattedPattern( ?.toFormattedDateTimeStyle(
pattern = LAST_UPDATED_DATE_TIME_PATTERN, dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
isPremiumUser = isPremiumUser, isPremiumUser = isPremiumUser,
@ -248,12 +249,12 @@ private fun LoginUriView.toUriData() =
private fun Fido2Credential?.getCreationDateText(clock: Clock): Text? = private fun Fido2Credential?.getCreationDateText(clock: Clock): Text? =
this?.let { this?.let {
R.string.created_xy.asText( R.string.created_xy.asText(
creationDate.toFormattedPattern( creationDate.toFormattedDateStyle(
pattern = FIDO2_CREDENTIAL_CREATION_DATE_PATTERN, dateStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
creationDate.toFormattedPattern( creationDate.toFormattedTimeStyle(
pattern = FIDO2_CREDENTIAL_CREATION_TIME_PATTERN, timeStyle = FormatStyle.SHORT,
clock = clock, clock = clock,
), ),
) )

View File

@ -3,7 +3,7 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import androidx.annotation.DrawableRes 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.fido.Fido2CredentialAutofillView
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView 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.feature.vault.util.toLoginIconData
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.x8bit.bitwarden.ui.vault.model.TotpData
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle
private const val DELETION_DATE_PATTERN: String = "MMM d, uuuu, hh:mm a"
/** /**
* Determines a predicate to filter a list of [CipherView] based on the * Determines a predicate to filter a list of [CipherView] based on the
@ -461,7 +460,11 @@ private fun SendView.toDisplayItem(
titleTestTag = "SendNameLabel", titleTestTag = "SendNameLabel",
secondSubtitle = null, secondSubtitle = null,
secondSubtitleTestTag = null, secondSubtitleTestTag = null,
subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock), subtitle = deletionDate.toFormattedDateTimeStyle(
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT,
clock = clock,
),
subtitleTestTag = "SendDateLabel", subtitleTestTag = "SendDateLabel",
iconData = IconData.Local( iconData = IconData.Local(
iconRes = when (type) { iconRes = when (type) {

View File

@ -1,12 +1,14 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util 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.bitwarden.ui.util.asText
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.format.FormatStyle
/** /**
* Helper function to create a [BitwardenSnackbarData] representing the active flight recorder. * Helper function to create a [BitwardenSnackbarData] representing the active flight recorder.
@ -21,8 +23,8 @@ fun FlightRecorderDataSet.toSnackbarData(
?: return null ?: return null
return BitwardenSnackbarData( return BitwardenSnackbarData(
message = R.string.flight_recorder_banner_message.asText( message = R.string.flight_recorder_banner_message.asText(
expirationTime.toFormattedPattern(pattern = "M/d/yy", clock = clock), expirationTime.toFormattedDateStyle(dateStyle = FormatStyle.SHORT, clock = clock),
expirationTime.toFormattedPattern(pattern = "h:mm a", clock = clock), expirationTime.toFormattedTimeStyle(timeStyle = FormatStyle.SHORT, clock = clock),
), ),
messageHeader = R.string.flight_recorder_banner_title.asText(), messageHeader = R.string.flight_recorder_banner_title.asText(),
actionLabel = R.string.go_to_settings.asText(), actionLabel = R.string.go_to_settings.asText(),

View File

@ -183,7 +183,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
email = EMAIL, email = EMAIL,
fingerprint = AUTH_REQUEST.fingerprint, fingerprint = AUTH_REQUEST.fingerprint,
ipAddress = AUTH_REQUEST.ipAddress, ipAddress = AUTH_REQUEST.ipAddress,
time = "9/13/24 12:00 AM", time = "9/13/24, 12:00 AM",
), ),
) )
val viewModel = createViewModel() val viewModel = createViewModel()
@ -455,7 +455,7 @@ private val DEFAULT_STATE: LoginApprovalState = LoginApprovalState(
email = EMAIL, email = EMAIL,
fingerprint = FINGERPRINT, fingerprint = FINGERPRINT,
ipAddress = "1.0.0.1", 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" private const val USER_ID = "userID"

View File

@ -56,7 +56,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
@Test @Test
fun `getPendingResults success with content should update state with some requests filtered`() { fun `getPendingResults success with content should update state with some requests filtered`() {
val dateTimeFormatter = DateTimeFormatter val dateTimeFormatter = DateTimeFormatter
.ofPattern("M/d/yy hh:mm a") .ofPattern("M/d/yy, hh:mm a")
.withZone(fixedClock.zone) .withZone(fixedClock.zone)
val nowZonedDateTime = ZonedDateTime.now(fixedClock) val nowZonedDateTime = ZonedDateTime.now(fixedClock)
val requestList = listOf( val requestList = listOf(
@ -284,15 +284,11 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("LongMethod")
@Test @Test
fun `on LifecycleResume should update state`() = runTest { fun `on LifecycleResume should update state`() = runTest {
val dateTimeFormatter = DateTimeFormatter val nowZonedDateTime = ZonedDateTime.now(fixedClock)
.ofPattern("M/d/yy hh:mm a") val fiveMinZonedDateTime = ZonedDateTime.now(fixedClock).minusMinutes(5)
.withZone(fixedClock.zone) val sixMinZonedDateTime = ZonedDateTime.now(fixedClock).minusMinutes(6)
val nowZonedDateTime = ZonedDateTime.now()
val fiveMinZonedDateTime = ZonedDateTime.now().minusMinutes(5)
val sixMinZonedDateTime = ZonedDateTime.now().minusMinutes(6)
val requestList = listOf( val requestList = listOf(
AuthRequest( AuthRequest(
id = "1", id = "1",
@ -353,12 +349,12 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
PendingRequestsState.ViewState.Content.PendingLoginRequest( PendingRequestsState.ViewState.Content.PendingLoginRequest(
fingerprintPhrase = "pantry-overdue-survive-sleep-jab", fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
platform = "Android", platform = "Android",
timestamp = nowZonedDateTime.format(dateTimeFormatter), timestamp = "10/27/23, 12:00 PM",
), ),
PendingRequestsState.ViewState.Content.PendingLoginRequest( PendingRequestsState.ViewState.Content.PendingLoginRequest(
fingerprintPhrase = "erupt-anew-matchbook-disk-student", fingerprintPhrase = "erupt-anew-matchbook-disk-student",
platform = "iOS", platform = "iOS",
timestamp = fiveMinZonedDateTime.format(dateTimeFormatter), timestamp = "10/27/23, 11:55 AM",
), ),
), ),
), ),

View File

@ -157,7 +157,7 @@ class OtherViewModelTest : BaseViewModelTest() {
mutableVaultLastSyncStateFlow.tryEmit(newSyncTime) mutableVaultLastSyncStateFlow.tryEmit(newSyncTime)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
lastSyncTime = "10/27/2023 12:00 PM", lastSyncTime = "Oct 27, 2023, 12:00 PM",
dialogState = null, dialogState = null,
), ),
awaitItem(), awaitItem(),
@ -236,6 +236,6 @@ private val DEFAULT_STATE = OtherState(
allowScreenCapture = false, allowScreenCapture = false,
allowSyncOnRefresh = false, allowSyncOnRefresh = false,
clearClipboardFrequency = ClearClipboardFrequency.NEVER, clearClipboardFrequency = ClearClipboardFrequency.NEVER,
lastSyncTime = "10/26/2023 12:00 PM", lastSyncTime = "Oct 26, 2023, 12:00 PM",
dialogState = null, dialogState = null,
) )

View File

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.generator.passwordhistory
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.data.repository.model.DataState 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.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.FormatStyle
class PasswordHistoryViewModelTest : BaseViewModelTest() { class PasswordHistoryViewModelTest : BaseViewModelTest() {
@ -171,7 +172,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
passwords = listOf( passwords = listOf(
PasswordHistoryState.GeneratedPassword( PasswordHistoryState.GeneratedPassword(
password = "mockPassword-1", 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( passwords = listOf(
PasswordHistoryState.GeneratedPassword( PasswordHistoryState.GeneratedPassword(
password = "password", password = "password",
date = passwordHistoryView.lastUsedDate.toFormattedPattern( date = passwordHistoryView.lastUsedDate.toFormattedDateTimeStyle(
pattern = "MM/dd/yy h:mm a", dateStyle = FormatStyle.SHORT,
timeStyle = FormatStyle.SHORT,
clock = fixedClock, clock = fixedClock,
), ),
), ),

View File

@ -48,7 +48,7 @@ class SendViewExtensionsTest {
), ),
shareLink = sendUrl, shareLink = sendUrl,
sendName = "mockName-1", sendName = "mockName-1",
deletionDate = "27 Oct, 2023, 12:00PM", deletionDate = "Oct 27, 2023, 12:00 PM",
maxAccessCount = 1, maxAccessCount = 1,
currentAccessCount = 1, currentAccessCount = 1,
notes = "mockNotes-1", notes = "mockNotes-1",
@ -76,7 +76,7 @@ class SendViewExtensionsTest {
), ),
shareLink = sendUrl, shareLink = sendUrl,
sendName = "mockName-2", sendName = "mockName-2",
deletionDate = "27 Oct, 2023, 12:00PM", deletionDate = "Oct 27, 2023, 12:00 PM",
maxAccessCount = 1, maxAccessCount = 1,
currentAccessCount = 1, currentAccessCount = 1,
notes = "mockNotes-2", notes = "mockNotes-2",

View File

@ -169,7 +169,7 @@ fun createCommonContent(
if (isEmpty) { if (isEmpty) {
VaultItemState.ViewState.Content.Common( VaultItemState.ViewState.Content.Common(
name = "mockName", name = "mockName",
lastUpdated = "1/1/70 12:16 AM", lastUpdated = "1/1/70, 12:16 AM",
notes = null, notes = null,
customFields = emptyList(), customFields = emptyList(),
requiresCloneConfirmation = false, requiresCloneConfirmation = false,
@ -186,7 +186,7 @@ fun createCommonContent(
} else { } else {
VaultItemState.ViewState.Content.Common( VaultItemState.ViewState.Content.Common(
name = "mockName", name = "mockName",
lastUpdated = "1/1/70 12:16 AM", lastUpdated = "1/1/70, 12:16 AM",
notes = "Lots of notes", notes = "Lots of notes",
customFields = listOf( customFields = listOf(
FieldView( 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, isPremiumUser = true,
totpCodeItemData = TotpCodeItemData( totpCodeItemData = TotpCodeItemData(
periodSeconds = 30, periodSeconds = 30,

View File

@ -1,17 +1,12 @@
package com.bitwarden.core.data.util package com.bitwarden.core.data.util
import java.time.Clock import java.time.Clock
import java.time.ZoneId import java.time.chrono.IsoChronology
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.format.FormatStyle
import java.time.temporal.TemporalAccessor import java.time.temporal.TemporalAccessor
import java.util.Locale
/**
* 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)
/** /**
* Converts the [TemporalAccessor] to a formatted string based on the provided pattern and timezone. * Converts the [TemporalAccessor] to a formatted string based on the provided pattern and timezone.
@ -19,4 +14,74 @@ fun TemporalAccessor.toFormattedPattern(
fun TemporalAccessor.toFormattedPattern( fun TemporalAccessor.toFormattedPattern(
pattern: String, pattern: String,
clock: Clock = Clock.systemDefaultZone(), 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,
)

View File

@ -4,33 +4,254 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.FormatStyle
import java.util.Locale
class TemporalAccessorExtensionsTest { class TemporalAccessorExtensionsTest {
@Test @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 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 @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 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,
)