[pim/kalarm] /: If a recurrence is suppressed by work time/holidays/exception, suppress its sub-repetitions.

David Jarvie null at kde.org
Sat Aug 30 22:21:00 BST 2025


Git commit d36ae6a61bcfc7aead668ec5c142bd40723b54a3 by David Jarvie.
Committed on 30/08/2025 at 21:20.
Pushed by djarvie into branch 'master'.

If a recurrence is suppressed by work time/holidays/exception, suppress its sub-repetitions.

M  +2    -2    CMakeLists.txt
M  +3    -0    Changelog
M  +12   -2    doc/index.docbook
M  +2    -2    src/kalarmapp.cpp
M  +216  -58   src/kalarmcalendar/kaevent.cpp
M  +47   -10   src/kalarmcalendar/kaevent.h
M  +1    -1    src/messagedisplayhelper.cpp

https://invent.kde.org/pim/kalarm/-/commit/d36ae6a61bcfc7aead668ec5c142bd40723b54a3

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 87e9862d0..7a9ce1544 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,9 +9,9 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
 # Whenever KALARM_VERSION changes, set the 3 variables below it to the first
 # KDE Release Service version which will contain that KAlarm release.
 # (This must be the full release version, not an alpha or beta release.)
-set(KALARM_VERSION "3.11.2")
+set(KALARM_VERSION "3.12.0")
 set(KALARM_VERSION_RELEASE_SERVICE_MAJOR "25")
-set(KALARM_VERSION_RELEASE_SERVICE_MINOR "08")
+set(KALARM_VERSION_RELEASE_SERVICE_MINOR "12")
 set(KALARM_VERSION_RELEASE_SERVICE_MICRO "0")
 
 # If KAlarm's version has not changed since the last KDE Release Service version,
diff --git a/Changelog b/Changelog
index 7d06964da..1ad807fe3 100644
--- a/Changelog
+++ b/Changelog
@@ -1,5 +1,8 @@
 KAlarm Change Log
 
+=== Version 3.12.0 (KDE Gear 25.12) --- 30 August 2025 ===
+* If a recurrence is suppressed by work time/holidays/exception, suppress its sub-repetitions.
+
 === Version 3.11.2 (KDE Gear 25.08) --- 14 July 2025 ===
 * Remove excess spacing in Preferences dialogue.
 
diff --git a/doc/index.docbook b/doc/index.docbook
index d3c434ba6..950e1700d 100644
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -39,8 +39,8 @@
 
 <!-- Don't change format of date and version of the documentation -->
 
-<date>2025-03-17</date>
-<releaseinfo>3.11.0 (KDE Gear 25.04)</releaseinfo>
+<date>2025-08-30</date>
+<releaseinfo>3.12.0 (KDE Gear 25.12)</releaseinfo>
 
 <abstract>
 <para>&kalarm; is a personal alarm message, command and email scheduler by &kde;.</para>
@@ -1883,6 +1883,16 @@ weekly recurrence on Thursday at 12:00, and use the Sub-Repetition
 dialog to specify an interval of 1 hour and either a count of 6 or a
 duration of 6 hours.</para>
 
+<note><para>Sub-repetitions are only triggered after a main recurrence
+which actually triggers. If a main recurrence is suppressed by an
+exception date/time or by holiday or working time restrictions, the
+sub-repetitions for that instance of the recurrence will not be
+triggered.</para>
+
+<para>Individual sub-repetitions will also be suppressed if they occur
+at an exception date/time, or during holiday or working time
+restrictions.</para></note>
+
 <para>In the Sub-Repetition dialog which is displayed when you click
 the <guibutton>Sub-Repetition</guibutton> button, check
 <guilabel>Repeat every</guilabel> to set up a repetition, or uncheck
diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp
index 2f061b763..7b41920c8 100644
--- a/src/kalarmapp.cpp
+++ b/src/kalarmapp.cpp
@@ -1909,7 +1909,7 @@ int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniq
                             // It's too late to display the scheduled occurrence.
                             // Find the last previous occurrence of the alarm.
                             DateTime next;
-                            const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
+                            const KAEvent::OccurType type = event.previousOccurrence(now, next, KAEvent::RepeatsP::Return);
                             switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat))
                             {
                                 case KAEvent::OccurType::FirstOrOnly:
@@ -1941,7 +1941,7 @@ int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniq
                             // It's over the maximum interval late.
                             // Find the most recent occurrence of the alarm.
                             DateTime next;
-                            const KAEvent::OccurType type = event.previousOccurrence(now, next, true);
+                            const KAEvent::OccurType type = event.previousOccurrence(now, next, KAEvent::RepeatsP::Return);
                             switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat))
                             {
                                 case KAEvent::OccurType::FirstOrOnly:
diff --git a/src/kalarmcalendar/kaevent.cpp b/src/kalarmcalendar/kaevent.cpp
index fcf2cc469..7d4ebb1b6 100644
--- a/src/kalarmcalendar/kaevent.cpp
+++ b/src/kalarmcalendar/kaevent.cpp
@@ -188,11 +188,13 @@ public:
     DateTime           deferralLimit(KAEvent::DeferLimit* = nullptr) const;
     KAEvent::Flags     flags() const;
     bool               excludedByWorkTimeOrHoliday(const KADateTime& dt) const;
+    bool               excludedByWorkTime(const KADateTime& wdt) const;
+    KAEvent::SubRepExclude repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int count) const;
     bool               setRepetition(const Repetition&);
     DateTime           nextDateTime(KAEvent::NextTypes) const;
     bool               occursAfter(const KADateTime& preDateTime, bool includeRepetitions) const;
     KAEvent::OccurType nextOccurrence(const KADateTime& preDateTime, DateTime& result, KAEvent::Repeats = KAEvent::Repeats::Ignore) const;
