Skip to content

Commit 76fbfa1

Browse files
committed
refactor: move recurrence logic to own file
1 parent 5554b75 commit 76fbfa1

File tree

2 files changed

+118
-111
lines changed

2 files changed

+118
-111
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.itsmatok.matcal.data.calendar.events
2+
3+
import java.time.LocalDate
4+
import java.time.temporal.ChronoUnit
5+
6+
object RecurrenceUtil {
7+
8+
fun expandEvents(
9+
events: List<CalendarEvent>,
10+
centerDate: LocalDate = LocalDate.now(),
11+
yearsBuffer: Int = 10
12+
): Map<LocalDate, MutableList<CalendarEvent>> {
13+
val resultMap = mutableMapOf<LocalDate, MutableList<CalendarEvent>>()
14+
val startYear = centerDate.year - yearsBuffer
15+
val endYear = centerDate.year + yearsBuffer
16+
17+
val rangeStartDate = LocalDate.of(startYear, 1, 1)
18+
val rangeEndDate = LocalDate.of(endYear, 12, 31)
19+
20+
events.forEach { event ->
21+
if (event.recurrenceType == RecurrenceType.NONE) {
22+
resultMap.getOrPut(event.date) { mutableListOf() }.add(event)
23+
} else {
24+
generateOccurrences(
25+
event,
26+
rangeStartDate,
27+
rangeEndDate
28+
).forEach { (date, eventCopy) ->
29+
resultMap.getOrPut(date) { mutableListOf() }.add(eventCopy)
30+
}
31+
}
32+
}
33+
return resultMap
34+
}
35+
36+
private fun generateOccurrences(
37+
originalEvent: CalendarEvent,
38+
rangeStartDate: LocalDate,
39+
rangeEndDate: LocalDate
40+
): Map<LocalDate, CalendarEvent> {
41+
val occurrences = mutableMapOf<LocalDate, CalendarEvent>()
42+
43+
if (originalEvent.date.isAfter(rangeEndDate)) return occurrences
44+
45+
var currentDate = originalEvent.date
46+
47+
currentDate = when (originalEvent.recurrenceType) {
48+
RecurrenceType.DAILY -> {
49+
if (currentDate.isBefore(rangeStartDate)) rangeStartDate else currentDate
50+
}
51+
52+
RecurrenceType.WEEKLY -> {
53+
if (currentDate.isBefore(rangeStartDate)) {
54+
val weeks = ChronoUnit.WEEKS.between(currentDate, rangeStartDate)
55+
currentDate.plusWeeks(weeks).let {
56+
if (it.isBefore(rangeStartDate)) it.plusWeeks(1) else it
57+
}
58+
} else currentDate
59+
}
60+
61+
RecurrenceType.MONTHLY -> {
62+
if (currentDate.year < rangeStartDate.year) {
63+
currentDate.withYear(rangeStartDate.year - 1)
64+
} else currentDate
65+
}
66+
67+
RecurrenceType.YEARLY -> {
68+
if (currentDate.year < rangeStartDate.year) {
69+
try {
70+
currentDate.withYear(rangeStartDate.year)
71+
} catch (e: Exception) {
72+
currentDate.withYear(rangeStartDate.year).plusDays(1)
73+
} // Fallback logic
74+
} else currentDate
75+
}
76+
77+
else -> currentDate
78+
}
79+
80+
while (!currentDate.isAfter(rangeEndDate)) {
81+
if (!currentDate.isBefore(rangeStartDate)) {
82+
val validDate = if (originalEvent.recurrenceType == RecurrenceType.YEARLY) {
83+
isValidYearlyDate(originalEvent.date, currentDate.year)
84+
} else currentDate
85+
86+
if (validDate != null) {
87+
occurrences[validDate] = originalEvent.copy(date = validDate)
88+
}
89+
}
90+
91+
currentDate = when (originalEvent.recurrenceType) {
92+
RecurrenceType.DAILY -> currentDate.plusDays(1)
93+
RecurrenceType.WEEKLY -> currentDate.plusWeeks(1)
94+
RecurrenceType.MONTHLY -> currentDate.plusMonths(1)
95+
RecurrenceType.YEARLY -> currentDate.plusYears(1)
96+
else -> rangeEndDate.plusDays(1)
97+
}
98+
}
99+
100+
return occurrences
101+
}
102+
103+
private fun isValidYearlyDate(original: LocalDate, targetYear: Int): LocalDate? {
104+
return try {
105+
original.withYear(targetYear)
106+
} catch (e: Exception) {
107+
// if no leap year, use feb 28
108+
original.withYear(targetYear - 1).plusYears(1)
109+
}
110+
}
111+
}

