kotlinx.datetimeのISO-8601パーサのワークアラウンドの追加。サイドメニューにタイムゾーンを表示する。

This commit is contained in:
tateisu 2021-11-14 22:23:17 +09:00
parent 04f95c89e3
commit f6bd590d46
5 changed files with 151 additions and 30 deletions

View File

@ -0,0 +1,50 @@
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")
}
}

View File

@ -8,10 +8,7 @@ import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Bundle
import android.os.Handler
import android.view.KeyEvent
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.view.*
import android.widget.HorizontalScrollView
import android.widget.ImageButton
import android.widget.LinearLayout
@ -143,6 +140,8 @@ class ActMain : AppCompatActivity(),
lateinit var handler: Handler
lateinit var appState: AppState
lateinit var sideMenuAdapter: SideMenuAdapter
//////////////////////////////////////////////////////////////////
// 読み取り専用のプロパティ
@ -433,6 +432,9 @@ class ActMain : AppCompatActivity(),
benchmark("onStart total") {
benchmark("reload color") { reloadColors() }
benchmark("reload timezone") { reloadTimeZone() }
sideMenuAdapter.onActivityStart()
// 残りの処理はActivityResultの処理より後回しにしたい
handler.postDelayed(onStartAfter, 1L)
}
@ -707,7 +709,7 @@ class ActMain : AppCompatActivity(),
drawer.addDrawerListener(this)
drawer.setExclusionSize(stripIconSize)
SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)
sideMenuAdapter = SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)
llFormRoot.setPadding(0, 0, 0, screenBottomPadding)

View File

@ -200,7 +200,8 @@ fun ActMain.reloadTimeZone() {
if (tzId.isNotEmpty()) {
tz = TimeZone.getTimeZone(tzId)
}
TootStatus.date_format.timeZone = tz
log.w("reloadTimeZone: tz=${tz.displayName}")
TootStatus.dateFormatFull.timeZone = tz
} catch (ex: Throwable) {
log.e(ex, "getTimeZone failed.")
}

View File

