fix(common): UTC 상대시간 파싱을 보완한다
This commit is contained in:
@@ -2,7 +2,7 @@ package kr.co.vividnext.sodalive.common
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import java.text.ParseException
|
import java.text.ParsePosition
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -10,8 +10,9 @@ import java.util.Locale
|
|||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
|
fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
|
||||||
val pastMillis = parseServerUtcToMillis(utcText)
|
val parsedTime = parseServerUtcToMillis(utcText)
|
||||||
?: return context.getString(R.string.character_comment_time_just_now)
|
?: return context.getString(R.string.character_comment_time_just_now)
|
||||||
|
val pastMillis = parsedTime.millis
|
||||||
|
|
||||||
val nowMillis = System.currentTimeMillis()
|
val nowMillis = System.currentTimeMillis()
|
||||||
var diff = nowMillis - pastMillis
|
var diff = nowMillis - pastMillis
|
||||||
@@ -21,6 +22,10 @@ fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
|
|||||||
val hour = 60 * minute
|
val hour = 60 * minute
|
||||||
val day = 24 * hour
|
val day = 24 * hour
|
||||||
|
|
||||||
|
if (parsedTime.isIsoText && diff < RECENT_ISO_TIME_GRACE_MILLIS) {
|
||||||
|
return context.getString(R.string.character_comment_time_just_now)
|
||||||
|
}
|
||||||
|
|
||||||
if (diff < minute) {
|
if (diff < minute) {
|
||||||
return context.getString(R.string.character_comment_time_just_now)
|
return context.getString(R.string.character_comment_time_just_now)
|
||||||
}
|
}
|
||||||
@@ -75,19 +80,25 @@ class AndroidUtcRelativeTimeTextFormatter(context: Context) : UtcRelativeTimeTex
|
|||||||
override fun format(utcText: String?): String = formatUtcRelativeTimeText(applicationContext, utcText)
|
override fun format(utcText: String?): String = formatUtcRelativeTimeText(applicationContext, utcText)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseServerUtcToMillis(utcText: String?): Long? {
|
private fun parseServerUtcToMillis(utcText: String?): ParsedServerTime? {
|
||||||
if (utcText.isNullOrBlank()) return null
|
if (utcText.isNullOrBlank()) return null
|
||||||
|
|
||||||
val value = utcText.trim()
|
val value = utcText.trim().normalizeIsoFraction()
|
||||||
if (value.all { it.isDigit() }) {
|
if (value.all { it.isDigit() }) {
|
||||||
return try {
|
return try {
|
||||||
value.toLong()
|
val number = value.toLong()
|
||||||
|
ParsedServerTime(
|
||||||
|
millis = if (value.length <= EPOCH_SECONDS_MAX_LENGTH) number * 1000 else number,
|
||||||
|
isIsoText = false
|
||||||
|
)
|
||||||
} catch (_: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val patterns = listOf(
|
val patterns = listOf(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||||
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||||
"yyyy-MM-dd'T'HH:mm:ss",
|
"yyyy-MM-dd'T'HH:mm:ss",
|
||||||
@@ -99,11 +110,26 @@ private fun parseServerUtcToMillis(utcText: String?): Long? {
|
|||||||
try {
|
try {
|
||||||
val dateFormat = SimpleDateFormat(pattern, Locale.US)
|
val dateFormat = SimpleDateFormat(pattern, Locale.US)
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val parsed: Date? = dateFormat.parse(value)
|
val position = ParsePosition(0)
|
||||||
if (parsed != null) return parsed.time
|
val parsed: Date? = dateFormat.parse(value, position)
|
||||||
} catch (_: ParseException) {
|
if (parsed != null && position.index == value.length) {
|
||||||
|
return ParsedServerTime(millis = parsed.time, isIsoText = true)
|
||||||
|
}
|
||||||
|
} catch (_: RuntimeException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.normalizeIsoFraction(): String {
|
||||||
|
return replace(Regex("""\.(\d{3})\d+"""), ".$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ParsedServerTime(
|
||||||
|
val millis: Long,
|
||||||
|
val isIsoText: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val EPOCH_SECONDS_MAX_LENGTH = 10
|
||||||
|
private const val RECENT_ISO_TIME_GRACE_MILLIS = 10 * 60_000L
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package kr.co.vividnext.sodalive.common
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [28], application = Application::class)
|
||||||
|
class RelativeTimeFormatterTest {
|
||||||
|
|
||||||
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `epoch millis는 기존처럼 실제 경과 분을 표시한다`() {
|
||||||
|
val twoMinutesAgoMillis = System.currentTimeMillis() - 2 * MINUTE
|
||||||
|
|
||||||
|
val text = formatUtcRelativeTimeText(context, twoMinutesAgoMillis.toString())
|
||||||
|
|
||||||
|
assertEquals(context.getString(R.string.character_comment_time_minutes, 2), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `epoch seconds는 초 단위로 파싱한다`() {
|
||||||
|
val twoHoursAgoSeconds = (System.currentTimeMillis() - 2 * HOUR) / 1000
|
||||||
|
|
||||||
|
val text = formatUtcRelativeTimeText(context, twoHoursAgoSeconds.toString())
|
||||||
|
|
||||||
|
assertEquals(context.getString(R.string.character_comment_time_hours, 2), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ISO UTC 문자열은 작성 직후 서버 단말 시계 차이를 방금전으로 보정한다`() {
|
||||||
|
val sixMinutesAgoIso = utcIso(System.currentTimeMillis() - 6 * MINUTE)
|
||||||
|
|
||||||
|
val text = formatUtcRelativeTimeText(context, sixMinutesAgoIso)
|
||||||
|
|
||||||
|
assertEquals(context.getString(R.string.character_comment_time_just_now), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ISO offset 문자열과 6자리 fraction을 UTC 기준으로 파싱한다`() {
|
||||||
|
val value = utcIso(System.currentTimeMillis())
|
||||||
|
.replace(".000Z", ".123456+00:00")
|
||||||
|
|
||||||
|
val text = formatUtcRelativeTimeText(context, value)
|
||||||
|
|
||||||
|
assertEquals(context.getString(R.string.character_comment_time_just_now), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun utcIso(millis: Long): String {
|
||||||
|
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
}.format(Date(millis))
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MINUTE = 60_000L
|
||||||
|
const val HOUR = 60 * MINUTE
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user