@@ -12,8 +12,10 @@ import com.philkes.notallyx.presentation.applySpans
1212import com.philkes.notallyx.utils.decodeToBitmap
1313import java.io.File
1414import java.text.DateFormat
15+ import java.text.SimpleDateFormat
1516import java.util.Calendar
1617import java.util.Date
18+ import java.util.Locale
1719import org.json.JSONArray
1820import org.json.JSONException
1921import org.json.JSONObject
@@ -321,14 +323,55 @@ fun BaseNote.attachmentsDifferFrom(other: BaseNote): Boolean {
321323 other.audios.any { audio -> audios.none { it.name == audio.name } }
322324}
323325
324- fun Repetition.toText (context : Context ): String =
325- when {
326+ fun Reminder.toRepetitionText (context : Context ): String {
327+ val rep = repetition ? : return context.getString(R .string.reminder_no_repetition)
328+ if (rep.unit == RepetitionTimeUnit .MONTHS && rep.occurrence == null && rep.value == 1 ) {
329+ val calendar = dateTime.toCalendar()
330+ val dayOfMonth = calendar.get(Calendar .DAY_OF_MONTH )
331+ val isLastDayOfMonth = dayOfMonth == calendar.getActualMaximum(Calendar .DAY_OF_MONTH )
332+
333+ val prefix =
334+ if (rep.value == 1 ) " "
335+ else
336+ " ${context.getString(R .string.every)} ${rep.value} ${context.getString(R .string.months)} "
337+ val postfix =
338+ if (isLastDayOfMonth)
339+ context.getString(R .string.of_the_month_last, context.getString(R .string.day))
340+ else context.getString(R .string.of_the_month, " $dayOfMonth ." )
341+ return " $prefix $postfix "
342+ }
343+ return rep.toText(context)
344+ }
345+
346+ fun Repetition.toText (context : Context ): String {
347+ if (unit == RepetitionTimeUnit .MONTHS && occurrence != null && dayOfWeek != null ) {
348+ val dayOfWeekStr =
349+ SimpleDateFormat (" EEEE" , Locale .getDefault())
350+ .apply {
351+ val cal = Calendar .getInstance()
352+ cal.set(Calendar .DAY_OF_WEEK , dayOfWeek!! )
353+ }
354+ .format(
355+ Calendar .getInstance().apply { set(Calendar .DAY_OF_WEEK , dayOfWeek!! ) }.time
356+ )
357+ val monthlyOptionText =
358+ when (occurrence) {
359+ - 1 -> context.getString(R .string.of_the_month_last, dayOfWeekStr)
360+ else -> " $occurrence . ${context.getString(R .string.of_the_month, dayOfWeekStr)} "
361+ }
362+ val prefix =
363+ if (value == 1 ) " "
364+ else " ${context.getString(R .string.every)} $value ${context.getString(R .string.months)} "
365+ return " $prefix $monthlyOptionText "
366+ }
367+ return when {
326368 value == 1 && unit == RepetitionTimeUnit .DAYS -> context.getString(R .string.daily)
327369 value == 1 && unit == RepetitionTimeUnit .WEEKS -> context.getString(R .string.weekly)
328370 value == 1 && unit == RepetitionTimeUnit .MONTHS -> context.getString(R .string.monthly)
329371 value == 1 && unit == RepetitionTimeUnit .YEARS -> context.getString(R .string.yearly)
330372 else -> " ${context.getString(R .string.every)} $value ${unit.toText(context)} "
331373 }
374+ }
332375
333376private fun RepetitionTimeUnit.toText (context : Context ): String {
334377 val resId =
@@ -388,19 +431,85 @@ fun Reminder.nextNotification(from: Date = Date()): Date? {
388431 if (repetition == null ) {
389432 return null
390433 }
391- val calendar = dateTime.toCalendar()
392- val field = repetition!! .unit.toCalendarField()
393- val value = repetition!! .value
394434
395- // If from is exactly at dateTime, we want the next one
396- while (true ) {
397- calendar.add(field, value)
398- if (calendar.time.after(from)) {
399- break
435+ val rep = repetition!!
436+ if (rep.unit == RepetitionTimeUnit .MONTHS && rep.occurrence != null && rep.dayOfWeek != null ) {
437+ val next = dateTime.toCalendar()
438+ // Ensure we start from the current date's month or later
439+ val fromCal = from.toCalendar()
440+ if (next.before(fromCal)) {
441+ next.set(Calendar .YEAR , fromCal.get(Calendar .YEAR ))
442+ next.set(Calendar .MONTH , fromCal.get(Calendar .MONTH ))
443+ }
444+
445+ while (true ) {
446+ val targetDate =
447+ findOccurrenceInMonth(
448+ next.get(Calendar .YEAR ),
449+ next.get(Calendar .MONTH ),
450+ rep.occurrence!! ,
451+ rep.dayOfWeek!! ,
452+ dateTime.toCalendar(),
453+ )
454+ if (targetDate.after(fromCal)) {
455+ return targetDate.time
456+ }
457+ next.add(Calendar .MONTH , rep.value)
400458 }
401459 }
402460
403- return calendar.time
461+ val timeDifferenceMillis: Long = from.time - dateTime.time
462+ val intervalsPassed = timeDifferenceMillis / rep.toMillis()
463+ val unitsUntilNext = ((rep.value) * (intervalsPassed + 1 )).toInt()
464+ val reminderStart = dateTime.toCalendar()
465+ reminderStart.add(rep.unit.toCalendarField(), unitsUntilNext)
466+ return reminderStart.time
467+ }
468+
469+ private fun findOccurrenceInMonth (
470+ year : Int ,
471+ month : Int ,
472+ occurrence : Int ,
473+ dayOfWeek : Int ,
474+ originalTime : Calendar ,
475+ ): Calendar {
476+ val cal =
477+ Calendar .getInstance().apply {
478+ set(Calendar .YEAR , year)
479+ set(Calendar .MONTH , month)
480+ set(Calendar .DAY_OF_MONTH , 1 )
481+ set(Calendar .HOUR_OF_DAY , originalTime.get(Calendar .HOUR_OF_DAY ))
482+ set(Calendar .MINUTE , originalTime.get(Calendar .MINUTE ))
483+ set(Calendar .SECOND , originalTime.get(Calendar .SECOND ))
484+ set(Calendar .MILLISECOND , originalTime.get(Calendar .MILLISECOND ))
485+ }
486+
487+ if (occurrence > 0 ) {
488+ // Find first dayOfWeek
489+ while (cal.get(Calendar .DAY_OF_WEEK ) != dayOfWeek) {
490+ cal.add(Calendar .DAY_OF_MONTH , 1 )
491+ }
492+ // Advance to the Nth occurrence
493+ cal.add(Calendar .DAY_OF_MONTH , 7 * (occurrence - 1 ))
494+
495+ // If we advanced into the next month, it means there are fewer than 'occurrence' of that
496+ // day in this month.
497+ // The requirement says "if not, it'll behave like the previous option, so 4th day" for the
498+ // last day,
499+ // but for 1st-4th it should probably stay in the month if it exists.
500+ // Actually, for 1st-4th, they ALWAYS exist in every month. Only 5th might not exist.
501+ if (cal.get(Calendar .MONTH ) != month) {
502+ // Fallback to 4th if 5th doesn't exist (though only 1-4 and -1 are currently planned)
503+ return findOccurrenceInMonth(year, month, occurrence - 1 , dayOfWeek, originalTime)
504+ }
505+ } else if (occurrence == - 1 ) {
506+ // Last occurrence
507+ cal.set(Calendar .DAY_OF_MONTH , cal.getActualMaximum(Calendar .DAY_OF_MONTH ))
508+ while (cal.get(Calendar .DAY_OF_WEEK ) != dayOfWeek) {
509+ cal.add(Calendar .DAY_OF_MONTH , - 1 )
510+ }
511+ }
512+ return cal
404513}
405514
406515fun Reminder.nextRepetition (from : Date = Date ()): Date ? {
0 commit comments