kotlinx.datetimeのISO-8601パーサのワークアラウンドの追加。サイドメニューにタイムゾーンを表示する。
This commit is contained in:
parent
04f95c89e3
commit
f6bd590d46
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
@ -436,8 +444,51 @@ 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)
|
||||
|
|
|
@ -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,25 +1189,10 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
|
|||
private val reMSPTime = """\A(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)"""
|
||||
.asciiRegex()
|
||||
|
||||
// 時刻を解釈してエポック秒(ミリ単位)を返す
|
||||
// 解釈に失敗すると0Lを返す
|
||||
fun parseTime(strTime: String?): Long {
|
||||
if (strTime?.isNotBlank() != true) return 0L
|
||||
private val reDateTimeWorkaround = """([+-]\d{2})(\d{2})\z""".toRegex()
|
||||
|
||||
// last_status_at などでは YYYY-MM-DD になることがある
|
||||
reDate.find(strTime)?.groupValues?.let { gv ->
|
||||
return parseTime("${gv[1]}T00:00:00.000Z")
|
||||
}
|
||||
|
||||
// kotlinx-datetime で雑にパース
|
||||
try {
|
||||
return Instant.parse(strTime).toEpochMilliseconds()
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "Instant.parse failed. $strTime")
|
||||
}
|
||||
|
||||
// 古い処理にフォールバック
|
||||
try {
|
||||
@Throws(Throwable::class)
|
||||
fun parseTime1(strTime: String): Long {
|
||||
val gv = reTime.find(strTime)?.groupValues
|
||||
?: error("time format not match.")
|
||||
return LocalDateTime(
|
||||
|
@ -1220,8 +1206,39 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
|
|||
)
|
||||
.toInstant(TimeZone.UTC)
|
||||
.toEpochMilliseconds()
|
||||
} catch (ex2: Throwable) {
|
||||
log.w(ex2, "parseTime failed(2)")
|
||||
}
|
||||
|
||||
|
||||
@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 {
|
||||
if (strTime?.isNotBlank() != true) return 0L
|
||||
|
||||
// last_status_at などでは YYYY-MM-DD になることがある
|
||||
reDate.find(strTime)?.groupValues?.let { gv ->
|
||||
return parseTime("${gv[1]}T00:00:00.000Z")
|
||||
}
|
||||
|
||||
// kotlinx-datetime で雑にパース
|
||||
try {
|
||||
return parseTime2(strTime)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "parseTime2 failed. $strTime")
|
||||
}
|
||||
|
||||
// 古い処理にフォールバック
|
||||
try {
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue