Skip to content

Commit

Permalink
refactor: remove old date time parser APIs (WPB-9934) (#3171)
Browse files Browse the repository at this point in the history
Co-authored-by: Mojtaba Chenani <chenani@outlook.com>
  • Loading branch information
yamilmedina and mchenani authored Jul 12, 2024
1 parent 25a5eb4 commit 3a803c8
Show file tree
Hide file tree
Showing 8 changed files with 612 additions and 193 deletions.
115 changes: 115 additions & 0 deletions app/src/androidTest/kotlin/com/wire/android/util/DateTimeUtilTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.util

import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString
import kotlinx.datetime.Clock
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.time.measureTime

@RunWith(AndroidJUnit4::class)
class DateTimeUtilTest {

@Test
fun givenDates_OutputPerformanceForServerDateFormattersInDevice() {
// warmup
val date = Clock.System.now().toIsoDateTimeString()
repeat(ITERATIONS / 2) {
serverDateOld(date)
date.serverDate()
}

// simple date format
val duration1 = measureTime {
repeat(ITERATIONS) {
serverDateOld(date)
}
}

// datetime format
val duration2 = measureTime {
repeat(ITERATIONS) {
date.serverDate()
}
}

Log.d("DateTimeParsersTest", "The duration of using ServerDateOld/SimpleDateFormat was: $duration1")
Log.d("DateTimeParsersTest", "The duration of using ServerDate/LocalDateTimeFormat was: $duration2")
}

@Test
fun givenDates_OutputPerformanceForDeviceDateFormattersInDevice() {
// warmup
val date = Clock.System.now().toIsoDateTimeString()
repeat(ITERATIONS / 2) {
date.deviceDateTimeFormat()
date.deviceDateTimeFormatOld()
}

// Old DateFormat from text api
val duration1 = measureTime {
repeat(ITERATIONS) {
date.deviceDateTimeFormatOld()
}
}

// New DateTimeFormatter from time api
val duration2 = measureTime {
repeat(ITERATIONS) {
date.deviceDateTimeFormat()
}
}

Log.d("DateTimeParsersTest", "The duration of using TextApi/DateFormat was: $duration1")
Log.d("DateTimeParsersTest", "The duration of using TimeApi/DateTimeFormatter was: $duration2")
}

@Test
fun givenDates_OutputPerformanceForMediumDateFormattersInDevice() {
// warmup
val date = Clock.System.now().toIsoDateTimeString()
repeat(ITERATIONS / 2) {
date.formatMediumDateTime()
date.formatMediumDateTimeOld()
}

// Old DateFormat from text api
val duration1 = measureTime {
repeat(ITERATIONS) {
date.formatMediumDateTimeOld()
}
}

// New DateTimeFormatter from time api
val duration2 = measureTime {
repeat(ITERATIONS) {
date.formatMediumDateTime()
}
}

Log.d("DateTimeParsersTest", "The duration of using TextApi/DateFormat was: $duration1")
Log.d("DateTimeParsersTest", "The duration of using TimeApi/DateTimeFormatter was: $duration2")
}

companion object {
const val ITERATIONS = 800_000
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,81 +15,103 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.mapper

@file:Suppress("TooManyFunctions")

package com.wire.android.util

import com.wire.android.appLogger
import kotlinx.datetime.Instant
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import com.wire.android.util.serverDate
import java.time.LocalDate
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone

private val serverDateTimeFormat = SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
Locale.getDefault()
).apply { timeZone = TimeZone.getTimeZone("UTC") }
private val mediumDateTimeFormat = DateFormat
.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
private val longDateShortTimeFormat = DateFormat
.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT)
private val mediumOnlyDateTimeFormat = DateFormat
.getDateInstance(DateFormat.MEDIUM)
private val messageTimeFormatter = DateFormat
.getTimeInstance(DateFormat.SHORT)
.apply { timeZone = TimeZone.getDefault() }

private const val ONE_MINUTE_FROM_MILLIS = 60 * 1000
private const val THIRTY_MINUTES = 30
private const val ONE_WEEK_IN_DAYS = 7
private const val ONE_DAY = 1
private const val FORTY_FIVE_MINUTES_DIFFERENCE = 45
private const val MINIMUM_DAYS_DIFFERENCE = 1

private val readReceiptDateTimeFormat = SimpleDateFormat(
"MMM dd yyyy, hh:mm a",
Locale.getDefault()
).apply { timeZone = TimeZone.getDefault() }

private val fileDateTimeFormat = SimpleDateFormat(
"yyyy-MM-dd-hh-mm-ss",
Locale.getDefault()
).apply { timeZone = TimeZone.getDefault() }

private val fullDateShortTimeFormatter = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.SHORT)
fun String.formatMediumDateTime(): String? =
try {
this.serverDate()?.let { mediumDateTimeFormat.format(it) }
} catch (e: ParseException) {
null
}
fun String.shouldDisplayDatesDifferenceDivider(previousDate: String): Boolean {
val currentDate = this@shouldDisplayDatesDifferenceDivider

fun String.deviceDateTimeFormat(): String? =
try {
this.serverDate()?.let { longDateShortTimeFormat.format(it) }
} catch (e: ParseException) {
null
}
val currentLocalDateTime = currentDate.serverDate()?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime()
val previousLocalDateTime = previousDate.serverDate()?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime()

val differenceInMinutes = ChronoUnit.MINUTES.between(
currentLocalDateTime,
previousLocalDateTime
)

val differenceInDays = ChronoUnit.DAYS.between(
currentLocalDateTime,
previousLocalDateTime
)

return differenceInMinutes > FORTY_FIVE_MINUTES_DIFFERENCE || differenceInDays >= MINIMUM_DAYS_DIFFERENCE
}