-    KAEvent::OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions = false) const;
+    KAEvent::OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, KAEvent::RepeatsP) const;
     void               setRecurrence(const KARecurrence&);
     bool               setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, QDate end, KARecurrence::Feb29Type = KARecurrence::Feb29_None);
     bool               setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, const KADateTime& end, KARecurrence::Feb29Type = KARecurrence::Feb29_None);
@@ -214,7 +216,7 @@ private:
     void               copy(const KAEventPrivate&);
     bool               mayOccurDailyDuringWork(const KADateTime&) const;
     int                nextWorkRepetition(const KADateTime& pre) const;
-    void               calcNextWorkingTime(const DateTime& nextTrigger) const;
+    void               calcNextWorkingTime(const DateTime& nextTrigger, bool skipRepeats) const;
     DateTime           nextWorkingTime() const;
     KAEvent::OccurType nextRecurrence(const KADateTime& preDateTime, DateTime& result) const;
     void               setAudioAlarm(const KCalendarCore::Alarm::Ptr&) const;
@@ -237,6 +239,7 @@ public:
     mutable DateTime   mMainTrigger;       // next trigger time, ignoring reminders and working hours
     mutable DateTime   mAllWorkTrigger;    // next trigger time, taking account of reminders and working hours
     mutable DateTime   mMainWorkTrigger;   // next trigger time, ignoring reminders but taking account of working hours
+    mutable bool       mMainWorkTriggerIsRecur; // whether mMainWorkTrigger is a recurrence, not a sub-repetition
     mutable KAEvent::CmdErr mCommandError{KAEvent::CmdErr::None}; // command execution error last time the alarm triggered
 
     QString            mEventID;           // UID: KCalendarCore::Event unique ID
@@ -1015,6 +1018,7 @@ void KAEventPrivate::copy(const KAEventPrivate& event)
     mMainTrigger             = event.mMainTrigger;
     mAllWorkTrigger          = event.mAllWorkTrigger;
     mMainWorkTrigger         = event.mMainWorkTrigger;
+    mMainWorkTriggerIsRecur  = event.mMainWorkTriggerIsRecur;
     mCommandError            = event.mCommandError;
     mEventID                 = event.mEventID;
     mCustomProperties        = event.mCustomProperties;
@@ -2458,6 +2462,9 @@ DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const
         return DateTime();
     const int offsetToRecur = now.secsTo(nextRecur.effectiveKDateTime());
 
+    bool checkedWorkHol = false;   // whether nextRecur has been checked for working time/holiday
+    bool excludeWorkHol = false;   // whether nextRecur is excluded by working time/holiday
+
     // If desired, check for the first sub-repetition after now.
     DateTime result;
     if (type & KAEvent::NextRepeat)
@@ -2465,27 +2472,47 @@ DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const
         // If the recurrence is before now, find the first sub-repetition after now.
         if (offsetToRecur <= 0)
         {
-            const int repInterval = mRepetition.intervalSeconds();
-            const int count = -offsetToRecur / repInterval + 1;   // first sub-repetition AFTER now
-            if (count <= mRepetition.count())
+            if (type & KAEvent::NextWorkHoliday)
             {
-                const DateTime nextRep = nextRecur.addSecs(repInterval * count);
-                if (!(type & KAEvent::NextWorkHoliday)
-                ||  !excludedByWorkTimeOrHoliday(nextRep.effectiveKDateTime()))
-                    result = nextRep;
+                // If the previous recurrence was excluded by working time/holiday
+                // retrictions, its sub-repetitions are also excluded.
+                excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime());
+                checkedWorkHol = true;
+            }
+            if (!excludeWorkHol)
+            {
+                const int repInterval = mRepetition.intervalSeconds();
+                bool foundRep = false;  // haven't found a sub-repetition ok for working time/holidays yet
+                for (int count = -offsetToRecur / repInterval + 1;   // first sub-repetition AFTER now
+                     count <= mRepetition.count();  ++count)
+                {
+                    const DateTime nextRep = nextRecur.addSecs(repInterval * count);
+                    if (!(type & KAEvent::NextWorkHoliday)
+                    ||  !excludedByWorkTimeOrHoliday(nextRep.effectiveKDateTime()))
+                    {
+                        result = nextRep;
+                        foundRep = true;   // found a sub-repeition ok for working time/holidays
+                        break;
+                    }
+                }
+                if (!foundRep)
+                {
+                    excludeWorkHol = true;   // the recurrence found is excluded by work time/holidays
+                    checkedWorkHol = true;
+                }
             }
         }
     }
 
