//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2021, 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 //////////////////////////////////////////////////////////////////////////////// // Checklist: // - last: Most recently generated instance integer. The first instance // generated is '1'. // - Sync must merge duplicate N. // - Remove recurrence.limit. Now always 1. It is not something that can be done // with rtype:chained tasks. // - Handle until. //////////////////////////////////////////////////////////////////////////////// // Given an old-style task, upgrades it perfectly. // Note: Works for both parent and child. static Task upgradeTask (const Task&) { Task upgraded; // TODO Convert 'mask' to 'last' <-- length (mask) // TODO Convert 'parent' to 'template'. // TODO Convert 'imask' to 'index'. // TODO Add 'rtype:periodic'. return upgraded; } //////////////////////////////////////////////////////////////////////////////// // Calculates the due date for a new instance N. static Datetime generateNextDueDate ( const Datetime& first, const std::string& period, const int n) { auto y = first.year (); auto m = first.month (); auto d = first.day (); auto hh = first.hour (); auto mm = first.minute (); auto ss = first.second (); Duration dur (period); auto normalized = dur.formatISO (); Context::getContext ().debug (" period " + period + " --> " + normalized); if (! dur._year && dur._month && ! dur._weeks && ! dur._day && ! dur._hours && ! dur._minutes && ! dur._seconds) { m += dur._month * n; while (m > 12) { y += 1; m -= 12; } while (! Datetime::valid (y, m, d)) --d; return Datetime (y, m, d, hh, mm, ss); } /* // 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 (), NULL, 10); 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 (0, period.length () - 1).c_str (), NULL, 10); 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 (), NULL, 10); 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 (); */ Datetime due; return due; } //////////////////////////////////////////////////////////////////////////////// // Calculates the due date for a new instance N. static std::vector generateAllDueDates (const Task& templateTask) { std::vector dueDates; // Determine due date, recur period and until date. Datetime due (templateTask.get_date ("due")); Context::getContext ().debug (" due " + due.toISOLocalExtended ()); auto recur = templateTask.get ("recur"); Context::getContext ().debug (" recur " + recur); auto lastN = std::max (1, templateTask.get_int ("last")); Context::getContext ().debug (format (" last {1}", lastN)); bool end_in_sight = false; Datetime until; if (templateTask.get ("until") != "") { until = Datetime (templateTask.get ("until")); end_in_sight = true; Context::getContext ().debug (" until " + until.toISOLocalExtended ()); } auto recurrence_limit = Context::getContext ().config.getInteger ("recurrence.limit"); Context::getContext ().debug (format (" recurrence.limit {1}", recurrence_limit)); int recurrence_counter = 0; Datetime now; while (1) { Datetime nextDue = generateNextDueDate (due, recur, lastN); // TODO Safety. if (dueDates.size () && dueDates.back () == nextDue) break; // If nextDue > until, it means there are no more tasks to generate, so // this templateTask is finished. if (end_in_sight && nextDue > until) break; if (nextDue > now) ++recurrence_counter; if (recurrence_counter >= recurrence_limit) break; dueDates.push_back (nextDue); } return dueDates; } //////////////////////////////////////////////////////////////////////////////// static void synthesizeTasks (const Task& templateTask) { Context::getContext ().debug ("synthesizeTasks start"); Context::getContext ().debug (" template " + templateTask.get ("uuid")); // TODO 'due' = starting point // TODO 'recur' = frequency // TODO 'last' = index of most recently synthesized instance auto all = generateAllDueDates (templateTask); for (auto& date : all) Context::getContext ().debug (" date " + date.toISOLocalExtended ()); // TODO Create task instances for each period between N and now. Context::getContext ().debug ("synthesizeTasks end"); } //////////////////////////////////////////////////////////////////////////////// // Generates all necessary recurring task instances. void handleRecurrence2 () { // Note: Disabling recurrence is currently a workaround for TD-44, TW-1520. if (Context::getContext ().config.getBoolean ("recurrence")) for (auto& t : Context::getContext ().tdb2.pending.get_tasks ()) if (t.getStatus () == Task::recurring) synthesizeTasks (t); } //////////////////////////////////////////////////////////////////////////////// // Delete expired tasks. void handleUntil () { Datetime now; auto tasks = Context::getContext ().tdb2.pending.get_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)); } } } } ////////////////////////////////////////////////////////////////////////////////