Share RRULE part tokenising
This commit is contained in:
1 parent
59a8f5cebe
commit
4a4daaaf76
4 files changed
+35
-28
No files matched your search
@@ -60,13 +60,7 @@ fun parseSimpleRecurrence(
|
|||||||
rrule: String,
|
rrule: String,
|
||||||
zone: TimeZone = TimeZone.currentSystemDefault(),
|
zone: TimeZone = TimeZone.currentSystemDefault(),
|
||||||
): SimpleRecurrence? {
|
): SimpleRecurrence? {
|
||||||
val parts = rrule.removePrefix("RRULE:").split(';')
|
val parts = rruleParts(rrule).associate { part -> (part.key ?: return null) to part.value }
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.associate { token ->
|
|
||||||
val eq = token.indexOf('=')
|
|
||||||
if (eq <= 0) return null
|
|
||||||
token.substring(0, eq).uppercase() to token.substring(eq + 1)
|
|
||||||
}
|
|
||||||
if (parts.keys.any { it !in setOf("FREQ", "INTERVAL", "UNTIL", "COUNT", "WKST", "BYDAY") }) {
|
if (parts.keys.any { it !in setOf("FREQ", "INTERVAL", "UNTIL", "COUNT", "WKST", "BYDAY") }) {
|
||||||
return null
|
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(
|
internal val RRULE_DAY_CODES: Map<DayOfWeek, String> = mapOf(
|
||||||
DayOfWeek.MONDAY to "MO",
|
DayOfWeek.MONDAY to "MO",
|
||||||
DayOfWeek.TUESDAY to "TU",
|
DayOfWeek.TUESDAY to "TU",
|
||||||
|
|||||||
@@ -19,30 +19,30 @@ import kotlinx.datetime.LocalDate
|
|||||||
fun realignRecurrence(rrule: String, oldStart: LocalDate, newStart: LocalDate): String? {
|
fun realignRecurrence(rrule: String, oldStart: LocalDate, newStart: LocalDate): String? {
|
||||||
if (oldStart == newStart) return rrule
|
if (oldStart == newStart) return rrule
|
||||||
val prefix = if (rrule.startsWith("RRULE:")) "RRULE:" else ""
|
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
|
if (parts.isEmpty()) return null
|
||||||
|
var freq = false
|
||||||
var weekly = false
|
var weekly = false
|
||||||
|
var byDay = false
|
||||||
val rebuilt = parts.map { part ->
|
val rebuilt = parts.map { part ->
|
||||||
val eq = part.indexOf('=')
|
val value = part.value.trim()
|
||||||
if (eq <= 0) return null
|
when (part.key) {
|
||||||
val key = part.substring(0, eq).uppercase()
|
|
||||||
val value = part.substring(eq + 1).trim()
|
|
||||||
when (key) {
|
|
||||||
"FREQ" -> {
|
"FREQ" -> {
|
||||||
|
freq = true
|
||||||
weekly = value.equals("WEEKLY", ignoreCase = true)
|
weekly = value.equals("WEEKLY", ignoreCase = true)
|
||||||
part
|
part.raw
|
||||||
}
|
}
|
||||||
"BYDAY" -> {
|
"BYDAY" -> {
|
||||||
|
byDay = true
|
||||||
val old = RRULE_DAY_CODES[oldStart.dayOfWeek] ?: return null
|
val old = RRULE_DAY_CODES[oldStart.dayOfWeek] ?: return null
|
||||||
if (!value.equals(old, ignoreCase = true)) return null
|
if (!value.equals(old, ignoreCase = true)) return null
|
||||||
"BYDAY=${RRULE_DAY_CODES.getValue(newStart.dayOfWeek)}"
|
"BYDAY=${RRULE_DAY_CODES.getValue(newStart.dayOfWeek)}"
|
||||||
}
|
}
|
||||||
"INTERVAL", "COUNT", "UNTIL", "WKST" -> part
|
"INTERVAL", "COUNT", "UNTIL", "WKST" -> part.raw
|
||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// BYDAY is only simple on a weekly rule, matching parseSimpleRecurrence.
|
// BYDAY is only simple on a weekly rule, matching parseSimpleRecurrence.
|
||||||
if (!weekly && parts.any { it.substringBefore('=').trim().uppercase() == "BYDAY" }) return null
|
if (!freq || (byDay && !weekly)) return null
|
||||||
if (parts.none { it.substringBefore('=').trim().uppercase() == "FREQ" }) return null
|
|
||||||
return prefix + rebuilt.joinToString(";")
|
return prefix + rebuilt.joinToString(";")
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.jeanlucmakiola.calendula.domain.ics
|
package de.jeanlucmakiola.calendula.domain.ics
|
||||||
|
|
||||||
|
import de.jeanlucmakiola.calendula.domain.rruleParts
|
||||||
|
|
||||||
private val VALID_FREQ = setOf(
|
private val VALID_FREQ = setOf(
|
||||||
"SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY",
|
"SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY",
|
||||||
)
|
)
|
||||||
@@ -79,13 +81,12 @@ fun sanitizeRrule(raw: String?): SanitizedRrule {
|
|||||||
return kept.takeIf { it.isNotEmpty() }?.joinToString(",")
|
return kept.takeIf { it.isNotEmpty() }?.joinToString(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (part in rule.split(';')) {
|
// A stray separator ("FREQ=DAILY;") normalises away; nothing was lost.
|
||||||
// A stray separator ("FREQ=DAILY;") normalises away; nothing was lost.
|
// A part that carries text but no '=' ("FREQ=WEEKLY;BYDAY") is a part
|
||||||
// A part that carries text but no '=' ("FREQ=WEEKLY;BYDAY") is a part
|
// being dropped, so it falls through and marks the rule repaired.
|
||||||
// being dropped, so it falls through and marks the rule repaired.
|
for (part in rruleParts(raw.orEmpty().trim())) {
|
||||||
if (part.isBlank()) continue
|
val key = part.key?.trim().orEmpty()
|
||||||
val key = part.substringBefore('=', "").trim().uppercase()
|
val value = part.value.trim()
|
||||||
val value = if ('=' in part) part.substringAfter('=').trim() else ""
|
|
||||||
val kept = when {
|
val kept = when {
|
||||||
key.isEmpty() || value.isEmpty() -> null
|
key.isEmpty() || value.isEmpty() -> null
|
||||||
key == "FREQ" ->
|
key == "FREQ" ->
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.font.FontStyle
|
|||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.SimpleRecurrence
|
import de.jeanlucmakiola.calendula.domain.SimpleRecurrence
|
||||||
import de.jeanlucmakiola.calendula.domain.occurrencesSpanYears
|
import de.jeanlucmakiola.calendula.domain.occurrencesSpanYears
|
||||||
|
import de.jeanlucmakiola.calendula.domain.rruleParts
|
||||||
import de.jeanlucmakiola.calendula.domain.ics.parseBasicDate
|
import de.jeanlucmakiola.calendula.domain.ics.parseBasicDate
|
||||||
import de.jeanlucmakiola.calendula.domain.upcomingOccurrences
|
import de.jeanlucmakiola.calendula.domain.upcomingOccurrences
|
||||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||||
@@ -33,10 +34,7 @@ import java.util.Locale
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun recurrenceText(rrule: String, locale: Locale): AnnotatedString {
|
fun recurrenceText(rrule: String, locale: Locale): AnnotatedString {
|
||||||
val parts = rrule.removePrefix("RRULE:").split(';').mapNotNull { token ->
|
val parts = rruleParts(rrule).mapNotNull { part -> part.key?.let { it to part.value } }.toMap()
|
||||||
val eq = token.indexOf('=')
|
|
||||||
if (eq <= 0) null else token.substring(0, eq).uppercase() to token.substring(eq + 1)
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
val freq = parts["FREQ"]?.uppercase()
|
val freq = parts["FREQ"]?.uppercase()
|
||||||
val interval = parts["INTERVAL"]?.toIntOrNull() ?: 1
|
val interval = parts["INTERVAL"]?.toIntOrNull() ?: 1
|
||||||
|
|||||||
Reference in new issue
Block a user