-    bool checkedWorkHol = false;   // whether nextRecur has been checked for working time/holiday
-    bool excludeWorkHol = false;   // whether nextRecur is excluded by working time/holiday
-
     // If desired, check for the first reminder after now.
     DateTime resultReminder;
     if (type & KAEvent::NextReminder)
     {
-        excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime());
-        checkedWorkHol = true;
+        if (!checkedWorkHol)
+        {
+            excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime());
+            checkedWorkHol = true;
+        }
         if (!excludeWorkHol)
         {
             // Reminders are never returned if the recurrence which they relate to
@@ -2524,9 +2551,9 @@ DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const
             excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime());
         if (excludeWorkHol)
         {
-            // Find first recurrence/repetition which complies with working time/
+            // The recurrence found is excluded by work time or holidays.
+            // Find next recurrence/repetition which complies with working time/
             // holiday restrictions.
-            // The next recurrence found is excluded by work time or holidays.
             // Find a subsequent sub-repetition or recurrence which is not excluded.
             calcTriggerTimes();
             return (type & KAEvent::NextReminder) ? mAllWorkTrigger : mMainWorkTrigger;
@@ -2691,6 +2718,11 @@ void KAEvent::setHolidays()
     KAEventPrivate::mHolidays = &KAEventPrivate::mDummyHolidays;
 }
 
+void KAEvent::setHolidays()
+{
+    KAEventPrivate::mHolidays = &KAEventPrivate::mDummyHolidays;
+}
+
 void KAEvent::setWorkTimeOnly(bool wto)
 {
     d->mWorkTimeOnly = wto;
@@ -2720,12 +2752,44 @@ bool KAEventPrivate::excludedByWorkTimeOrHoliday(const KADateTime& dt) const
     if (!mWorkTimeOnly)
         return false;
     const KADateTime wdt = dt.toTimeSpec(mWorkDayTimeSpec);
+    return excludedByWorkTime(wdt);
+}
+
+/******************************************************************************
+* Check whether a date/time (which must use mWorkDayTimeSpec) conflicts with
+* working hours restrictions for the alarm.
+*/
+bool KAEventPrivate::excludedByWorkTime(const KADateTime& wdt) const
+{
+    Q_ASSERT(wdt.timeSpec() == mWorkDayTimeSpec);
     if (!mWorkDays.testBit(wdt.date().dayOfWeek() - 1))
         return true;
     return !wdt.isDateOnly()
        &&  (wdt.time() < mWorkDayStart  ||  wdt.time() >= mWorkDayEnd);
 }
 
+/******************************************************************************
+* Check whether a sub-repetition conflicts with working hours and/or holiday
+* restrictions for the alarm. If its recurrence conflicts, the sub-repetition
+* automatically conflicts.
+*/
+KAEvent::SubRepExclude KAEvent::repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int count) const
+{
+    return d->repExcludedByWorkTimeOrHoliday(recurDt, count);
+}
+
+KAEvent::SubRepExclude KAEventPrivate::repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int count) const
+{
+    if (!mExcludeHolidays  &&  !mWorkTimeOnly)
+        return KAEvent::SubRepExclude::Ok;
+    // Check the recurrence
+    if (excludedByWorkTimeOrHoliday(recurDt))
+        return KAEvent::SubRepExclude::Recur;
+    // Check the sub-repetition
+    KADateTime repDt(mRepetition.duration(count).end(recurDt.qDateTime()));
+    return excludedByWorkTimeOrHoliday(repDt) ? KAEvent::SubRepExclude::Repeat : KAEvent::SubRepExclude::Ok;
+}
+
 /******************************************************************************
 * Set new working days and times.
 * Increment a counter so that working-time-only alarms can detect that they
@@ -3387,7 +3451,7 @@ KAEvent::OccurType KAEventPrivate::nextOccurrence(const KADateTime& preDateTime,
             // However, if the intervals between recurrences vary, we could possibly
             // have missed a later recurrence which fits the criterion, so check again.
             DateTime dt;
-            const KAEvent::OccurType newType = previousOccurrence(repeatDT.effectiveKDateTime(), dt, false);
+            const KAEvent::OccurType newType = previousOccurrence(repeatDT.effectiveKDateTime(), dt, KAEvent::RepeatsP::Ignore);
             if (dt > result)
             {
                 type = newType;
@@ -3419,13 +3483,13 @@ KAEvent::OccurType KAEventPrivate::nextOccurrence(const KADateTime& preDateTime,
 * last previous repetition is returned if appropriate.
 * 'result' = date/time of previous occurrence, or invalid date/time if none.
 */
