Create new RRule implementation

This should allow me to support more complex recurring rules once I figure out how to implement the user-facing input
This commit is contained in:
NovaFox161
2026-01-20 00:07:41 -06:00
parent c07d9a3161
commit d2a6e190e2
7 changed files with 135 additions and 24 deletions

View File

@@ -9,10 +9,9 @@ import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.dreamexposure.discal.client.commands.SlashCommand
import org.dreamexposure.discal.core.business.*
import org.dreamexposure.discal.core.enums.event.EventColor
import org.dreamexposure.discal.core.enums.event.EventFrequency
import org.dreamexposure.discal.core.logger.LOGGER
import org.dreamexposure.discal.core.`object`.event.Recurrence
import org.dreamexposure.discal.core.`object`.new.Event
import org.dreamexposure.discal.core.`object`.new.EventRecurrence
import org.dreamexposure.discal.core.`object`.new.EventWizardState
import org.dreamexposure.discal.core.`object`.new.GuildSettings
import org.dreamexposure.discal.core.utils.getCommonMsg
@@ -484,8 +483,8 @@ class EventCommand(
val frequency = event.options[0].getOption("frequency")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.map(EventFrequency.Companion::fromValue)
.orElse(EventFrequency.WEEKLY)
.map(EventRecurrence.Frequency::valueOf)
.orElse(EventRecurrence.Frequency.WEEKLY)
val interval = event.options[0].getOption("interval")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asLong)
@@ -510,7 +509,7 @@ class EventCommand(
.awaitSingle()
val modifiedWizard = if (shouldRecur)
existingWizard.copy(entity = existingWizard.entity.copy(recur = true, recurrence = Recurrence(frequency, interval, count)))
existingWizard.copy(entity = existingWizard.entity.copy(recur = true, recurrence = EventRecurrence(frequency, interval, count)))
else existingWizard.copy(entity = existingWizard.entity.copy(recur = false, recurrence = null))
calendarService.putEventWizard(modifiedWizard)

View File

@@ -400,7 +400,7 @@ class EmbedService(
if (wizard.entity.recurrence != null) builder.addField(
getEmbedMessage("event", "wizard.field.recurrence", settings.locale),
wizard.entity.recurrence.toHumanReadable(),
wizard.entity.recurrence.asHumanReadable(),
true
) else if (wizard.editing && wizard.entity.id != null && wizard.entity.id.contains("_")) builder.addField(
getEmbedMessage("event", "wizard.field.recurrence", settings.locale),

View File

@@ -11,11 +11,7 @@ import org.dreamexposure.discal.core.crypto.KeyGenerator
import org.dreamexposure.discal.core.enums.event.EventColor
import org.dreamexposure.discal.core.exceptions.ApiException
import org.dreamexposure.discal.core.extensions.google.asInstant
import org.dreamexposure.discal.core.`object`.event.Recurrence
import org.dreamexposure.discal.core.`object`.new.Calendar
import org.dreamexposure.discal.core.`object`.new.CalendarMetadata
import org.dreamexposure.discal.core.`object`.new.Event
import org.dreamexposure.discal.core.`object`.new.EventMetadata
import org.dreamexposure.discal.core.`object`.new.*
import org.springframework.stereotype.Component
import java.time.Instant
import java.time.ZoneId
@@ -197,7 +193,7 @@ class GoogleCalendarProviderService(
event.colorId = spec.color.id.toString()
if (spec.recur && spec.recurrence != null)
event.recurrence = listOf(spec.recurrence.toRRule())
event.recurrence = listOf(spec.recurrence.asRRule())
// Create event in google
val response = googleCalendarApiWrapper.createEvent(calendar.metadata, event)
@@ -240,11 +236,11 @@ class GoogleCalendarProviderService(
if (spec.recur != null) {
if (spec.recur) {
//event now recurs, add the RRUle.
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
spec.recurrence?.let { event.recurrence = listOf(it.asRRule()) }
}
} else {
//Recur equals null, so it's not changing whether its recurring, so handle if RRule changes only
spec.recurrence?.let { event.recurrence = listOf(it.toRRule()) }
spec.recurrence?.let { event.recurrence = listOf(it.asRRule()) }
}
// Okay, all values are set, let's patch this event now
@@ -320,7 +316,7 @@ class GoogleCalendarProviderService(
.atZone(calendar.timezone)
.toInstant(),
recur = !baseEvent.recurrence.isNullOrEmpty(),
recurrence = if (baseEvent.recurrence.isNullOrEmpty()) Recurrence() else Recurrence.fromRRule(baseEvent.recurrence[0]),
recurrence = if (baseEvent.recurrence.isNullOrEmpty()) EventRecurrence() else EventRecurrence.fromRRule(baseEvent.recurrence[0]),
image = metadata.imageLink,
timezone = calendar.timezone,
)

View File

@@ -3,6 +3,7 @@ package org.dreamexposure.discal.core.`object`.event
import kotlinx.serialization.Serializable
import org.dreamexposure.discal.core.enums.event.EventFrequency
@Deprecated("Use new EventRecurrence class")
@Serializable
data class Recurrence(
val frequency: EventFrequency = EventFrequency.DAILY,
@@ -25,14 +26,14 @@ data class Recurrence(
val inter = c.replace("INTERVAL=", "")
try {
recur = recur.copy(interval = inter.toInt())
} catch (ignore: NumberFormatException) {
} catch (_: NumberFormatException) {
}
}
c.contains("COUNT=") -> {
val con = c.replaceAfter("COUNT=", "")
try {
recur = recur.copy(count = con.toInt())
} catch (ignore: NumberFormatException) {
} catch (_: NumberFormatException) {
}
}
}

View File

@@ -2,7 +2,6 @@ package org.dreamexposure.discal.core.`object`.new
import discord4j.common.util.Snowflake
import org.dreamexposure.discal.core.enums.event.EventColor
import org.dreamexposure.discal.core.`object`.event.Recurrence
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
@@ -20,7 +19,7 @@ data class Event(
val start: Instant,
val end: Instant,
val recur: Boolean,
val recurrence: Recurrence,
val recurrence: EventRecurrence,
val image: String,
val timezone: ZoneId,
) {
@@ -67,7 +66,7 @@ data class Event(
val start: Instant?,
val end: Instant?,
val recur: Boolean,
val recurrence: Recurrence?,
val recurrence: EventRecurrence?,
val image: String?,
val timezone: ZoneId,
)
@@ -81,7 +80,7 @@ data class Event(
val location: String?,
val image: String?,
val recur: Boolean,
val recurrence: Recurrence?,
val recurrence: EventRecurrence?,
)
data class UpdateSpec(
@@ -94,7 +93,7 @@ data class Event(
val location: String?,
val image: String?,
val recur: Boolean?,
val recurrence: Recurrence?,
val recurrence: EventRecurrence?,
)
}

View File

@@ -0,0 +1,116 @@
package org.dreamexposure.discal.core.`object`.new
import java.time.Month
data class EventRecurrence(
val frequency: Frequency = Frequency.DAILY,
val interval: Int = 1,
val count: Int? = null,
val bySetPos: SetPos? = null,
val byDay: List<Day> = emptyList(),
val byMonthDay: Int? = null,
val byMonth: Month? = null,
) {
// Companion object to translate from rrule string
companion object {
fun fromRRule(rrule: String): EventRecurrence {
var frequency = Frequency.DAILY
var interval = 1
var count: Int? = null
var bySetPos: SetPos? = null
var byDay = emptyList<Day>()
var byMonthDay: Int? = null
var byMonth: Month? = null
rrule.replace("RRULE:", "").split(";").forEach {
when {
it.contains("FREQ=") -> frequency = Frequency.valueOf(it.replace("FREQ=", ""))
it.contains("INTERVAL=") -> try {
interval = it.replace("INTERVAL=", "").toInt()
} catch (_: NumberFormatException) {}
it.contains("COUNT=") -> try {
count = it.replace("COUNT=", "").toInt()
} catch (_: NumberFormatException) {}
it.contains("BYSETPOS=") -> try {
bySetPos = SetPos.entries.firstOrNull { v -> v.value == it.replace("BYSETPOS=", "").toInt() }
} catch (_: NumberFormatException) {}
it.contains("BYDAY=") -> byDay = it.replace("BYDAY=", "").split(",").map { dv -> Day.valueOf(dv) }
it.contains("BYMONTHDAY=") -> try {
byMonthDay = it.replace("BYMONTHDAY=", "").toInt()
} catch (_: NumberFormatException) {}
it.contains("BYMONTH=") -> try {
byMonth = Month.of(it.replace("BYMONTH=", "").toInt())
} catch (_: NumberFormatException) {}
}
}
return EventRecurrence(frequency, interval, count, bySetPos, byDay, byMonthDay, byMonth)
}
}
// Some helpful functions
fun asRRule(): String {
val rrule = StringBuilder()
.append("RRULE:")
.append("FREQ=${frequency.name};")
.append("INTERVAL=${interval};")
if (count != null) rrule.append("COUNT=${count};")
if (bySetPos != null) rrule.append("BYSETPOS=${bySetPos};")
if (byDay.isNotEmpty()) rrule.append("BYDAY=${byDay.joinToString(",")};")
if (byMonthDay != null) rrule.append("BYMONTHDAY=${byMonthDay};")
if (byMonth != null) rrule.append("BYMONTH=${byMonth.value};")
return rrule.toString()
}
fun asHumanReadable(): String {
val builder = StringBuilder()
.append("Repeat ${frequency.name} every $count ")
when (frequency) {
Frequency.DAILY -> builder.append("day(s) ")
Frequency.MONTHLY -> builder.append("month(s) ")
else -> {}
}
if (byMonth != null && byMonthDay != null) builder.append("on ${byMonth.name} $byMonthDay ")
else if (byMonthDay != null) builder.append("$byMonthDay ")
if (byMonth != null && bySetPos != null && byDay.isNotEmpty()) builder.append("on the ${bySetPos.name} ${byDay.joinToString(",")} of ${byMonth.name} ")
else if (bySetPos != null && byDay.isNotEmpty()) builder.append("on the ${bySetPos.name} ${byDay.joinToString(",")} ")
else if (byDay.isNotEmpty()) builder.append("on ${byDay.joinToString(",")} ")
if (count != null) builder.append("End after $count occurrence(s)")
return builder.toString()
}
////////////////////////////
////// Nested classes //////
////////////////////////////
enum class Frequency {
DAILY,
WEEKLY,
MONTHLY,
YEARLY,
}
enum class SetPos(val value: Int) {
FIRST(1),
SECOND(2),
THIRD(3),
FOURTH(4),
LAST(-1),
}
enum class Day(val value: String) {
SUNDAY("SU"),
MONDAY("MO"),
TUESDAY("TU"),
WEDNESDAY("WE"),
THURSDAY("TH"),
FRIDAY("FR"),
SATURDAY("SA"),
}
}

View File

@@ -40,8 +40,8 @@ data class EventV2Model(
isParent = !event.id.contains("_"),
color = event.color.name,
recur = event.recur,
recurrence = event.recurrence,
rrule = event.recurrence.toRRule(),
recurrence = Recurrence.fromRRule(event.recurrence.asRRule()),
rrule = event.recurrence.asRRule(),
image = event.image,
)
}