//////////////////////////////////////////////////////////////////////////////// // task - a command line task list manager. // // Copyright 2006 - 2010, Paul Beckingham. // All rights reserved. // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software // Foundation; either version 2 of the License, or (at your option) any later // version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more // details. // // You should have received a copy of the GNU General Public License along with // this program; if not, write to the // // Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, // Boston, MA // 02110-1301 // USA // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include #include "Context.h" #include "Date.h" #include "Duration.h" #include "text.h" #include "util.h" #include "main.h" #ifdef HAVE_LIBNCURSES #include #endif // Global context for use by all. extern Context context; //////////////////////////////////////////////////////////////////////////////// // Scans all tasks, and for any recurring tasks, determines whether any new // child tasks need to be generated to fill gaps. void handleRecurrence () { std::vector tasks; Filter filter; context.tdb.loadPending (tasks, filter); std::vector modified; // Look at all tasks and find any recurring ones. foreach (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)) { std::cout << "Task " << t->get ("uuid") << " (" << trim (t->get ("description")) << ") has past its 'until' date, and has been deleted." << std::endl; // Determine the end date. char endTime[16]; sprintf (endTime, "%u", (unsigned int) time (NULL)); t->set ("end", endTime); t->setStatus (Task::deleted); context.tdb.update (*t); continue; } // Get the mask from the parent task. std::string mask = t->get ("mask"); // Iterate over the due dates, and check each against the mask. bool changed = false; unsigned int i = 0; foreach (d, due) { if (mask.length () <= i) { mask += '-'; changed = true; Task rec (*t); // Clone the parent. rec.set ("uuid", uuid ()); // New UUID. rec.setStatus (Task::pending); // Shiny. rec.set ("parent", t->get ("uuid")); // Remember mom. char dueDate[16]; sprintf (dueDate, "%u", (unsigned int) d->toEpoch ()); rec.set ("due", dueDate); // Store generated due date. char indexMask[12]; sprintf (indexMask, "%u", (unsigned int) i); rec.set ("imask", indexMask); // Store index into mask. // Add the new task to the vector, for immediate use. modified.push_back (rec); // Add the new task to the DB. context.tdb.add (rec); } ++i; } // Only modify the parent if necessary. if (changed) { t->set ("mask", mask); context.tdb.update (*t); } } else modified.push_back (*t); } tasks = modified; } //////////////////////////////////////////////////////////////////////////////// // 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. Date due (atoi (parent.get ("due").c_str ())); std::string recur = parent.get ("recur"); bool specificEnd = false; Date until; if (parent.get ("until") != "") { until = Date (atoi (parent.get ("until").c_str ())); specificEnd = true; } int recurrence_limit = context.config.getInteger ("recurrence.limit"); int recurrence_counter = 0; Date now; for (Date 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. std::string 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; } //////////////////////////////////////////////////////////////////////////////// Date getNextRecurrence (Date& current, std::string& period) { int m = current.month (); int d = current.day (); int y = current.year (); // Some periods are difficult, because they can be vague. if (period == "monthly") { if (++m > 12) { m -= 12; ++y; } while (! Date::valid (m, d, y)) --d; return Date (m, d, y); } if (period == "weekdays") { int dow = current.dayOfWeek (); int days; if (dow == 5) days = 3; else if (dow == 6) days = 2; else days = 1; return current + (days * 86400); } if (isdigit (period[0]) && period[period.length () - 1] == 'm') { std::string numeric = period.substr (0, period.length () - 1); int increment = atoi (numeric.c_str ()); m += increment; while (m > 12) { m -= 12; ++y; } while (! Date::valid (m, d, y)) --d; return Date (m, d, y); } else if (period == "quarterly") { m += 3; if (m > 12) { m -= 12; ++y; } while (! Date::valid (m, d, y)) --d; return Date (m, d, y); } else if (isdigit (period[0]) && period[period.length () - 1] == 'q') { std::string numeric = period.substr (0, period.length () - 1); int increment = atoi (numeric.c_str ()); m += 3 * increment; while (m > 12) { m -= 12; ++y; } while (! Date::valid (m, d, y)) --d; return Date (m, d, y); } else if (period == "semiannual") { m += 6; if (m > 12) { m -= 12; ++y; } while (! Date::valid (m, d, y)) --d; return Date (m, d, y); } else if (period == "bimonthly") { m += 2; if (m > 12) { m -= 12; ++y; } while (! Date::valid (m, d, y)) --d; return Date (m, d, y); } else if (period == "biannual" || period == "biyearly") { y += 2; return Date (m, d, y); } else if (period == "annual" || period == "yearly") { 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 Date (m, d, y); } // If the period is an 'easy' one, add it to current, and we're done. int secs = 0; try { Duration du (period); secs = du; } catch (...) { secs = 0; } return current + secs; } //////////////////////////////////////////////////////////////////////////////// // When the status of a recurring child task changes, the parent task must // update it's mask. void updateRecurrenceMask ( std::vector & all, Task& task) { std::string parent = task.get ("parent"); if (parent != "") { std::vector ::iterator it; for (it = all.begin (); it != all.end (); ++it) { if (it->get ("uuid") == parent) { unsigned int index = atoi (task.get ("imask").c_str ()); std::string mask = it->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' : '?'; it->set ("mask", mask); context.tdb.update (*it); } 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' : '?'; } return; // No point continuing the loop. } } } } //////////////////////////////////////////////////////////////////////////////// // Determines whether a task is overdue. Returns // 0 = not due at all // 1 = imminent // 2 = today // 3 = overdue int getDueState (const std::string& due) { if (due.length ()) { Date dt (::atoi (due.c_str ())); // rightNow is the current date + time. Date rightNow; Date thisDay (rightNow.month (), rightNow.day (), rightNow.year ()); if (dt < rightNow) return 3; if (dt == thisDay) return 2; int imminentperiod = context.config.getInteger ("due"); if (imminentperiod == 0) return 1; Date imminentDay = thisDay + imminentperiod * 86400; if (dt < imminentDay) return 1; } return 0; } //////////////////////////////////////////////////////////////////////////////// // Returns a Boolean indicator as to whether a nag message was generated, so // that commands can control the number of nag messages displayed (ie one is // enough). bool nag (Task& task) { // Special tag overrides nagging. if (task.hasTag ("nonag")) return false; std::string nagMessage = context.config.get ("nag"); if (nagMessage != "") { // Load all pending tasks. std::vector tasks; Filter filter; // Piggy-back on existing locked TDB. context.tdb.loadPending (tasks, filter); // Counters. int overdue = 0; int high = 0; int medium = 0; int low = 0; bool isOverdue = false; char pri = ' '; // Scan all pending tasks. foreach (t, tasks) { if (t->id == task.id) { if (getDueState (t->get ("due")) == 3) isOverdue = true; std::string priority = t->get ("priority"); if (priority.length ()) pri = priority[0]; } else if (t->getStatus () == Task::pending) { if (getDueState (t->get ("due")) == 3) overdue++; std::string priority = t->get ("priority"); if (priority.length ()) { switch (priority[0]) { case 'H': high++; break; case 'M': medium++; break; case 'L': low++; break; } } } } // General form is "if there are no more deserving tasks", suppress the nag. if (isOverdue ) return false; if (pri == 'H' && !overdue ) return false; if (pri == 'M' && !overdue && !high ) return false; if (pri == 'L' && !overdue && !high && !medium ) return false; if (pri == ' ' && !overdue && !high && !medium && !low) return false; // All the excuses are made, all that remains is to nag the user. context.footnote (nagMessage); return true; } return false; } ////////////////////////////////////////////////////////////////////////////////