//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // https://www.opensource.org/licenses/mit-license.php // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //////////////////////////////////////////////////////////////////////////////// // Scans all tasks, and for any recurring tasks, determines whether any new // child tasks need to be generated to fill gaps. void handleRecurrence () { // Recurrence can be disabled. // Note: This is currently a workaround for TD-44, TW-1520. if (! Context::getContext ().config.getBoolean ("recurrence")) return; auto tasks = Context::getContext ().tdb2.pending_tasks (); Datetime now; // Look at all tasks and find any recurring ones. for (auto& t : tasks) { if (t.getStatus () == Task::recurring) { // Generate a list of due dates for this recurring task, regardless of // the mask. std::vector due; if (! generateDueDates (t, due)) { // Determine the end date. t.setStatus (Task::deleted); Context::getContext ().tdb2.modify (t); Context::getContext ().footnote (onExpiration (t)); continue; } // Get the mask from the parent task. auto mask = t.get ("mask"); // Iterate over the due dates, and check each against the mask. auto changed = false; unsigned int i = 0; for (auto& d : due) { if (mask.length () <= i) { changed = true; Task rec (t); // Clone the parent. rec.setStatus (Task::pending); // Change the status. rec.set ("uuid", uuid ()); // New UUID. rec.set ("parent", t.get ("uuid")); // Remember mom. rec.setAsNow ("entry"); // New entry date. rec.set ("due", format (d.toEpoch ())); if (t.has ("wait")) { Datetime old_wait (t.get_date ("wait")); Datetime old_due (t.get_date ("due")); Datetime due (d); rec.set ("wait", format ((due + (old_wait - old_due)).toEpoch ())); rec.setStatus (Task::waiting); mask += 'W'; } else { mask += '-'; rec.setStatus (Task::pending); } rec.set ("imask", i); rec.remove ("mask"); // Remove the mask of the parent. // Add the new task to the DB. Context::getContext ().tdb2.add (rec); } ++i; } // Only modify the parent if necessary. if (changed) { t.set ("mask", mask); Context::getContext ().tdb2.modify (t); if (Context::getContext ().verbose ("recur")) Context::getContext ().footnote (format ("Creating recurring task instance '{1}'", t.get ("description"))); } } } } //////////////////////////////////////////////////////////////////////////////// // Determine a start date (due), an optional end date (until), and an increment // period (recur). Then generate a set of corresponding dates. // // Returns false if the parent recurring task is depleted. bool generateDueDates (Task& parent, std::vector & allDue) { // Determine due date, recur period and until date. Datetime due (parent.get_date ("due")); if (due._date == 0) return false; std::string recur = parent.get ("recur"); bool specificEnd = false; Datetime until; if (parent.get ("until") != "") { until = Datetime (parent.get ("until")); specificEnd = true; } auto recurrence_limit = Context::getContext ().config.getInteger ("recurrence.limit"); int recurrence_counter = 0; Datetime now; for (Datetime i = due; ; i = getNextRecurrence (i, recur)) { allDue.push_back (i); if (specificEnd && i > until) { // If i > until, it means there are no more tasks to generate, and if the // parent mask contains all + or X, then there never will be another task // to generate, and this parent task may be safely reaped. auto mask = parent.get ("mask"); if (mask.length () == allDue.size () && mask.find ('-') == std::string::npos) return false; return true; } if (i > now) ++recurrence_counter; if (recurrence_counter >= recurrence_limit) return true; } return true; } //////////////////////////////////////////////////////////////////////////////// Datetime getNextRecurrence (Datetime& current, std::string& period) { auto m = current.month (); auto d = current.day (); auto y = current.year (); auto ho = current.hour (); auto mi = current.minute (); auto se = current.second (); // Some periods are difficult, because they can be vague. if (period == "monthly" || period == "P1M") { if (++m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, ho, mi, se); } else if (period == "weekdays") { auto dow = current.dayOfWeek (); int days; if (dow == 5) days = 3; else if (dow == 6) days = 2; else days = 1; return current + (days * 86400); } else if (unicodeLatinDigit (period[0]) && period[period.length () - 1] == 'm') { int increment = strtol (period.substr (0, period.length () - 1).c_str (), nullptr, 10); if (increment <= 0) throw format ("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment); m += increment; while (m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, ho, mi, se); } else if (period[0] == 'P' && Lexer::isAllDigits (period.substr (1, period.length () - 2)) && period[period.length () - 1] == 'M') { int increment = strtol (period.substr (1, period.length () - 2).c_str (), nullptr, 10); if (increment <= 0) throw format ("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment); m += increment; while (m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d); } else if (period == "quarterly" || period == "P3M") { m += 3; if (m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, ho, mi, se); } else if (unicodeLatinDigit (period[0]) && period[period.length () - 1] == 'q') { int increment = strtol (period.substr (0, period.length () - 1).c_str (), nullptr, 10); if (increment <= 0) throw format ("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment); m += 3 * increment; while (m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, ho, mi, se); } else if (period == "semiannual" || period == "P6M") { m += 6; if (m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, ho, mi, se); } else if (period == "bimonthly" || period == "P2M") { m += 2; if (m > 12) { m -= 12; ++y; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, ho, mi, se); } else if (period == "biannual" || period == "biyearly" || period == "P2Y") { y += 2; return Datetime (y, m, d, ho, mi, se); } else if (period == "annual" || period == "yearly" || period == "P1Y") { y += 1; // If the due data just happens to be 2/29 in a leap year, then simply // incrementing y is going to create an invalid date. if (m == 2 && d == 29) d = 28; return Datetime (y, m, d, ho, mi, se); } // Add the period to current, and we're done. std::string::size_type idx = 0; Duration p; if (! p.parse (period, idx)) throw std::string (format ("The recurrence value '{1}' is not valid.", period)); return current + p.toTime_t (); } //////////////////////////////////////////////////////////////////////////////// // When the status of a recurring child task changes, the parent task must // update it's mask. void updateRecurrenceMask (Task& task) { auto uuid = task.get ("parent"); Task parent; if (uuid != "" && Context::getContext ().tdb2.get (uuid, parent)) { unsigned int index = strtol (task.get ("imask").c_str (), nullptr, 10); auto mask = parent.get ("mask"); if (mask.length () > index) { mask[index] = (task.getStatus () == Task::pending) ? '-' : (task.getStatus () == Task::completed) ? '+' : (task.getStatus () == Task::deleted) ? 'X' : (task.getStatus () == Task::waiting) ? 'W' : '?'; } else { std::string mask; for (unsigned int i = 0; i < index; ++i) mask += "?"; mask += (task.getStatus () == Task::pending) ? '-' : (task.getStatus () == Task::completed) ? '+' : (task.getStatus () == Task::deleted) ? 'X' : (task.getStatus () == Task::waiting) ? 'W' : '?'; } parent.set ("mask", mask); Context::getContext ().tdb2.modify (parent); } } //////////////////////////////////////////////////////////////////////////////// // Delete expired tasks. void handleUntil () { Datetime now; auto tasks = Context::getContext ().tdb2.pending_tasks (); for (auto& t : tasks) { // TODO What about expiring template tasks? if (t.getStatus () == Task::pending && t.has ("until")) { auto until = Datetime (t.get_date ("until")); if (until < now) { Context::getContext ().debug (format ("handleUntil: recurrence expired until {1} < now {2}", until.toISOLocalExtended (), now.toISOLocalExtended ())); t.setStatus (Task::deleted); Context::getContext ().tdb2.modify(t); Context::getContext ().footnote (onExpiration (t)); } } } } ////////////////////////////////////////////////////////////////////////////////