fun String.groupedUIMessageDateTime(now: Long): MessageDateTimeGroup? = this
.serverDate()?.let { serverDate ->
val localDate = serverDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
val serverDateInMillis = serverDate.time
val differenceBetweenServerDateAndNow = now - serverDateInMillis
val differenceInMinutes: Long = differenceBetweenServerDateAndNow / ONE_MINUTE_FROM_MILLIS
val isSameDay = isDatesSameDay(date = serverDateInMillis, now = now)
val withinWeek = isDatesWithinWeek(date = serverDateInMillis, now = now)
val isSameYear = isDatesSameYear(date = serverDateInMillis, now = now)

when {
differenceBetweenServerDateAndNow < ONE_MINUTE_FROM_MILLIS -> MessageDateTimeGroup.Now
differenceInMinutes <= THIRTY_MINUTES -> MessageDateTimeGroup.Within30Minutes
differenceInMinutes > THIRTY_MINUTES && isSameDay -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.Today,
date = localDate
)

isYesterday(serverDateInMillis, now) -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.Yesterday,
date = localDate
)

withinWeek -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.WithinWeek,
date = localDate
)

!withinWeek && isSameYear -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.NotWithinWeekButSameYear,
date = localDate
)

fun String.formatFullDateShortTime(): String? =
try {
this.serverDate()?.let { fullDateShortTimeFormatter.format(it) }
} catch (e: ParseException) {
null
else -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.Other,
date = localDate
)
}
}