-KAEvent::OccurType KAEvent::previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions) const
+KAEvent::OccurType KAEvent::previousOccurrence(const KADateTime& afterDateTime, DateTime& result, RepeatsP o) const
 {
-    return d->previousOccurrence(afterDateTime, result, includeRepetitions);
+    return d->previousOccurrence(afterDateTime, result, o);
 }
 
 KAEvent::OccurType KAEventPrivate::previousOccurrence(const KADateTime& afterDateTime, DateTime& result,
-        bool includeRepetitions) const
+        KAEvent::RepeatsP includeRepetitions) const
 {
     Q_ASSERT(!afterDateTime.isDateOnly());
     if (mStartDateTime >= afterDateTime)
@@ -3460,7 +3524,7 @@ KAEvent::OccurType KAEventPrivate::previousOccurrence(const KADateTime& afterDat
             type = KAEvent::OccurType::LastRecur;
     }
 
-    if (includeRepetitions  &&  mRepetition)
+    if (includeRepetitions == KAEvent::RepeatsP::Return  &&  mRepetition)
     {
         // Find the latest repetition which is before the specified time.
         const int repetition = mRepetition.previousRepeatCount(result.effectiveKDateTime(), afterDateTime);
@@ -4019,6 +4083,7 @@ void KAEventPrivate::dumpDebug() const
     qCDebug(KALARMCAL_LOG) << "-- mMainTrigger:" << mMainTrigger.toString();
     qCDebug(KALARMCAL_LOG) << "-- mAllWorkTrigger:" << mAllWorkTrigger.toString();
     qCDebug(KALARMCAL_LOG) << "-- mMainWorkTrigger:" << mMainWorkTrigger.toString();
+    qCDebug(KALARMCAL_LOG) << "-- mMainWorkTriggerIsRecur:" << mMainWorkTriggerIsRecur;
     qCDebug(KALARMCAL_LOG) << "-- mCategory:" << mCategory;
     qCDebug(KALARMCAL_LOG) << "-- mName:" << mName;
     if (mCategory == CalEvent::TEMPLATE)
@@ -4507,9 +4572,26 @@ void KAEventPrivate::calcTriggerTimes() const
                 :                                             mMainTrigger.addMins(-mReminderMinutes);
     // If only-during-working-time is set and it recurs, it won't actually trigger
     // unless it falls during working hours.
-    if ((!mWorkTimeOnly && !excludeHolidays)
-    ||  !recurs
-    ||  !excludedByWorkTimeOrHoliday(mMainTrigger.kDateTime()))
+    bool excluded = false;   // whether next occurrence is excluded by working time/holidays
+    bool skipRepeats = false;   // whether next sub-rep is excluded by recurrence working time/holidays
+    if (recurs  &&  (mWorkTimeOnly || excludeHolidays))
+    {
+        // Check if current recurrence is excluded by working time/holidays.
+        if (mNextRepeat && mRepetition)
+        {
+            // The next trigger is a sub-repetition.
+            KAEvent::SubRepExclude excl = repExcludedByWorkTimeOrHoliday(mainDateTime(false).kDateTime(), mNextRepeat);
+            skipRepeats = (excl == KAEvent::SubRepExclude::Recur);
+            excluded = (excl != KAEvent::SubRepExclude::Ok);
+        }
+        else
+        {
+            // The next trigger is a recurrence.
+            excluded = excludedByWorkTimeOrHoliday(mMainTrigger.kDateTime());
+            skipRepeats = excluded;
+        }
+    }
+    if (!excluded)
     {
         // It only occurs once, or it complies with any working hours/holiday
         // restrictions.
@@ -4525,7 +4607,7 @@ void KAEventPrivate::calcTriggerTimes() const
         if (!excludeHolidays)
         {
             // There are no holiday restrictions.
-            calcNextWorkingTime(mMainTrigger);
+            calcNextWorkingTime(mMainTrigger, skipRepeats);
         }
         else if (mHolidays->isValid())
         {
@@ -4534,12 +4616,15 @@ void KAEventPrivate::calcTriggerTimes() const
             KADateTime kdt;
             for (int i = 0;  i < 20;  ++i)
             {
-                calcNextWorkingTime(nextTrigger);
+                calcNextWorkingTime(nextTrigger, skipRepeats);
                 if (!mHolidays->isHoliday(mMainWorkTrigger.date()))
                     return;    // found a non-holiday occurrence
                 kdt = mMainWorkTrigger.effectiveKDateTime();
                 kdt.setTime(QTime(23, 59, 59));
-                const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, KAEvent::Repeats::Return);
+                // If mMainWorkTrigger is a recurrence, skip its sub-repetitions.
+                // If it's a sub-repetition, look for the next sub-repetition.
+                KAEvent::Repeats repType = mMainWorkTriggerIsRecur ? KAEvent::Repeats::Ignore : KAEvent::Repeats::Return;
+                const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, repType);
                 if (!nextTrigger.isValid())
                     break;
                 if (!excludedByWorkTimeOrHoliday(nextTrigger.kDateTime()))
@@ -4555,14 +4640,15 @@ void KAEventPrivate::calcTriggerTimes() const
     }
     else if (excludeHolidays  &&  mHolidays->isValid())
     {
-        // Holidays are excluded.
+        // Holidays are excluded, but there are no working time restrictions.
         DateTime nextTrigger = mMainTrigger;
         KADateTime kdt;
         for (int i = 0;  i < 20;  ++i)
         {
             kdt = nextTrigger.effectiveKDateTime();
             kdt.setTime(QTime(23, 59, 59));
-            const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, KAEvent::Repeats::Return);
+            KAEvent::Repeats repType = skipRepeats ? KAEvent::Repeats::Ignore : KAEvent::Repeats::Return;
+            const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, repType);
             if (!nextTrigger.isValid())
                 break;
             if (!mHolidays->isHoliday(nextTrigger.date()))
@@ -4572,18 +4658,24 @@ void KAEventPrivate::calcTriggerTimes() const
                 mAllWorkTrigger = (type & KAEvent::OccurType::Repeat) ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder);
                 return;   // found a non-holiday occurrence
             }