@ -25,6 +25,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.subwaytooter.util.openBrowser
@ -32,6 +33,9 @@ import jp.juggler.util.*
import kotlinx.coroutines.*
import org.jetbrains.anko.backgroundColor
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class SideMenuAdapter(
private val actMain: ActMain,
@ -165,10 +169,12 @@ class SideMenuAdapter(
IT_NORMAL(0),
IT_GROUP_HEADER(1),
IT_DIVIDER(2),
IT_VERSION(3)
IT_VERSION(3),
IT_TIMEZONE(4)
}
private class Item(
// 項目の文字列リソース or 0: divider, 1: バージョン表記, 2: タイムゾーン
val title: Int = 0,
val icon: Int = 0,
val action: ActMain.() -> Unit = {}
@ -178,6 +184,7 @@ class SideMenuAdapter(
get() = when {
title == 0 -> ItemType.IT_DIVIDER
title == 1 -> ItemType.IT_VERSION
title == 2 -> ItemType.IT_TIMEZONE
icon == 0 -> ItemType.IT_GROUP_HEADER
else -> ItemType.IT_NORMAL
}
@ -192,6 +199,7 @@ class SideMenuAdapter(
private val list = arrayOf(
Item(icon = R.drawable.ic_info, title = 1),
Item(icon = R.drawable.ic_info, title = 2),
Item(),
Item(title = R.string.account),
@ -398,7 +406,7 @@ class SideMenuAdapter(
resId: Int
): T =
(view ?: actMain.layoutInflater.inflate(resId, parent, false))
as? T ?: error("invalid view type! ${T::class.java.simpleName}")
as? T ?: error("invalid view type! ${T::class.java.simpleName}")
override fun getView(position: Int, view: View?, parent: ViewGroup?): View =
list[position].run {
@ -436,9 +444,52 @@ class SideMenuAdapter(
background = null
text = versionRow
}
ItemType.IT_TIMEZONE ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
textSize = 14f
isAllCaps = false
background = null
text = getTimeZoneString(context)
}
}
}
private fun getTimeZoneString(context: Context): String {
try {
var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone()
if (tzId.isBlank()) {
return tz.displayName +"("+context.getString(R.string.device_timezone)+")"
}
tz = TimeZone.getTimeZone(tzId)
var offset = tz.rawOffset.toLong()
return when (offset) {
0L -> "(UTC\u00B100:00) ${tz.id} ${tz.displayName}"
else -> {
val format = when {
offset > 0 -> "(UTC+%02d:%02d) %s %s"
else -> "(UTC-%02d:%02d) %s %s"
}
offset = abs(offset)
val hours = TimeUnit.MILLISECONDS.toHours(offset)
val minutes =
TimeUnit.MILLISECONDS.toMinutes(offset) - TimeUnit.HOURS.toMinutes(hours)
String.format(format, hours, minutes, tz.id, tz.displayName)
}
}
} catch (ex: Throwable) {
return "(incorrect TimeZone)"
}
}
fun onActivityStart() {
this.notifyDataSetChanged()
}
init {
checkVersion(actMain.applicationContext, handler)

View File

@ -25,6 +25,7 @@ import java.util.*
import java.util.regex.Pattern
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.jvm.Throws
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@ -1188,6 +1189,34 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
private val reMSPTime = """\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)"""
.asciiRegex()
private val reDateTimeWorkaround = """([+-]\d{2})(\d{2})\z""".toRegex()
@Throws(Throwable::class)
fun parseTime1(strTime: String): Long {
val gv = reTime.find(strTime)?.groupValues
?: error("time format not match.")
return LocalDateTime(
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
gv.elementAtOrNull(2)?.toIntOrNull() ?: 1,
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
(gv.elementAtOrNull(7)?.toIntOrNull() ?: 0) * 1_000_000,
)
.toInstant(TimeZone.UTC)
.toEpochMilliseconds()
}
@Throws(Throwable::class)
fun parseTime2(strTime: String): Long {
// https://github.com/Kotlin/kotlinx-datetime/issues/139
val src = reDateTimeWorkaround.replace(strTime, "\$1:\$2")
return Instant.parse(src).toEpochMilliseconds()
}
// 時刻を解釈してエポック秒(ミリ単位)を返す
// 解釈に失敗すると0Lを返す
fun parseTime(strTime: String?): Long {
@ -1200,28 +1229,16 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
// kotlinx-datetime で雑にパース
try {
return Instant.parse(strTime).toEpochMilliseconds()
return parseTime2(strTime)
} catch (ex: Throwable) {
log.w(ex, "Instant.parse failed. $strTime")
log.w(ex, "parseTime2 failed. $strTime")
}
// 古い処理にフォールバック
try {
val gv = reTime.find(strTime)?.groupValues
?: error("time format not match.")
return LocalDateTime(
gv.elementAtOrNull(1)?.toIntOrNull() ?: 1,
gv.elementAtOrNull(2)?.toIntOrNull() ?: 1,
gv.elementAtOrNull(3)?.toIntOrNull() ?: 1,
gv.elementAtOrNull(4)?.toIntOrNull() ?: 0,
gv.elementAtOrNull(5)?.toIntOrNull() ?: 0,
gv.elementAtOrNull(6)?.toIntOrNull() ?: 0,
(gv.elementAtOrNull(7)?.toIntOrNull() ?: 0) * 1_000_000,
)
.toInstant(TimeZone.UTC)
.toEpochMilliseconds()
} catch (ex2: Throwable) {
log.w(ex2, "parseTime failed(2)")
return parseTime1(strTime)
} catch (ex: Throwable) {
log.w(ex, "parseTime1 failed. $strTime")
}
return 0L
@ -1250,10 +1267,10 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
}
@SuppressLint("SimpleDateFormat")
internal val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val dateFormatFull = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
@SuppressLint("SimpleDateFormat")
internal val date_format2 = SimpleDateFormat("yyyy-MM-dd")
val date_format2 = SimpleDateFormat("yyyy-MM-dd")
fun formatTime(
context: Context,
@ -1323,7 +1340,7 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
// fall back to absolute time
}
return formatDate(t, date_format, omitZeroSecond = false, omitYear = false)
return formatDate(t, dateFormatFull, omitZeroSecond = false, omitYear = false)
}
// 告知の開始/終了日付
@ -1358,12 +1375,12 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
val strStart = when {
start <= 0L -> ""
allDay -> formatDate(start, date_format2, omitZeroSecond = false, omitYear = true)
else -> formatDate(start, date_format, omitZeroSecond = true, omitYear = true)
else -> formatDate(start, dateFormatFull, omitZeroSecond = true, omitYear = true)
}
val strEnd = when {
end <= 0L -> ""
allDay -> formatDate(end, date_format2, omitZeroSecond = false, omitYear = true)
else -> formatDate(end, date_format, omitZeroSecond = true, omitYear = true)
else -> formatDate(end, dateFormatFull, omitZeroSecond = true, omitYear = true)
}
// 終了日は先頭と同じ部分を省略する
var skip = 0