Share RRULE part tokenising
This commit is contained in:
@@ -60,13 +60,7 @@ fun parseSimpleRecurrence(
|
||||
rrule: String,
|
||||
zone: TimeZone = TimeZone.currentSystemDefault(),
|
||||
): SimpleRecurrence? {
|
||||
val parts = rrule.removePrefix("RRULE:").split(';')
|
||||
.filter { it.isNotBlank() }
|
||||
.associate { token ->
|
||||
val eq = token.indexOf('=')
|
||||
if (eq <= 0) return null
|
||||
token.substring(0, eq).uppercase() to token.substring(eq + 1)
|
||||
}
|
||||
val parts = rruleParts(rrule).associate { part -> (part.key ?: return null) to part.value }
|
||||
if (parts.keys.any { it !in setOf("FREQ", "INTERVAL", "UNTIL", "COUNT", "WKST", "BYDAY") }) {
|
||||
return null
|
||||
}
|
||||
@@ -134,6 +128,20 @@ fun SimpleRecurrence.toRRule(zone: TimeZone = TimeZone.currentSystemDefault()):
|
||||
}
|
||||
}
|
||||
|
||||
/** One `;`-separated part of an RRULE as written; [key] is null when it has no `KEY=`. */
|
||||
internal class RrulePart(val raw: String) {
|
||||
private val eq = raw.indexOf('=')
|
||||
val key: String? = if (eq > 0) raw.substring(0, eq).uppercase() else null
|
||||
val value: String = if (eq > 0) raw.substring(eq + 1) else ""
|
||||
}
|
||||
|
||||
/**
|
||||
* [rule]'s non-blank parts, after an optional leading "RRULE:". What to do
|
||||
* with a malformed one (reject, repair or skip) is the caller's call.
|
||||
*/
|
||||
internal fun rruleParts(rule: String): List<RrulePart> =
|
||||
rule.removePrefix("RRULE:").split(';').filter { it.isNotBlank() }.map(::RrulePart)
|
||||
|
||||
internal val RRULE_DAY_CODES: Map<DayOfWeek, String> = mapOf(
|
||||
DayOfWeek.MONDAY to "MO",
|
||||
DayOfWeek.TUESDAY to "TU",
|
||||
|
||||
@@ -19,30 +19,30 @@ import kotlinx.datetime.LocalDate
|
||||
fun realignRecurrence(rrule: String, oldStart: LocalDate, newStart: LocalDate): String? {
|
||||
if (oldStart == newStart) return rrule
|
||||
val prefix = if (rrule.startsWith("RRULE:")) "RRULE:" else ""
|
||||
val parts = rrule.removePrefix("RRULE:").split(';').filter { it.isNotBlank() }
|
||||
val parts = rruleParts(rrule)
|
||||
if (parts.isEmpty()) return null
|
||||
var freq = false
|
||||
var weekly = false
|
||||
var byDay = false
|
||||
val rebuilt = parts.map { part ->
|
||||
val eq = part.indexOf('=')
|
||||
if (eq <= 0) return null
|
||||
val key = part.substring(0, eq).uppercase()
|
||||
val value = part.substring(eq + 1).trim()
|
||||
when (key) {
|
||||
val value = part.value.trim()
|
||||
when (part.key) {
|
||||
"FREQ" -> {
|
||||
freq = true
|
||||
weekly = value.equals("WEEKLY", ignoreCase = true)
|
||||
part
|
||||
part.raw
|
||||
}
|
||||
"BYDAY" -> {
|
||||
byDay = true
|
||||
val old = RRULE_DAY_CODES[oldStart.dayOfWeek] ?: return null
|
||||
if (!value.equals(old, ignoreCase = true)) return null
|
||||
"BYDAY=${RRULE_DAY_CODES.getValue(newStart.dayOfWeek)}"
|
||||
}
|
||||
"INTERVAL", "COUNT", "UNTIL", "WKST" -> part
|
||||
"INTERVAL", "COUNT", "UNTIL", "WKST" -> part.raw
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
// BYDAY is only simple on a weekly rule, matching parseSimpleRecurrence.
|
||||
if (!weekly && parts.any { it.substringBefore('=').trim().uppercase() == "BYDAY" }) return null
|
||||
if (parts.none { it.substringBefore('=').trim().uppercase() == "FREQ" }) return null
|
||||
if (!freq || (byDay && !weekly)) return null
|
||||
return prefix + rebuilt.joinToString(";")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.jeanlucmakiola.calendula.domain.ics
|
||||
|
||||
import de.jeanlucmakiola.calendula.domain.rruleParts
|
||||
|
||||
private val VALID_FREQ = setOf(
|
||||
"SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY",
|
||||
)
|
||||
@@ -79,13 +81,12 @@ fun sanitizeRrule(raw: String?): SanitizedRrule {
|
||||
return kept.takeIf { it.isNotEmpty() }?.joinToString(",")
|
||||
}
|
||||
|
||||
for (part in rule.split(';')) {
|
||||
// A stray separator ("FREQ=DAILY;") normalises away; nothing was lost.
|
||||
// A part that carries text but no '=' ("FREQ=WEEKLY;BYDAY") is a part
|
||||
// being dropped, so it falls through and marks the rule repaired.
|
||||
if (part.isBlank()) continue
|
||||
val key = part.substringBefore('=', "").trim().uppercase()
|
||||
val value = if ('=' in part) part.substringAfter('=').trim() else ""
|
||||
// A stray separator ("FREQ=DAILY;") normalises away; nothing was lost.
|
||||
// A part that carries text but no '=' ("FREQ=WEEKLY;BYDAY") is a part
|
||||
// being dropped, so it falls through and marks the rule repaired.
|
||||
for (part in rruleParts(raw.orEmpty().trim())) {
|
||||
val key = part.key?.trim().orEmpty()
|
||||
val value = part.value.trim()
|
||||
val kept = when {
|
||||
key.isEmpty() || value.isEmpty() -> null
|
||||
key == "FREQ" ->
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.SimpleRecurrence
|
||||
import de.jeanlucmakiola.calendula.domain.occurrencesSpanYears
|
||||
import de.jeanlucmakiola.calendula.domain.rruleParts
|
||||
import de.jeanlucmakiola.calendula.domain.ics.parseBasicDate
|
||||
import de.jeanlucmakiola.calendula.domain.upcomingOccurrences
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
@@ -33,10 +34,7 @@ import java.util.Locale
|
||||
*/
|
||||
@Composable
|
||||
fun recurrenceText(rrule: String, locale: Locale): AnnotatedString {
|
||||
val parts = rrule.removePrefix("RRULE:").split(';').mapNotNull { token ->
|
||||
val eq = token.indexOf('=')
|
||||
if (eq <= 0) null else token.substring(0, eq).uppercase() to token.substring(eq + 1)
|
||||
}.toMap()
|
||||
val parts = rruleParts(rrule).mapNotNull { part -> part.key?.let { it to part.value } }.toMap()
|
||||
|
||||
val freq = parts["FREQ"]?.uppercase()
|
||||
val interval = parts["INTERVAL"]?.toIntOrNull() ?: 1
|
||||
|
||||
Reference in New Issue
Block a user