+            // If nextTrigger is a recurrence, skip its sub-repetitions.
+            skipRepeats = !(type & KAEvent::OccurType::Repeat);
         }
         mMainWorkTrigger = mAllWorkTrigger = DateTime();
     }
 }
 
 /******************************************************************************
-* Return the time of the next scheduled occurrence of the event during working
+* Set the time of the next scheduled occurrence of the event during working
 * hours, for an alarm which is restricted to working hours.
+* mMainWorkTriggerIsRecur is set to indicate whether mMainWorkTrigger is a
+* recurrence or a sub-repetition.
 * On entry, 'nextTrigger' = the next recurrence or repetition (as returned by
 * mainDateTime(true) ).
+* 'skipRepeats' is true if the next occurrence is a sub-repetition and the
+* current recurrence is excluded by working time/holidays.
 */
-void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
+void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger, bool skipRepeats) const
 {
     qCDebug(KALARMCAL_LOG) << "next=" << nextTrigger.kDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M"));
 
@@ -4636,6 +4728,8 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
         ||  nDayPos == 1)
         {
             // It recurs on the same day each week
+            if (skipRepeats)
+                return;    // it recurs on a non-working day
             if (!mRepetition || weeklyRepeat)
                 return;    // any repetitions are also weekly
 
@@ -4644,7 +4738,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             // on a working day.
             KADateTime dt(nextTriggerWT.kDateTime().addDays(1));
             dt.setTime(QTime(0, 0, 0));
-            previousOccurrence(dt, newdt, false);
+            previousOccurrence(dt, newdt, KAEvent::RepeatsP::Ignore);
             if (!newdt.isValid())
                 return;    // this should never happen
             kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
@@ -4657,18 +4751,23 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                     break;
                 if (!repeatNum)
                 {
+                    // Have checked all sub-repetitions: now find the next recurrence.
                     nextOccurrence(newdt.kDateTime(), newdt, KAEvent::Repeats::Ignore);
                     if (mWorkDays.testBit(day))
                     {
                         newdt = newdt.toTimeSpec(nextTrigger.timeSpec());
                         mMainWorkTrigger = newdt;
                         mAllWorkTrigger  = mMainWorkTrigger.addMins(-reminder);
+                        mMainWorkTriggerIsRecur = true;
                         return;
                     }
+                    if (skipRepeats)
+                        return;
                     kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
                 }
                 else
                 {
+                    // Check a sub-repetition.
                     const int inc = repeatFreq * repeatNum;
                     if (mWorkDays.testBit((day + inc) % 7))
                     {
@@ -4676,6 +4775,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                         kdt.setDateOnly(true);
                         kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
                         mMainWorkTrigger = mAllWorkTrigger = kdt;
+                        mMainWorkTriggerIsRecur = false;
                         return;
                     }
                 }
@@ -4686,7 +4786,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
         {
             // It's a date-only alarm with either no sub-repetition or a
             // sub-repetition which always falls on the same day of the week
-            // as the recurrence (if any).
+            // as the recurrence.
             unsigned days = 0;
             for (; ;)
             {
@@ -4707,6 +4807,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
             mMainWorkTrigger = kdt;
             mAllWorkTrigger  = kdt.addSecs(-60 * reminder);
+            mMainWorkTriggerIsRecur = true;
             return;
         }
 
@@ -4714,16 +4815,24 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
         // as does the sub-repetition.
         // Find the previous recurrence (as opposed to sub-repetition)
         unsigned days = 1 << (kdt.date().dayOfWeek() - 1);
-        KADateTime dt(nextTriggerWT.kDateTime().addDays(1));
-        dt.setTime(QTime(0, 0, 0));
-        previousOccurrence(dt, newdt, false);
-        if (!newdt.isValid())
-            return;    // this should never happen
-        kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
-        int day = kdt.date().dayOfWeek() - 1;   // Monday = 0
+        int day = 0;
+        if (!skipRepeats)
+        {
+            // The previous recurrence was on a working day, so its remaining
+            // sub-repetitions need to be checked.
+            KADateTime dt(nextTriggerWT.kDateTime().addDays(1));
+            dt.setTime(QTime(0, 0, 0));
+            previousOccurrence(dt, newdt, KAEvent::RepeatsP::Ignore);
+            if (!newdt.isValid())
+                return;    // this should never happen
+            kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+            day = kdt.date().dayOfWeek() - 1;   // Monday = 0
+        }
         for (int repeatNum = mNextRepeat;  ;  repeatNum = 0)
         {
-            while (++repeatNum <= mRepetition.count())
+            // Check sub-repetitions - but only if the recurrence was in
+            // working time.
+            while (!skipRepeats  &&  ++repeatNum <= mRepetition.count())
             {
                 const int inc = repeatFreq * repeatNum;
                 if (mWorkDays.testBit((day + inc) % 7))
@@ -4732,12 +4841,15 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                     kdt.setDateOnly(true);
                     kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
                     mMainWorkTrigger = mAllWorkTrigger = kdt;
+                    mMainWorkTriggerIsRecur = false;
                     return;
                 }
                 if ((days & allDaysMask) == allDaysMask)
                     return;    // found an occurrence on every possible day of the week!?!
                 days |= 1 << day;
             }
+
+            // Get the next recurrence.
             nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore);
             if (!newdt.isValid())
                 return;
@@ -4745,15 +4857,18 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             day = kdt.date().dayOfWeek() - 1;
             if (mWorkDays.testBit(day))
             {
+                // Found a recurrence on a working day.
                 kdt.setDateOnly(true);
                 kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
                 mMainWorkTrigger = kdt;
                 mAllWorkTrigger  = kdt.addSecs(-60 * reminder);
+                mMainWorkTriggerIsRecur = true;
                 return;
             }
             if ((days & allDaysMask) == allDaysMask)
                 return;    // found an occurrence on every possible day of the week!?!
             days |= 1 << day;
+            skipRepeats = true;   // the recurrence is not on a working day
         }
         return;
     }