app/src/main/java/com/itsmatok/matcal/viewmodels/CalendarViewModel.kt

Lines changed: 7 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ import biweekly.ICalendar
99
import com.itsmatok.matcal.data.calendar.events.CalendarEvent
1010
import com.itsmatok.matcal.data.calendar.events.CalendarEventDatabase
1111
import com.itsmatok.matcal.data.calendar.events.EventMapper
12-
import com.itsmatok.matcal.data.calendar.events.RecurrenceType
12+
import com.itsmatok.matcal.data.calendar.events.RecurrenceUtil
1313
import com.itsmatok.matcal.data.calendar.subscriptions.CalendarSubscription
1414
import kotlinx.coroutines.Dispatchers
1515
import kotlinx.coroutines.flow.Flow
1616
import kotlinx.coroutines.flow.MutableStateFlow
1717
import kotlinx.coroutines.flow.asStateFlow
18+
import kotlinx.coroutines.flow.flowOn
1819
import kotlinx.coroutines.flow.map
1920
import kotlinx.coroutines.launch
2021
import kotlinx.coroutines.withContext
2122
import java.net.URL
2223
import java.time.LocalDate
23-
import java.time.temporal.ChronoUnit
2424

2525
class CalendarViewModel(application: Application) : AndroidViewModel(application) {
2626
private val db = CalendarEventDatabase.getDatabase(application)
@@ -30,27 +30,13 @@ class CalendarViewModel(application: Application) : AndroidViewModel(application
3030
private val _selectedDate = MutableStateFlow(LocalDate.now())
3131
val selectedDate = _selectedDate.asStateFlow()
3232

33+
val subscriptions: Flow<List<CalendarSubscription>> = subDao.getAllSubscriptionsFlow()
34+
3335
val events: Flow<Map<LocalDate, List<CalendarEvent>>> =
34-
eventDao.getAllEvents().map { allEvents ->
35-
val resultMap = mutableMapOf<LocalDate, MutableList<CalendarEvent>>()
36-
37-
val currentYear = LocalDate.now().year
38-
val startYear = currentYear - 10
39-
val endYear = currentYear + 10
40-
41-
allEvents.forEach { event ->
42-
if (event.recurrenceType == RecurrenceType.NONE) {
43-
resultMap.getOrPut(event.date) { mutableListOf() }.add(event)
44-
} else {
45-
generateOccurrences(event, startYear, endYear).forEach { entry ->
46-
resultMap.getOrPut(entry.key) { mutableListOf() }.add(entry.value)
47-
}
48-
}
49-
}
50-
resultMap
51-
}
36+
eventDao.getAllEvents()
37+
.map { list -> RecurrenceUtil.expandEvents(list) }
38+
.flowOn(Dispatchers.Default)
5239

53-
val subscriptions: Flow<List<CalendarSubscription>> = subDao.getAllSubscriptionsFlow()
5440

5541
fun onDateSelected(date: LocalDate) {
5642
_selectedDate.value = date
@@ -146,103 +132,13 @@ class CalendarViewModel(application: Application) : AndroidViewModel(application
146132
}
147133
}
148134

149-
150-
private fun generateOccurrences(
151-
originalEvent: CalendarEvent,
152-
startYear: Int,
153-
endYear: Int
154-
): Map<LocalDate, CalendarEvent> {
155-
val occurrences = mutableMapOf<LocalDate, CalendarEvent>()
156-
157-
val rangeStartDate = LocalDate.of(startYear, 1, 1)
158-
val rangeEndDate = LocalDate.of(endYear, 12, 31)
159-
160-
if (originalEvent.date.isAfter(rangeEndDate)) {
161-
return occurrences
162-
}
163-
164-
var currentDate = originalEvent.date
165-
166-
when (originalEvent.recurrenceType) {
167-
RecurrenceType.DAILY -> {
168-
if (currentDate.isBefore(rangeStartDate)) {
169-
currentDate = rangeStartDate
170-
}
171-
172-
while (!currentDate.isAfter(rangeEndDate)) {
173-
occurrences[currentDate] = originalEvent.copy(date = currentDate)
174-
currentDate = currentDate.plusDays(1)
175-
}
176-
}
177-
178-
RecurrenceType.WEEKLY -> {
179-
if (currentDate.isBefore(rangeStartDate)) {
180-
val weeksToSkip = ChronoUnit.WEEKS.between(currentDate, rangeStartDate)
181-
currentDate = currentDate.plusWeeks(weeksToSkip)
182-
183-
if (currentDate.isBefore(rangeStartDate)) {
184-
currentDate = currentDate.plusWeeks(1)
185-
}
186-
}
187-
188-
while (!currentDate.isAfter(rangeEndDate)) {
189-
occurrences[currentDate] = originalEvent.copy(date = currentDate)
190-
currentDate = currentDate.plusWeeks(1)
191-
}
192-
}
193-
194-
RecurrenceType.MONTHLY -> {
195-
if (currentDate.year < startYear) {
196-
currentDate = currentDate.withYear(startYear - 1)
197-
}
198-
199-
while (!currentDate.isAfter(rangeEndDate)) {
200-
if (!currentDate.isBefore(rangeStartDate)) {
201-
occurrences[currentDate] = originalEvent.copy(date = currentDate)
202-
}
203-
currentDate = currentDate.plusMonths(1)
204-
}
205-
}
206-
207-
RecurrenceType.YEARLY -> {
208-
var yearIter = if (currentDate.year < startYear) startYear else currentDate.year
209-
210-
while (yearIter <= endYear) {
211-
// leap year
212-
val newDate = try {
213-
currentDate.withYear(yearIter)
214-
} catch (_: Exception) {
215-
// if no feb 29, use feb 28
216-
currentDate.withYear(yearIter - 1).plusYears(1)
217-
}
218-
219-
if (!newDate.isBefore(originalEvent.date)) {
220-
occurrences[newDate] = originalEvent.copy(date = newDate)
221-
}
222-
yearIter++
223-
}
224-
}
225-
226-
RecurrenceType.NONE -> {
227-
if (!currentDate.isBefore(rangeStartDate) && !currentDate.isAfter(rangeEndDate)) {
228-
occurrences[currentDate] = originalEvent
229-
}
230-
}
231-
232-
null -> {}
233-
}
234-
235-
return occurrences
236-
}
237-
238135
private suspend fun processAndSaveEvents(url: String, iCal: ICalendar, sourceName: String) {
239136
val eventsToSync = iCal.events.mapNotNull { vEvent ->
240137
EventMapper.mapVEventToCalendarEvent(vEvent, url, sourceName)
241138
}
242139
eventDao.syncEvents(url, eventsToSync)
243140
}
244141

245-
246142
private suspend fun showToast(message: String) {
247143
withContext(Dispatchers.Main) {
248144
Toast.makeText(getApplication(), message, Toast.LENGTH_SHORT).show()

0 commit comments

Comments
 (0)