fun String.serverDate(): Date? = try {
serverDateTimeFormat.parse(this)
} catch (e: ParseException) {
appLogger.e("There was an error parsing the server date")
null
/**
* Verifies if received dates are the same year
*
* @param date: Long - message date
* @param now: Long - current user date when checking the message
*
* @return Boolean
*/
private fun isDatesSameYear(date: Long, now: Long): Boolean =
date.getCalendar().get(Calendar.YEAR) == now.getCalendar().get(Calendar.YEAR)

sealed interface MessageDateTimeGroup {
data object Now : MessageDateTimeGroup
data object Within30Minutes : MessageDateTimeGroup
data class Daily(val type: Type, val date: LocalDate) : MessageDateTimeGroup {
enum class Type {
Today,
Yesterday,
WithinWeek,
NotWithinWeekButSameYear,
Other
}
}
}

/**
Expand Down Expand Up @@ -152,104 +174,3 @@ private fun isDatesWithinWeek(date: Long, now: Long): Boolean =
add(Calendar.DATE, -ONE_WEEK_IN_DAYS)
}
)

/**
* Verifies if received dates are the same year
*
* @param date: Long - message date
* @param now: Long - current user date when checking the message
*
* @return Boolean
*/
private fun isDatesSameYear(date: Long, now: Long): Boolean =
date.getCalendar().get(Calendar.YEAR) == now.getCalendar().get(Calendar.YEAR)

sealed interface MessageDateTimeGroup {
data object Now : MessageDateTimeGroup
data object Within30Minutes : MessageDateTimeGroup
data class Daily(val type: Type, val date: LocalDate) : MessageDateTimeGroup {
enum class Type {
Today,
Yesterday,
WithinWeek,
NotWithinWeekButSameYear,
Other
}
}
}

fun String.uiMessageDateTime(): String? = this
.serverDate()?.let { serverDate ->
messageTimeFormatter.format(serverDate)
}

fun String.shouldDisplayDatesDifferenceDivider(previousDate: String): Boolean {
val currentDate = this@shouldDisplayDatesDifferenceDivider

val currentLocalDateTime = currentDate.serverDate()?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime()
val previousLocalDateTime = previousDate.serverDate()?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime()

val differenceInMinutes = ChronoUnit.MINUTES.between(
currentLocalDateTime,
previousLocalDateTime
)

val differenceInDays = ChronoUnit.DAYS.between(
currentLocalDateTime,
previousLocalDateTime
)

return differenceInMinutes > FORTY_FIVE_MINUTES_DIFFERENCE || differenceInDays >= MINIMUM_DAYS_DIFFERENCE
}

fun String.groupedUIMessageDateTime(now: Long): MessageDateTimeGroup? = this
.serverDate()?.let { serverDate ->
val localDate = serverDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
val serverDateInMillis = serverDate.time
val differenceBetweenServerDateAndNow = now - serverDateInMillis
val differenceInMinutes: Long = differenceBetweenServerDateAndNow / ONE_MINUTE_FROM_MILLIS
val isSameDay = isDatesSameDay(date = serverDateInMillis, now = now)
val withinWeek = isDatesWithinWeek(date = serverDateInMillis, now = now)
val isSameYear = isDatesSameYear(date = serverDateInMillis, now = now)

when {
differenceBetweenServerDateAndNow < ONE_MINUTE_FROM_MILLIS -> MessageDateTimeGroup.Now
differenceInMinutes <= THIRTY_MINUTES -> MessageDateTimeGroup.Within30Minutes
differenceInMinutes > THIRTY_MINUTES && isSameDay -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.Today,
date = localDate
)
isYesterday(serverDateInMillis, now) -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.Yesterday,
date = localDate
)
withinWeek -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.WithinWeek,
date = localDate
)
!withinWeek && isSameYear -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.NotWithinWeekButSameYear,
date = localDate
)
else -> MessageDateTimeGroup.Daily(
type = MessageDateTimeGroup.Daily.Type.Other,
date = localDate
)
}
}

fun Date.toMediumOnlyDateTime(): String = mediumOnlyDateTimeFormat.format(this)

fun Instant.uiReadReceiptDateTime(): String = readReceiptDateTimeFormat.format(Date(this.toEpochMilliseconds()))

fun Instant.fileDateTime(): String = fileDateTimeFormat
.format(Date(this.toEpochMilliseconds()))

fun getCurrentParsedDateTime(): String = mediumDateTimeFormat.format(System.currentTimeMillis())

fun Long.timestampToServerDate(): String? = try {
serverDateTimeFormat.format(Date(this))
} catch (e: ParseException) {
appLogger.e("There was an error parsing the timestamp")
null
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.mapper.MessageDateTimeGroup
import com.wire.android.media.audiomessage.AudioState
import com.wire.android.model.Clickable
import com.wire.android.model.SnackBarMessage
Expand Down Expand Up @@ -152,7 +153,6 @@ import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectMessageDialo
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.MessageDateTimeGroup
import com.wire.android.util.normalizeLink
import com.wire.android.util.serverDate
import com.wire.android.util.ui.PreviewMultipleThemes
Expand Down
Loading

0 comments on commit 3a803c8

Please sign in to comment.