@@ -4783,7 +4898,9 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
         unsigned days = 0;
         for (; ;)
         {
-            const KAEvent::OccurType type = nextOccurrence(kdt, newdt, KAEvent::Repeats::Return);
+            // Skip sub-repetitions if the recurrence wasn't in working time.
+            KAEvent::Repeats repType = skipRepeats ? KAEvent::Repeats::Ignore : KAEvent::Repeats::Return;
+            const KAEvent::OccurType type = nextOccurrence(kdt, newdt, repType);
             if (!newdt.isValid())
                 return;
             repetition = (type & KAEvent::OccurType::Repeat);
@@ -4797,12 +4914,14 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                 if ((days & allDaysMask) == allDaysMask)
                     return;    // found a recurrence on every possible day of the week!?!
                 days |= 1 << day;
+                skipRepeats = true;   // the recurrence is not on a working day
             }
         }
         mMainWorkTrigger = nextTriggerWT;
         mMainWorkTrigger.setDate(kdt.date());
         mMainWorkTrigger = mMainWorkTrigger.toTimeSpec(nextTrigger.timeSpec());
         mAllWorkTrigger = repetition ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder);
+        mMainWorkTriggerIsRecur = !repetition;
         return;
     }
 
@@ -4832,14 +4951,30 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             // It's a repetition inside a recurrence, each of which occurs
             // at different times of day (bearing in mind that the repetition
             // may occur at daily intervals after each recurrence).
-            // Find the previous recurrence (as opposed to sub-repetition)
             repeatFreq = mRepetition.intervalSeconds();
-            previousOccurrence(kdt.addSecs(1), newdt, false);
-            if (!newdt.isValid())
-                return;    // this should never happen
-            kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
-            repeatNum = kdtRecur.secsTo(kdt) / repeatFreq;
-            kdt = kdtRecur.addSecs(repeatNum * repeatFreq);
+
+            if (skipRepeats)
+            {
+                // The last recurrence was not in working time, so skip its
+                // sub-repetitions.
+                nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore);
+                if (!newdt.isValid())
+                    return;    // this should never happen
+                kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+                skipRepeats = excludedByWorkTime(kdtRecur);
+                repeatNum = 0;
+                kdt = kdtRecur;
+            }
+            else
+            {
+                // Find the previous recurrence (as opposed to sub-repetition)
+                previousOccurrence(kdt.addSecs(1), newdt, KAEvent::RepeatsP::Ignore);
+                if (!newdt.isValid())
+                    return;    // this should never happen
+                kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+                repeatNum = kdtRecur.secsTo(kdt) / repeatFreq;
+                kdt = kdtRecur.addSecs(repeatNum * repeatFreq);
+            }
         }
         else
         {
@@ -4865,7 +5000,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
         int transitionIx = -1;
         for (int n = 0;  n < 7 * 24 * 60;  ++n)
         {
-            if (mRepetition)
+            if (!skipRepeats  &&  mRepetition)
             {
                 // Check the sub-repetitions for this recurrence
                 for (; ;)
@@ -4883,6 +5018,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                         {
                             kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
                             mMainWorkTrigger = mAllWorkTrigger = kdt;
+                            mMainWorkTriggerIsRecur = false;
                             return;
                         }
                     }
@@ -4893,6 +5029,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             if (!newdt.isValid())
                 return;
             kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+            skipRepeats = excludedByWorkTime(kdtRecur);
             dayRecur = kdtRecur.date().dayOfWeek() - 1;   // Monday = 0
             const QTime t = kdtRecur.time();
             if (t >= mWorkDayStart  &&  t < mWorkDayEnd)
@@ -4902,6 +5039,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                     kdtRecur = kdtRecur.toTimeSpec(nextTrigger.timeSpec());
                     mMainWorkTrigger = kdtRecur;
                     mAllWorkTrigger  = kdtRecur.addSecs(-60 * reminder);
+                    mMainWorkTriggerIsRecur = true;
                     return;
                 }
             }
@@ -4922,10 +5060,11 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                     transitionIx = i;
                 if (++transitionIx >= tzTransitions.count())
                     return;
-                previousOccurrence(KADateTime(tzTransitions[transitionIx].atUtc), newdt, false);
+                previousOccurrence(KADateTime(tzTransitions[transitionIx].atUtc), newdt, KAEvent::RepeatsP::Ignore);
                 kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
                 if (finalDate.daysTo(kdtRecur.date()) > 365)
                     return;
+                skipRepeats = excludedByWorkTime(kdtRecur);
                 firstTime = kdtRecur.time();
                 firstOffset = kdtRecur.utcOffset();
                 currentOffset = firstOffset;
@@ -4945,12 +5084,27 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
          * Then, it is still possible that a working time sub-repetition
          * could occur immediately after a seasonal time change.
          */
-        // Find the previous recurrence (as opposed to sub-repetition)
         const int repeatFreq = mRepetition.intervalSeconds();
-        previousOccurrence(kdt.addSecs(1), newdt, false);
-        if (!newdt.isValid())
-            return;    // this should never happen
-        KADateTime kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+        KADateTime kdtRecur;
+        if (skipRepeats)
+        {
+            // The last recurrence was not in working time, so skip its
+            // sub-repetitions.
+            nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore);
+            if (!newdt.isValid())
+                return;    // this should never happen
+            kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+            skipRepeats = excludedByWorkTime(kdtRecur);
+            kdt = kdtRecur;
+        }
+        else
+        {
+            // Find the previous recurrence (as opposed to sub-repetition)
+            previousOccurrence(kdt.addSecs(1), newdt, KAEvent::RepeatsP::Ignore);
+            if (!newdt.isValid())
+                return;    // this should never happen
+            kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+        }
         const bool recurDuringWork = (kdtRecur.time() >= mWorkDayStart  &&  kdtRecur.time() < mWorkDayEnd);
 
         // Use the previous recurrence as a base for checking whether
@@ -4980,7 +5134,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             for (; ;)
             {
                 // Check the sub-repetitions for this recurrence
-                if (repeatsDuringWork >= 0)
+                if (!skipRepeats  &&  repeatsDuringWork >= 0)
                 {
                     for (; ;)
                     {
@@ -5008,6 +5162,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                             {
                                 kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
                                 mMainWorkTrigger = mAllWorkTrigger = kdt;
+                                mMainWorkTriggerIsRecur = false;
                                 return;
                             }
                             repeatsDuringWork = 1;
@@ -5032,6 +5187,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                 kdtRecur = kdtNextRecur;
                 nextOccurrence(kdtRecur, newdt, KAEvent::Repeats::Ignore);
                 kdtNextRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+                skipRepeats = excludedByWorkTime(kdtNextRecur);
                 dateRecur = kdtRecur.date();
                 const int dayRecur = dateRecur.dayOfWeek() - 1;   // Monday = 0
                 if (recurDuringWork  &&  mWorkDays.testBit(dayRecur))
@@ -5039,6 +5195,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
                     kdtRecur = kdtRecur.toTimeSpec(nextTrigger.timeSpec());
                     mMainWorkTrigger = kdtRecur;
                     mAllWorkTrigger  = kdtRecur.addSecs(-60 * reminder);
+                    mMainWorkTriggerIsRecur = true;
                     return;
                 }
                 days |= 1 << dayRecur;
@@ -5056,9 +5213,10 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
             if (++transitionIx >= tzTransitions.count())
                 return;
             kdt = KADateTime(tzTransitions[transitionIx].atUtc);
-            previousOccurrence(kdt, newdt, false);
+            previousOccurrence(kdt, newdt, KAEvent::RepeatsP::Ignore);
             kdt = kdt.toTimeSpec(mWorkDayTimeSpec);
             kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
+            skipRepeats = excludedByWorkTime(kdtRecur);
         }
         return;  // not found - give up
     }
diff --git a/src/kalarmcalendar/kaevent.h b/src/kalarmcalendar/kaevent.h
index 53a94d446..345dae386 100644
--- a/src/kalarmcalendar/kaevent.h
+++ b/src/kalarmcalendar/kaevent.h
@@ -256,14 +256,29 @@ public:
         Repeat            = 0x10           //!< a sub-repetition of an occurrence (bitmask)
     };
 
