kotlinx.datetimeを使うのをやめる。ISO8601のタイムゾーンオフセットを自前パースする。
This commit is contained in:
parent
f6bd590d46
commit
18ad53ab50
|
@ -174,7 +174,6 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1"
|
||||||
implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
|
implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.1"
|
|
||||||
|
|
||||||
// Anko Layouts
|
// Anko Layouts
|
||||||
// sdk15, sdk19, sdk21, sdk23 are also available
|
// sdk15, sdk19, sdk21, sdk23 are also available
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package jp.juggler.subwaytooter
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class TestKotlinxDateTime {
|
|
||||||
@Test
|
|
||||||
fun kotlinInstantEpoch() {
|
|
||||||
assertEquals(
|
|
||||||
"epoch is zero",
|
|
||||||
0L,
|
|
||||||
Instant.parse("1970-01-01T00:00:00.000Z").toEpochMilliseconds()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun kotlinInstant() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
val tzTokyo = java.util.TimeZone.getTimeZone("Asia/Tokyo")
|
|
||||||
TootStatus.dateFormatFull.timeZone = tzTokyo
|
|
||||||
fun a(src: String, l1Adj:Long, formatted: String) {
|
|
||||||
// using kotlinx.datetime
|
|
||||||
val l2 = TootStatus.parseTime2(src)
|
|
||||||
// using old utc onlt
|
|
||||||
val l1 = TootStatus.parseTime1(src)
|
|
||||||
assertEquals(src, l2, l1+ l1Adj)
|
|
||||||
|
|
||||||
val formattedActual = TootStatus.formatTime(
|
|
||||||
context = context,
|
|
||||||
t = l2,
|
|
||||||
bAllowRelative = false,
|
|
||||||
onlyDate = false,
|
|
||||||
)
|
|
||||||
assertEquals("src formatted", formatted, formattedActual)
|
|
||||||
}
|
|
||||||
|
|
||||||
val adjUtcToTokyo = 1000L * 3600L * -9L
|
|
||||||
|
|
||||||
a("1970-01-01T00:00:00.000Z", 0,"1970-01-01 09:00:00")
|
|
||||||
a("2017-08-26T00:00:00.000Z", 0,"2017-08-26 09:00:00")
|
|
||||||
a("2021-11-14T00:00:00.000Z", 0,"2021-11-14 09:00:00")
|
|
||||||
a("2021-11-14T11:40:45.086Z", 0,"2021-11-14 20:40:45")
|
|
||||||
a("2021-11-13T05:30:39.188+00:00", 0,"2021-11-13 14:30:39")
|
|
||||||
|
|
||||||
a("2021-11-14T12:34:56.0+0900", adjUtcToTokyo,"2021-11-14 12:34:56")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
|
import jp.juggler.util.asciiRegex
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TestTimeParser {
|
||||||
|
@Test
|
||||||
|
fun testEpoch() {
|
||||||
|
assertEquals(
|
||||||
|
"epoch UTC",
|
||||||
|
0L,
|
||||||
|
TootStatus.parseTimeIso8601("1970-01-01T00:00:00.000Z")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEpochJST() {
|
||||||
|
assertEquals(
|
||||||
|
"epoch +0900",
|
||||||
|
-32400000L,
|
||||||
|
TootStatus.parseTimeIso8601("1970-01-01T00:00:00.000+0900")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIso8601TimeZone() {
|
||||||
|
val reTimeZoneSpec = """(?:(Z|[+-])(\d+):?(\d*))?""".asciiRegex()
|
||||||
|
fun a(spec: String, expect: Long) {
|
||||||
|
val gr = reTimeZoneSpec.find(spec)?.groupValues
|
||||||
|
?: error("timezoneSpec parse failed")
|
||||||
|
val actual = TootStatus.parseTimeZoneOffset(
|
||||||
|
gr.elementAtOrNull(1),
|
||||||
|
gr.elementAtOrNull(2),
|
||||||
|
gr.elementAtOrNull(3),
|
||||||
|
)
|
||||||
|
assertEquals("spec=$spec", expect, actual)
|
||||||
|
}
|
||||||
|
a("", 0L)
|
||||||
|
a("Z", 0L)
|
||||||
|
a("+1", 3600000L)
|
||||||
|
a("-1", -3600000L)
|
||||||
|
a("+105", 3900000L)
|
||||||
|
a("-105", -3900000L)
|
||||||
|
a("+1:05", 3900000L)
|
||||||
|
a("-1:05", -3900000L)
|
||||||
|
a("+12:00", 43200000L)
|
||||||
|
a("-12:00", -43200000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIso8601() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val tzTokyo = java.util.TimeZone.getTimeZone("Asia/Tokyo")
|
||||||
|
TootStatus.dateFormatFull.timeZone = tzTokyo
|
||||||
|
fun a(src: String, l1Adj: Long, formatted: String) {
|
||||||
|
// using kotlinx.datetime
|
||||||
|
val l2 = TootStatus.parseTimeIso8601(src)
|
||||||
|
// using old utc onlt
|
||||||
|
val l1 = TootStatus.parseTimeUtc(src)
|
||||||
|
assertEquals(src, l2, l1 + l1Adj)
|
||||||
|
|
||||||
|
val formattedActual = TootStatus.formatTime(
|
||||||
|
context = context,
|
||||||
|
t = l2,
|
||||||
|
bAllowRelative = false,
|
||||||
|
onlyDate = false,
|
||||||
|
)
|
||||||
|
assertEquals("src formatted", formatted, formattedActual)
|
||||||
|
}
|
||||||
|
|
||||||
|
val adjUtcToTokyo = 1000L * 3600L * -9L
|
||||||
|
|
||||||
|
a("1970-01-01T00:00:00.000Z", 0, "1970-01-01 09:00:00")
|
||||||
|
a("2017-08-26T00:00:00.000Z", 0, "2017-08-26 09:00:00")
|
||||||
|
a("2021-11-14T00:00:00.000Z", 0, "2021-11-14 09:00:00")
|
||||||
|
a("2021-11-14T11:40:45.086Z", 0, "2021-11-14 20:40:45")
|
||||||
|
a("2021-11-13T05:30:39.188+00:00", 0, "2021-11-13 14:30:39")
|
||||||
|
|
||||||
|
a("2021-11-14T12:34:56.0+0900", adjUtcToTokyo, "2021-11-14 12:34:56")
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,10 +15,6 @@ import jp.juggler.subwaytooter.table.HighlightWord
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.util.*
|
import jp.juggler.subwaytooter.util.*
|
||||||
import jp.juggler.util.*
|
import jp.juggler.util.*
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.toInstant
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -1186,35 +1182,85 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
|
||||||
private val reTime = """\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)(?:\D+(\d+))?"""
|
private val reTime = """\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)(?:\D+(\d+))?"""
|
||||||
.asciiRegex()
|
.asciiRegex()
|
||||||
|
|
||||||
|
private val reTimeWithZone =
|
||||||
|
"""\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)(?:\D+(\d+))?(?:(Z|[+-])(\d+):?(\d*))?"""
|
||||||
|
.asciiRegex()
|
||||||
|
|
||||||
private val reMSPTime = """\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)"""
|
private val reMSPTime = """\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)"""
|
||||||
.asciiRegex()
|
.asciiRegex()
|
||||||
|
|
||||||
private val reDateTimeWorkaround = """([+-]\d{2})(\d{2})\z""".toRegex()
|
private val tzUtc = TimeZone.getTimeZone("UTC")
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun parseTime1(strTime: String): Long {
|
fun parseTimeUtc(strTime: String): Long {
|
||||||
val gv = reTime.find(strTime)?.groupValues
|
val gv = reTime.find(strTime)?.groupValues
|
||||||
?: error("time format not match.")
|
?: error("time format not match.")
|
||||||
return LocalDateTime(
|
return GregorianCalendar.getInstance()
|
||||||
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
|
.apply {
|
||||||
gv.elementAtOrNull(2)?.toIntOrNull() ?: 1,
|
timeZone = tzUtc
|
||||||
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
|
set(
|
||||||
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
|
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
|
||||||
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
|
(gv.elementAtOrNull(2)?.toIntOrNull() ?: 1) - 1,
|
||||||
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
|
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
|
||||||
(gv.elementAtOrNull(7)?.toIntOrNull() ?: 0) * 1_000_000,
|
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
|
||||||
)
|
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
|
||||||
.toInstant(TimeZone.UTC)
|
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
|
||||||
.toEpochMilliseconds()
|
)
|
||||||
|
set(Calendar.MILLISECOND, (gv.elementAtOrNull(7)?.toIntOrNull() ?: 0))
|
||||||
|
}.timeInMillis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ISO-8601の Z,[+-]HH:mm,[+-]HHmm,[+-]HH 部分を解釈してオフセット(ミリ秒)を返す
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun parseTimeZoneOffset(sign: String?, hArg: String?, mArg: String?): Long {
|
||||||
|
val minutes = when {
|
||||||
|
sign == null || sign == "Z" || hArg == null || hArg.isEmpty() -> {
|
||||||
|
// Z or missing hour part
|
||||||
|
0
|
||||||
|
}
|
||||||
|
mArg != null && mArg.isNotEmpty() -> {
|
||||||
|
// HH:mm or H:m
|
||||||
|
val h = hArg.toInt()
|
||||||
|
val m = mArg.toInt()
|
||||||
|
h * 60 + m
|
||||||
|
}
|
||||||
|
hArg.length >= 3 -> {
|
||||||
|
// HHmm or Hmm
|
||||||
|
val h = hArg.substring(0, hArg.length - 2).toInt()
|
||||||
|
val m = hArg.substring(hArg.length - 2).toInt()
|
||||||
|
h * 60 + m
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// HH or H
|
||||||
|
val h = hArg.toInt()
|
||||||
|
h * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minutes.toLong() * 60000L * (if (sign == "-") -1L else 1L)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun parseTime2(strTime: String): Long {
|
fun parseTimeIso8601(strTime: String): Long {
|
||||||
// https://github.com/Kotlin/kotlinx-datetime/issues/139
|
val gv = reTimeWithZone.find(strTime)?.groupValues
|
||||||
val src = reDateTimeWorkaround.replace(strTime, "\$1:\$2")
|
?: error("time format not match.")
|
||||||
|
return GregorianCalendar.getInstance()
|
||||||
return Instant.parse(src).toEpochMilliseconds()
|
.apply {
|
||||||
|
timeZone = tzUtc
|
||||||
|
set(
|
||||||
|
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
|
||||||
|
(gv.elementAtOrNull(2)?.toIntOrNull() ?: 1) - 1,
|
||||||
|
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
|
||||||
|
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
|
||||||
|
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
|
||||||
|
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
|
||||||
|
)
|
||||||
|
set(Calendar.MILLISECOND, (gv.elementAtOrNull(7)?.toIntOrNull() ?: 0))
|
||||||
|
}.timeInMillis -
|
||||||
|
parseTimeZoneOffset(
|
||||||
|
gv.elementAtOrNull(8),
|
||||||
|
gv.elementAtOrNull(9),
|
||||||
|
gv.elementAtOrNull(10),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 時刻を解釈してエポック秒(ミリ単位)を返す
|
// 時刻を解釈してエポック秒(ミリ単位)を返す
|
||||||
|
@ -1227,16 +1273,16 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
|
||||||
return parseTime("${gv[1]}T00:00:00.000Z")
|
return parseTime("${gv[1]}T00:00:00.000Z")
|
||||||
}
|
}
|
||||||
|
|
||||||
// kotlinx-datetime で雑にパース
|
// タイムゾーン指定を考慮したパース
|
||||||
try {
|
try {
|
||||||
return parseTime2(strTime)
|
return parseTimeIso8601(strTime)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.w(ex, "parseTime2 failed. $strTime")
|
log.w(ex, "parseTime2 failed. $strTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 古い処理にフォールバック
|
// 古い処理にフォールバック
|
||||||
try {
|
try {
|
||||||
return parseTime1(strTime)
|
return parseTimeUtc(strTime)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.w(ex, "parseTime1 failed. $strTime")
|
log.w(ex, "parseTime1 failed. $strTime")
|
||||||
}
|
}
|
||||||
|
@ -1249,17 +1295,19 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
|
||||||
try {
|
try {
|
||||||
val gv = reMSPTime.find(strTime)?.groupValues
|
val gv = reMSPTime.find(strTime)?.groupValues
|
||||||
?: error("time format not match.")
|
?: error("time format not match.")
|
||||||
return LocalDateTime(
|
return GregorianCalendar.getInstance()
|
||||||
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
|
.apply {
|
||||||
gv.elementAtOrNull(2)?.toIntOrNull() ?: 1,
|
timeZone = tzUtc
|
||||||
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
|
set(
|
||||||
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
|
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
|
||||||
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
|
(gv.elementAtOrNull(2)?.toIntOrNull() ?: 1) - 1,
|
||||||
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
|
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
|
||||||
(500) * 1_000_000,
|
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
|
||||||
)
|
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
|
||||||
.toInstant(TimeZone.UTC)
|
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
|
||||||
.toEpochMilliseconds()
|
)
|
||||||
|
set(Calendar.MILLISECOND, 500)
|
||||||
|
}.timeInMillis
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.w(ex, "parseTimeMSP failed. src=$strTime")
|
log.w(ex, "parseTimeMSP failed. src=$strTime")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue