Add zoneOrNull for the repeated TimeZone.of guards
This commit is contained in:
@@ -148,16 +148,17 @@ enum class EventFormProblem {
|
||||
fun EventDetail.toEditForm(beginMillis: Long, endMillis: Long, zone: TimeZone): EventForm {
|
||||
// All-day events are date-anchored and carry a nominal "UTC" that is an
|
||||
// anchor, not a location, so they never pin a zone.
|
||||
val pinnedZone = if (instance.isAllDay) {
|
||||
val pinned = if (instance.isAllDay) {
|
||||
null
|
||||
} else {
|
||||
eventTimezone
|
||||
?.takeIf { it != zone.id }
|
||||
// An unparseable id (a malformed sync row) can't be honoured or
|
||||
// shown; fall back to the device zone rather than failing the open.
|
||||
?.takeIf { runCatching { TimeZone.of(it) }.isSuccess }
|
||||
?.let { id -> zoneOrNull(id)?.let { id to it } }
|
||||
}
|
||||
val formZone = pinnedZone?.let { TimeZone.of(it) } ?: zone
|
||||
val pinnedZone = pinned?.first
|
||||
val formZone = pinned?.second ?: zone
|
||||
val begin = Instant.fromEpochMilliseconds(beginMillis)
|
||||
val finish = Instant.fromEpochMilliseconds(endMillis)
|
||||
val (start, end) = if (instance.isAllDay) {
|
||||
@@ -243,8 +244,7 @@ fun EventDetail.toEditSnapshot(beginMillis: Long, endMillis: Long, zone: TimeZon
|
||||
*/
|
||||
fun EventForm.isPinnedAwayFrom(target: TimeZone): Boolean {
|
||||
if (isAllDay) return false
|
||||
val pinned = timezone?.let { runCatching { TimeZone.of(it) }.getOrNull() } ?: return false
|
||||
return pinned.id != target.id
|
||||
return resolvedZone(target).id != target.id
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,12 +17,15 @@ import kotlin.time.Instant
|
||||
import kotlin.time.toJavaInstant
|
||||
import kotlin.time.toKotlinInstant
|
||||
|
||||
/** [id] as a zone, or null when the tz database doesn't know it. */
|
||||
internal fun zoneOrNull(id: String): TimeZone? = runCatching { TimeZone.of(id) }.getOrNull()
|
||||
|
||||
/**
|
||||
* The zone this form's wall-clock times mean, matching what the data layer
|
||||
* resolves them in at write time: the form's own pinned zone, else [deviceZone].
|
||||
*/
|
||||
fun EventForm.resolvedZone(deviceZone: TimeZone): TimeZone =
|
||||
timezone?.let { runCatching { TimeZone.of(it) }.getOrNull() } ?: deviceZone
|
||||
timezone?.let(::zoneOrNull) ?: deviceZone
|
||||
|
||||
/**
|
||||
* This wall-clock time in [zone], at [preferred] when the fall-back hour repeats
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.jeanlucmakiola.calendula.domain.ics
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Instant
|
||||
|
||||
/** RFC 5545 basic format, `yyyyMMddTHHmmss`. */
|
||||
private val BASIC_DATE_TIME = LocalDateTime.Format {
|
||||
date(LocalDate.Formats.ISO_BASIC)
|
||||
char('T')
|
||||
hour()
|
||||
minute()
|
||||
second()
|
||||
}
|
||||
|
||||
/** `yyyyMMdd`. */
|
||||
internal fun LocalDate.toBasic(): String = format(LocalDate.Formats.ISO_BASIC)
|
||||
|
||||
/** `yyyyMMddTHHmmss`, floating. */
|
||||
internal fun LocalDateTime.toBasic(): String = format(BASIC_DATE_TIME)
|
||||
|
||||
/** `yyyyMMddTHHmmssZ`, the instant in UTC. */
|
||||
internal fun Instant.toUtcBasic(): String = toLocalDateTime(TimeZone.UTC).toBasic() + "Z"
|
||||
|
||||
/**
|
||||
* The date in the first eight characters of a basic-format value; whatever
|
||||
* follows (a time, a `Z`) is ignored. Null when those aren't a valid date.
|
||||
*/
|
||||
internal fun parseBasicDate(s: String): LocalDate? = runCatching {
|
||||
LocalDate(s.substring(0, 4).toInt(), s.substring(4, 6).toInt(), s.substring(6, 8).toInt())
|
||||
}.getOrNull()
|
||||
|
||||
/** `yyyyMMddTHHmm[ss]`; the separator isn't checked and missing seconds read as zero. */
|
||||
internal fun parseBasicDateTime(s: String): LocalDateTime? = runCatching {
|
||||
val date = parseBasicDate(s) ?: return null
|
||||
val time = LocalTime(
|
||||
s.substring(9, 11).toInt(),
|
||||
s.substring(11, 13).toInt(),
|
||||
if (s.length >= 15) s.substring(13, 15).toInt() else 0,
|
||||
)
|
||||
LocalDateTime(date, time)
|
||||
}.getOrNull()
|
||||
@@ -2,6 +2,7 @@ package de.jeanlucmakiola.calendula.domain.ics
|
||||
|
||||
import de.jeanlucmakiola.calendula.domain.Availability
|
||||
import de.jeanlucmakiola.calendula.domain.EventStatus
|
||||
import de.jeanlucmakiola.calendula.domain.zoneOrNull
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.LocalTime
|
||||
@@ -343,7 +344,7 @@ class IcsParser(private val deviceZone: TimeZone = TimeZone.currentSystemDefault
|
||||
*/
|
||||
private fun normalizeExDates(lines: List<IcsContentLine>, start: IcsDateTime): List<String> {
|
||||
if (lines.isEmpty()) return emptyList()
|
||||
val startZone = runCatching { TimeZone.of(start.zoneId) }.getOrNull() ?: deviceZone
|
||||
val startZone = zoneOrNull(start.zoneId) ?: deviceZone
|
||||
val startTime = start.instant.toLocalDateTime(startZone).time
|
||||
return lines
|
||||
.flatMap { line -> line.value.split(',').map { line to it.trim() } }
|
||||
@@ -373,7 +374,7 @@ class IcsParser(private val deviceZone: TimeZone = TimeZone.currentSystemDefault
|
||||
val ldt = parseBasicDateTime(token.removeSuffix("Z")) ?: return null
|
||||
val zone = when {
|
||||
token.endsWith("Z") -> TimeZone.UTC
|
||||
else -> line.params["TZID"]?.let { runCatching { TimeZone.of(it) }.getOrNull() }
|
||||
else -> line.params["TZID"]?.let(::zoneOrNull)
|
||||
?: startZone
|
||||
}
|
||||
return ldt.toInstant(zone)
|
||||
@@ -409,7 +410,7 @@ class IcsParser(private val deviceZone: TimeZone = TimeZone.currentSystemDefault
|
||||
if (isUtc) return IcsDateTime(ldt.toInstant(TimeZone.UTC), isAllDay = false, zoneId = "UTC")
|
||||
|
||||
val tzid = line.params["TZID"]
|
||||
val resolved = tzid?.let { id -> runCatching { TimeZone.of(id) }.getOrNull()?.let { id to it } }
|
||||
val resolved = tzid?.let { id -> zoneOrNull(id)?.let { id to it } }
|
||||
if (tzid != null && resolved == null) warnings.add(IcsParseWarning.UnknownTimezone)
|
||||
val (zoneId, zone) = resolved ?: (deviceZone.id to deviceZone)
|
||||
return IcsDateTime(ldt.toInstant(zone), isAllDay = false, zoneId = zoneId)
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.jeanlucmakiola.calendula.domain.ics
|
||||
|
||||
import de.jeanlucmakiola.calendula.domain.Availability
|
||||
import de.jeanlucmakiola.calendula.domain.EventStatus
|
||||
import de.jeanlucmakiola.calendula.domain.zoneOrNull
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.number
|
||||
@@ -68,7 +69,7 @@ class IcsWriter(private val prodId: String = ICS_PROD_ID) {
|
||||
}
|
||||
// Recurring: anchor to wall-clock in the event's own zone.
|
||||
event.recurrenceRule?.isNotBlank() == true -> {
|
||||
val zone = runCatching { TimeZone.of(event.zoneId) }.getOrNull()
|
||||
val zone = zoneOrNull(event.zoneId)
|
||||
if (zone != null) {
|
||||
add("DTSTART;TZID=${event.zoneId}:${localStamp(event.start, zone)}")
|
||||
add("DTEND;TZID=${event.zoneId}:${localStamp(event.end, zone)}")
|
||||
|
||||
@@ -89,6 +89,7 @@ import de.jeanlucmakiola.calendula.domain.RichText
|
||||
import de.jeanlucmakiola.calendula.domain.TimeZoneOption
|
||||
import de.jeanlucmakiola.calendula.domain.timeZoneOptionOf
|
||||
import de.jeanlucmakiola.calendula.domain.zoneDescriptor
|
||||
import de.jeanlucmakiola.calendula.domain.zoneOrNull
|
||||
import de.jeanlucmakiola.floret.components.IconInfoCard
|
||||
import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
|
||||
import de.jeanlucmakiola.floret.identity.predictiveBack
|
||||
@@ -420,7 +421,7 @@ private fun EventDetailContent(
|
||||
// — which a cross-zone event easily does — carries the whole span
|
||||
// in the primary instead, so fall back to it. If the zone can't be
|
||||
// read back at all, drop to just the abbreviation + offset.
|
||||
val originalTime = runCatching { TimeZone.of(zoneOption.id) }.getOrNull()
|
||||
val originalTime = zoneOrNull(zoneOption.id)
|
||||
?.let { rememberWhen(instance, it, locale) }
|
||||
?.let { (primary, secondary) -> secondary ?: primary }
|
||||
Spacer(Modifier.height(2.dp))
|
||||
|
||||
Reference in New Issue
Block a user