+    /** Whether a sub-repetition is excluded due to working time/holidays. */
+    enum class SubRepExclude
+    {
+        Ok,      //!< not affected by working time/holidays
+        Recur,   //!< the recurrence is excluded due to working time/holidays
+        Repeat   //!< the sub-repetition (but not the recurrence) is excluded due to working time/holidays
+    };
+
     /** How to treat sub-repetitions in nextOccurrence(). */
     enum class Repeats
     {
         Ignore = 0,    //!< check for recurrences only, ignore sub-repetitions
-        Return,        //!< return a sub-repetition if it's the next occurrence
+        Return,        //!< return a sub-repetition if it's the next/previous occurrence
         RecurBefore    //!< if a sub-repetition is the next occurrence, return the previous recurrence, not the sub-repetition
     };
 
+    /** How to treat sub-repetitions in previousOccurrence(). */
+    enum class RepeatsP
+    {
+        Ignore = 0,    //!< check for recurrences only, ignore sub-repetitions
+        Return         //!< return a sub-repetition if it's the next/previous occurrence
+    };
+
     /** What type of occurrence currently limits how long the alarm can be deferred. */
     enum class DeferLimit
     {
@@ -883,8 +898,8 @@ public:
      *                                     be returned. N.B. Reminders are not displayed for
      *                                     sub-repetitions.
      * - @p type contains NextWorkHoliday: as above but the search continues until a
-     *                                     recurrence is found which occurs during working
-     *                                     hours and not on a holiday.
+     *                                     recurrence is found which complies with any
+     *                                     working hours and holiday restrictions.
      * - @p type contains NextDeferral:    if the event (not a reminder) has been deferred,
      *                                     returns the deferral time.
      *                                     If a reminder has been deferred AND @p type
@@ -1007,6 +1022,9 @@ public:
     /** Clear the holiday data used by all KAEvent instances. */
     static void setHolidays();
 
+    /** Clear the holiday data used by all KAEvent instances. */
+    static void setHolidays();
+
     /** Enable or disable the alarm on non-working days and outside working hours.
      *  Note that this option only has any effect for recurring alarms.
      *  @param wto  true to restrict to working time, false to enable any time
@@ -1034,6 +1052,23 @@ public:
      */
     bool excludedByWorkTimeOrHoliday(const KADateTime& dt) const;
 
+    /** Check whether a sub-repetition conflicts with working hours and/or holiday
+     *  restrictions for the alarm. Note that if its recurrence conflicts, the
+     *  sub-repetition automatically conflicts.
+     *  @note If @p recurDt is date-only, only holidays and/or working days are
+     *        taken account of; working hours are ignored.
+     *  @note No check is made as to whether a recurrence is actually scheduled
+     *        to occur at time @p recurDt.
+     *  @param recurDt  the date/time of the sub-repetition's recurrence.
+     *  @param index    the index to the sub-repetition to check.
+     *  @return whether the alarm is disabled from occurring either because the
+     *               recurrence at @p recurDt is outside working hours (if the alarm
+     *               is working time only) or is during a holiday (if the alarm is
+     *               disabled on holidays), or if the time of the sub-repetition
+     *               is similarly restricted;
+     */
+    SubRepExclude repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int index) const;
+
     /** Set working days and times, to be used by all KAEvent instances.
      *  @param days      bits set to 1 for each working day. Array element 0 = Monday ... 6 = Sunday.
      *  @param start     start time in working day.
@@ -1274,15 +1309,17 @@ public:
      *  @note If the event is date-only, its occurrences are considered to occur
      *        at the start-of-day time when comparing with @p preDateTime.
      *
-     *  @param afterDateTime       the specified date/time.
-     *  @param result              date/time of previous occurrence, or invalid
-     *                             date/time if none.
-     *  @param includeRepetitions  if true and the alarm has a sub-repetition, the
-     *                             last previous repetition is returned if
-     *                             appropriate.
+     *  @param afterDateTime  the specified date/time.
+     *  @param result         date/time of previous occurrence, or invalid
+     *                        date/time if none.
+     *  @param option         = Ignore: only recurrences are returned;
+     *                        = Return: if the alarm has a sub-repetition, the
+     *                                  last previous repetition is returned if
+     *                                  appropriate;
+     *                        Other values must not be specified.
      *  @see nextOccurrence()
      */
-    OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions = false) const;
+    OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, RepeatsP includeRepetitions) const;
 
     /** Set the event to be a copy of the specified event, making the specified
      *  alarm the 'displaying' alarm.
diff --git a/src/messagedisplayhelper.cpp b/src/messagedisplayhelper.cpp
index 0c824df43..d987e9449 100644
--- a/src/messagedisplayhelper.cpp
+++ b/src/messagedisplayhelper.cpp
@@ -113,7 +113,7 @@ MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent, const KAEvent
     {
         if (event.reminderMinutes() < 0)
         {
-            event.previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false);
+            event.previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, KAEvent::RepeatsP::Ignore);
             if (!mDateTime.isValid()  &&  event.repeatAtLogin())
                 mDateTime = alarm.dateTime().addSecs(event.reminderMinutes() * 60);
         }


More information about the kde-doc-english mailing list