//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2014, 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. // // http://www.opensource.org/licenses/mit-license.php // //////////////////////////////////////////////////////////////////////////////// #include #include // TODO Remove #include #include #include #ifdef PRODUCT_TASKWARRIOR #include #endif #include #ifdef PRODUCT_TASKWARRIOR #include #include #include #endif #include #include #include #include #ifdef PRODUCT_TASKWARRIOR #include #endif #include #include #include #ifdef PRODUCT_TASKWARRIOR #include #include #include #include #include #include #define APPROACHING_INFINITY 1000 // Close enough. This isn't rocket surgery. extern Context context; extern Task& contextTask; static const float epsilon = 0.000001; #endif std::string Task::defaultProject = ""; std::string Task::defaultPriority = ""; std::string Task::defaultDue = ""; bool Task::searchCaseSensitive = true; bool Task::regex = false; std::map Task::attributes; std::map Task::coefficients; float Task::urgencyPriorityCoefficient = 0.0; float Task::urgencyProjectCoefficient = 0.0; float Task::urgencyActiveCoefficient = 0.0; float Task::urgencyScheduledCoefficient = 0.0; float Task::urgencyWaitingCoefficient = 0.0; float Task::urgencyBlockedCoefficient = 0.0; float Task::urgencyInheritCoefficient = 0.0; float Task::urgencyAnnotationsCoefficient = 0.0; float Task::urgencyTagsCoefficient = 0.0; float Task::urgencyNextCoefficient = 0.0; float Task::urgencyDueCoefficient = 0.0; float Task::urgencyBlockingCoefficient = 0.0; float Task::urgencyAgeCoefficient = 0.0; float Task::urgencyAgeMax = 0.0; static const std::string dummy (""); //////////////////////////////////////////////////////////////////////////////// Task::Task () : id (0) , urgency_value (0.0) , recalc_urgency (true) , is_blocked (false) , is_blocking (false) , annotation_count (0) { } //////////////////////////////////////////////////////////////////////////////// Task::Task (const Task& other) { *this = other; } //////////////////////////////////////////////////////////////////////////////// Task& Task::operator= (const Task& other) { if (this != &other) { std::map ::operator= (other); id = other.id; urgency_value = other.urgency_value; recalc_urgency = other.recalc_urgency; is_blocked = other.is_blocked; is_blocking = other.is_blocking; annotation_count = other.annotation_count; } return *this; } //////////////////////////////////////////////////////////////////////////////// // The uuid and id attributes must be exempt from comparison. bool Task::operator== (const Task& other) { if (size () != other.size ()) return false; Task::iterator i; for (i = this->begin (); i != this->end (); ++i) if (i->first != "uuid" && i->second != other.get (i->first)) return false; return true; } //////////////////////////////////////////////////////////////////////////////// Task::Task (const std::string& input) { id = 0; urgency_value = 0.0; recalc_urgency = true; is_blocked = false; is_blocking = false; annotation_count = 0; parse (input); } //////////////////////////////////////////////////////////////////////////////// Task::~Task () { } //////////////////////////////////////////////////////////////////////////////// Task::status Task::textToStatus (const std::string& input) { if (input[0] == 'p') return Task::pending; else if (input[0] == 'c') return Task::completed; else if (input[0] == 'd') return Task::deleted; else if (input[0] == 'r') return Task::recurring; else if (input[0] == 'w') return Task::waiting; return Task::pending; } //////////////////////////////////////////////////////////////////////////////// std::string Task::statusToText (Task::status s) { if (s == Task::pending) return "pending"; else if (s == Task::recurring) return "recurring"; else if (s == Task::waiting) return "waiting"; else if (s == Task::completed) return "completed"; else if (s == Task::deleted) return "deleted"; return "pending"; } //////////////////////////////////////////////////////////////////////////////// void Task::setAsNow (const std::string& att) { char now[16]; sprintf (now, "%u", (unsigned int) time (NULL)); set (att, now); recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// bool Task::has (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return true; return false; } //////////////////////////////////////////////////////////////////////////////// std::vector Task::all () { std::vector all; Task::iterator i; for (i = this->begin (); i != this->end (); ++i) all.push_back (i->first); return all; } //////////////////////////////////////////////////////////////////////////////// const std::string Task::get (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return i->second; return ""; } //////////////////////////////////////////////////////////////////////////////// const std::string& Task::get_ref (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return i->second; return dummy; } //////////////////////////////////////////////////////////////////////////////// int Task::get_int (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return strtol (i->second.c_str (), NULL, 10); return 0; } //////////////////////////////////////////////////////////////////////////////// unsigned long Task::get_ulong (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return strtoul (i->second.c_str (), NULL, 10); return 0; } //////////////////////////////////////////////////////////////////////////////// float Task::get_float (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return strtof (i->second.c_str (), NULL); return 0.0; } //////////////////////////////////////////////////////////////////////////////// time_t Task::get_date (const std::string& name) const { Task::const_iterator i = this->find (name); if (i != this->end ()) return (time_t) strtoul (i->second.c_str (), NULL, 10); return 0; } //////////////////////////////////////////////////////////////////////////////// void Task::set (const std::string& name, const std::string& value) { (*this)[name] = json::decode (value); recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// void Task::set (const std::string& name, int value) { (*this)[name] = format (value); recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// void Task::remove (const std::string& name) { Task::iterator it; if ((it = this->find (name)) != this->end ()) { this->erase (it); recalc_urgency = true; } } //////////////////////////////////////////////////////////////////////////////// Task::status Task::getStatus () const { return textToStatus (get ("status")); } //////////////////////////////////////////////////////////////////////////////// void Task::setStatus (Task::status status) { set ("status", statusToText (status)); recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// // Determines status of a date attribute. Task::dateState Task::getDateState (const std::string& name) const { std::string value = get (name); if (value.length ()) { Date reference (value); Date now; Date today ("today"); if (reference < today) return dateBeforeToday; if (reference.sameDay (now)) { if (reference < now) return dateEarlierToday; else return dateLaterToday; } int imminentperiod = context.config.getInteger ("due"); if (imminentperiod == 0) return dateAfterToday; Date imminentDay = today + imminentperiod * 86400; if (reference < imminentDay) return dateAfterToday; } return dateNotDue; } #ifdef PRODUCT_TASKWARRIOR //////////////////////////////////////////////////////////////////////////////// // Ready means pending, not blocked and either not scheduled or scheduled before // now. bool Task::is_ready () const { return getStatus () == Task::pending && !is_blocked && (! has ("scheduled") || Date ("now").operator> (get_date ("scheduled"))); } //////////////////////////////////////////////////////////////////////////////// bool Task::is_due () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { Task::dateState state = getDateState ("due"); if (state == dateAfterToday || state == dateEarlierToday || state == dateLaterToday) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_dueyesterday () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { if (Date ("yesterday").sameDay (get_date ("due"))) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_duetoday () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { Task::dateState state = getDateState ("due"); if (state == dateEarlierToday || state == dateLaterToday) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_duetomorrow () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { if (Date ("tomorrow").sameDay (get_date ("due"))) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_dueweek () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { Date due (get_date ("due")); if (due >= Date ("socw") && due <= Date ("eocw")) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_duemonth () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { Date due (get_date ("due")); if (due >= Date ("socm") && due <= Date ("eocm")) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_dueyear () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { Date now; Date due (get_date ("due")); if (now.year () == due.year ()) return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool Task::is_overdue () const { if (has ("due")) { Task::status status = getStatus (); if (status != Task::completed && status != Task::deleted) { Task::dateState state = getDateState ("due"); if (state == dateEarlierToday || state == dateBeforeToday) return true; } } return false; } #endif //////////////////////////////////////////////////////////////////////////////// // Attempt an FF4 parse first, using Task::parse, and in the event of an error // try a JSON parse, otherwise a legacy parse (currently no legacy formats are // supported). // // Note that FF1, FF2 and FF3 are no longer supported. // // start --> [ --> Att --> ] --> end // ^ | // +-------+ // void Task::parse (const std::string& input) { // TODO Is this simply a 'chomp'? std::string copy; if (input[input.length () - 1] == '\n') copy = input.substr (0, input.length () - 1); else copy = input; try { // File format version 4, from 2009-5-16 - now, v1.7.1+ // This is the parse format tried first, because it is most used. clear (); if (copy[0] == '[') { Nibbler n (copy); std::string line; if (n.skip ('[') && n.getUntil (']', line) && n.skip (']') && n.depleted ()) { if (line.length () == 0) throw std::string (STRING_RECORD_EMPTY); Nibbler nl (line); std::string name; std::string value; while (!nl.depleted ()) { if (nl.getUntil (':', name) && nl.skip (':') && nl.getQuoted ('"', value)) { legacyAttributeMap (name); legacyValueMap (name, value); if (name.substr (0, 11) == "annotation_") ++annotation_count; (*this)[name] = decode (json::decode (value)); } nl.skip (' '); } std::string remainder; nl.getUntilEOS (remainder); if (remainder.length ()) throw std::string (STRING_RECORD_JUNK_AT_EOL); } } else if (copy[0] == '{') parseJSON (copy); else throw std::string (STRING_RECORD_NOT_FF4); upgradeLegacyValues (); } catch (const std::string&) { parseLegacy (copy); } recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// // Note that all fields undergo encode/decode. void Task::parseJSON (const std::string& line) { // Parse the whole thing. json::value* root = json::parse (line); if (root->type () == json::j_object) { json::object* root_obj = (json::object*)root; // For each object element... json_object_iter i; for (i = root_obj->_data.begin (); i != root_obj->_data.end (); ++i) { // If the attribute is a recognized column. std::string type = Task::attributes[i->first]; if (type != "") { // Any specified id is ignored. if (i->first == "id") ; // Urgency, if present, is ignored. else if (i->first == "urgency") ; // TW-1274 Standardization. else if (i->first == "modification") { Date d (unquoteText (i->second->dump ())); set ("modified", d.toEpochString ()); } // Dates are converted from ISO to epoch. else if (type == "date") { Date d (unquoteText (i->second->dump ())); set (i->first, d.toEpochString ()); } // Tags are an array of JSON strings. else if (i->first == "tags" && i->second->type() == json::j_array) { json::array* tags = (json::array*)i->second; json_array_iter t; for (t = tags->_data.begin (); t != tags->_data.end (); ++t) { json::string* tag = (json::string*)*t; addTag (tag->_data); } } // This is a temporary measure to allow Mirakel sync, and will be removed // in a future release. else if (i->first == "tags" && i->second->type() == json::j_string) { json::string* tag = (json::string*)i->second; addTag (tag->_data); } // Strings are decoded. else if (type == "string") set (i->first, json::decode (unquoteText (i->second->dump ()))); // Other types are simply added. else set (i->first, unquoteText (i->second->dump ())); } // UDA orphans and annotations do not have columns. else { // Annotations are an array of JSON objects with 'entry' and // 'description' values and must be converted. if (i->first == "annotations") { std::map annos; json::array* atts = (json::array*)i->second; json_array_iter annotations; for (annotations = atts->_data.begin (); annotations != atts->_data.end (); ++annotations) { json::object* annotation = (json::object*)*annotations; json::string* when = (json::string*)annotation->_data["entry"]; json::string* what = (json::string*)annotation->_data["description"]; if (! when) throw format (STRING_TASK_NO_ENTRY, line); if (! what) throw format (STRING_TASK_NO_DESC, line); std::string name = "annotation_" + Date (when->_data).toEpochString (); annos.insert (std::make_pair (name, json::decode (what->_data))); } setAnnotations (annos); } // UDA Orphan - must be preserved. else { #ifdef PRODUCT_TASKWARRIOR std::stringstream message; message << "Task::parseJSON found orphan '" << i->first << "' with value '" << i->second << "' --> preserved\n"; context.debug (message.str ()); #endif set (i->first, json::decode (unquoteText (i->second->dump ()))); } } } upgradeLegacyValues (); } } //////////////////////////////////////////////////////////////////////////////// // No legacy formats are currently supported as of 2.4.0. void Task::parseLegacy (const std::string& line) { switch (determineVersion (line)) { // File format version 1, from 2006-11-27 - 2007-12-31, v0.x+ - v0.9.3 case 1: throw std::string (STRING_TASK_NO_FF1); // File format version 2, from 2008-1-1 - 2009-3-23, v0.9.3 - v1.5.0 case 2: throw std::string (STRING_TASK_NO_FF2); // File format version 3, from 2009-3-23 - 2009-05-16, v1.6.0 - v1.7.1 case 3: throw std::string (STRING_TASK_NO_FF3); default: std::stringstream message; message << "Invalid fileformat at line '" << line << "'"; context.debug (message.str ()); throw std::string (STRING_TASK_PARSE_UNREC_FF); break; } recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// // The format is: // // [ : ... ] // std::string Task::composeF4 () const { std::string ff4 = "["; bool first = true; Task::const_iterator it; for (it = this->begin (); it != this->end (); ++it) { if (it->second != "") { ff4 += (first ? "" : " ") + it->first + ":\"" + encode (json::encode (it->second)) + "\""; first = false; } } ff4 += "]"; return ff4; } //////////////////////////////////////////////////////////////////////////////// std::string Task::composeJSON (bool decorate /*= false*/) const { std::stringstream out; out << "{"; // ID inclusion is optional, but not a good idea, because it remains correct // only until the next gc. if (decorate) out << "\"id\":" << id << ","; // First the non-annotations. int attributes_written = 0; Task::const_iterator i; for (i = this->begin (); i != this->end (); ++i) { // Annotations are not written out here. if (i->first.substr (0, 11) == "annotation_") continue; if (attributes_written) out << ","; std::string type = Task::attributes[i->first]; if (type == "") type = "string"; // Date fields are written as ISO 8601. if (type == "date") { Date d (i->second); if (i->first == "modification") out << "\"modified\":\"" << d.toISO () << "\""; else out << "\"" << i->first << "\":\"" << d.toISO () << "\""; ++attributes_written; } // Tags are converted to an array. else if (i->first == "tags") { std::vector tags; split (tags, i->second, ','); out << "\"tags\":["; std::vector ::iterator i; for (i = tags.begin (); i != tags.end (); ++i) { if (i != tags.begin ()) out << ","; out << "\"" << *i << "\""; } out << "]"; } // Everything else is a quoted value. else { out << "\"" << i->first << "\":\"" << json::encode (i->second) << "\""; ++attributes_written; } } // Now the annotations, if any. if (annotation_count) { out << "," << "\"annotations\":["; int annotations_written = 0; for (i = this->begin (); i != this->end (); ++i) { if (i->first.substr (0, 11) == "annotation_") { if (annotations_written) out << ","; Date d (i->first.substr (11)); out << "{\"entry\":\"" << d.toISO () << "\",\"description\":\"" << json::encode (i->second) << "\"}"; ++annotations_written; } } out << "]"; } #ifdef PRODUCT_TASKWARRIOR // Include urgency. if (decorate) out << "," << "\"urgency\":\"" << urgency_c () <<"\""; #endif out << "}"; return out.str (); } //////////////////////////////////////////////////////////////////////////////// bool Task::hasAnnotations () const { return annotation_count ? true : false; } //////////////////////////////////////////////////////////////////////////////// // The timestamp is part of the name: // annotation_1234567890:"..." // // Note that the time is incremented (one second) in order to find a unique // timestamp. void Task::addAnnotation (const std::string& description) { time_t now = time (NULL); std::string key; do { key = "annotation_" + format ((int) now); ++now; } while (has (key)); (*this)[key] = json::decode (description); ++annotation_count; recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// void Task::removeAnnotations () { // Erase old annotations. Task::iterator i = this->begin (); while (i != this->end ()) { if (i->first.substr (0, 11) == "annotation_") { --annotation_count; this->erase (i++); } else i++; } recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// void Task::getAnnotations (std::map & annotations) const { annotations.clear (); Task::const_iterator ci; for (ci = this->begin (); ci != this->end (); ++ci) if (ci->first.substr (0, 11) == "annotation_") annotations.insert (*ci); } //////////////////////////////////////////////////////////////////////////////// void Task::setAnnotations (const std::map & annotations) { // Erase old annotations. removeAnnotations (); std::map ::const_iterator ci; for (ci = annotations.begin (); ci != annotations.end (); ++ci) this->insert (*ci); annotation_count = annotations.size (); recalc_urgency = true; } #ifdef PRODUCT_TASKWARRIOR //////////////////////////////////////////////////////////////////////////////// void Task::addDependency (int id) { // Check that id is resolvable. std::string uuid = context.tdb2.pending.uuid (id); if (uuid == "") throw format (STRING_TASK_DEPEND_MISS_CREA, id); std::string depends = get ("depends"); if (depends.find (uuid) != std::string::npos) throw format (STRING_TASK_DEPEND_DUP, this->id, id); addDependency(uuid); } //////////////////////////////////////////////////////////////////////////////// void Task::addDependency (const std::string& uuid) { if (uuid == get ("uuid")) throw std::string (STRING_TASK_DEPEND_ITSELF); // Store the dependency. std::string depends = get ("depends"); if (depends != "") { // Check for extant dependency. if (depends.find (uuid) == std::string::npos) set ("depends", depends + "," + uuid); else throw format (STRING_TASK_DEPEND_DUP, this->get ("uuid"), uuid); } else set ("depends", uuid); // Prevent circular dependencies. if (dependencyIsCircular (*this)) throw std::string (STRING_TASK_DEPEND_CIRCULAR); recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// void Task::removeDependency (const std::string& uuid) { std::vector deps; split (deps, get ("depends"), ','); std::vector ::iterator i; i = std::find (deps.begin (), deps.end (), uuid); if (i != deps.end ()) { deps.erase (i); std::string combined; join (combined, ",", deps); set ("depends", combined); recalc_urgency = true; } else throw format (STRING_TASK_DEPEND_MISS_DEL, uuid); } //////////////////////////////////////////////////////////////////////////////// void Task::removeDependency (int id) { std::string depends = get ("depends"); std::string uuid = context.tdb2.pending.uuid (id); if (uuid != "" && depends.find (uuid) != std::string::npos) removeDependency (uuid); else throw format (STRING_TASK_DEPEND_MISS_DEL, id); } //////////////////////////////////////////////////////////////////////////////// void Task::getDependencies (std::vector & all) const { std::vector deps; split (deps, get ("depends"), ','); all.clear (); std::vector ::iterator i; for (i = deps.begin (); i != deps.end (); ++i) all.push_back (context.tdb2.pending.id (*i)); } //////////////////////////////////////////////////////////////////////////////// void Task::getDependencies (std::vector & all) const { all.clear (); split (all, get ("depends"), ','); } #endif //////////////////////////////////////////////////////////////////////////////// int Task::getTagCount () const { std::vector tags; split (tags, get ("tags"), ','); return (int) tags.size (); } //////////////////////////////////////////////////////////////////////////////// // // OVERDUE YESTERDAY DUE TODAY TOMORROW WEEK MONTH YEAR // due:-1week Y - - - - ? ? ? // due:-1day Y Y - - - ? ? ? // due:today Y - Y Y - ? ? ? // due:tomorrow - - Y - Y ? ? ? // due:3days - - Y - - ? ? ? // due:1month - - - - - - - ? // due:1year - - - - - - - - // bool Task::hasTag (const std::string& tag) const { // Synthetic tags - dynamically generated, but do not occupy storage space. if (tag == "BLOCKED") return is_blocked; if (tag == "UNBLOCKED") return !is_blocked; if (tag == "BLOCKING") return is_blocking; #ifdef PRODUCT_TASKWARRIOR if (tag == "READY") return is_ready (); if (tag == "DUE") return is_due (); if (tag == "DUETODAY") return is_duetoday (); if (tag == "TODAY") return is_duetoday (); if (tag == "YESTERDAY") return is_dueyesterday (); if (tag == "TOMORROW") return is_duetomorrow (); if (tag == "OVERDUE") return is_overdue (); if (tag == "WEEK") return is_dueweek (); if (tag == "MONTH") return is_duemonth (); if (tag == "YEAR") return is_dueyear (); #endif if (tag == "ACTIVE") return has ("start"); if (tag == "SCHEDULED") return has ("scheduled"); if (tag == "CHILD") return has ("parent"); if (tag == "UNTIL") return has ("until"); if (tag == "WAITING") return has ("wait"); if (tag == "ANNOTATED") return hasAnnotations (); if (tag == "TAGGED") return has ("tags"); if (tag == "PARENT") return has ("mask"); if (tag == "PENDING") return get ("status") == "pending"; if (tag == "COMPLETED") return get ("status") == "completed"; if (tag == "DELETED") return get ("status") == "deleted"; // Concrete tags. std::vector tags; split (tags, get ("tags"), ','); if (std::find (tags.begin (), tags.end (), tag) != tags.end ()) return true; return false; } //////////////////////////////////////////////////////////////////////////////// void Task::addTag (const std::string& tag) { std::vector tags; split (tags, get ("tags"), ','); if (std::find (tags.begin (), tags.end (), tag) == tags.end ()) { tags.push_back (tag); std::string combined; join (combined, ",", tags); set ("tags", combined); recalc_urgency = true; } } //////////////////////////////////////////////////////////////////////////////// void Task::addTags (const std::vector & tags) { remove ("tags"); std::vector ::const_iterator it; for (it = tags.begin (); it != tags.end (); ++it) addTag (*it); recalc_urgency = true; } //////////////////////////////////////////////////////////////////////////////// void Task::getTags (std::vector& tags) const { split (tags, get ("tags"), ','); } //////////////////////////////////////////////////////////////////////////////// void Task::removeTag (const std::string& tag) { std::vector tags; split (tags, get ("tags"), ','); std::vector ::iterator i; i = std::find (tags.begin (), tags.end (), tag); if (i != tags.end ()) { tags.erase (i); std::string combined; join (combined, ",", tags); set ("tags", combined); } recalc_urgency = true; } #ifdef PRODUCT_TASKWARRIOR //////////////////////////////////////////////////////////////////////////////// // A UDA is an attribute that has supporting config entries such as a data type: // 'uda..type' void Task::getUDAs (std::vector & names) const { Task::const_iterator it; for (it = this->begin (); it != this->end (); ++it) if (context.config.get ("uda." + it->first + ".type") != "") names.push_back (it->first); } //////////////////////////////////////////////////////////////////////////////// // A UDA Orphan is an attribute that is not represented in context.columns. void Task::getUDAOrphans (std::vector & names) const { Task::const_iterator it; for (it = this->begin (); it != this->end (); ++it) if (it->first.substr (0, 11) != "annotation_") if (context.columns.find (it->first) == context.columns.end ()) names.push_back (it->first); } //////////////////////////////////////////////////////////////////////////////// void Task::substitute ( const std::string& from, const std::string& to, bool global) { // Get the data to modify. std::string description = get ("description"); std::map annotations; getAnnotations (annotations); // Count the changes, so we know whether to proceed to annotations, after // modifying description. int changes = 0; bool done = false; // Regex support is optional. if (Task::regex) { // Create the regex. RX rx (from, Task::searchCaseSensitive); std::vector start; std::vector end; // Perform all subs on description. if (rx.match (start, end, description)) { int skew = 0; for (unsigned int i = 0; i < start.size () && !done; ++i) { description.replace (start[i + skew], end[i] - start[i], to); skew += to.length () - (end[i] - start[i]); ++changes; if (!global) done = true; } } if (!done) { // Perform all subs on annotations. std::map ::iterator it; for (it = annotations.begin (); it != annotations.end () && !done; ++it) { start.clear (); end.clear (); if (rx.match (start, end, it->second)) { int skew = 0; for (unsigned int i = 0; i < start.size () && !done; ++i) { it->second.replace (start[i + skew], end[i] - start[i], to); skew += to.length () - (end[i] - start[i]); ++changes; if (!global) done = true; } } } } } else { // Perform all subs on description. int counter = 0; std::string::size_type pos = 0; int skew = 0; while ((pos = ::find (description, from, pos, Task::searchCaseSensitive)) != std::string::npos && !done) { description.replace (pos + skew, from.length (), to); skew += to.length () - from.length (); pos += to.length (); ++changes; if (!global) done = true; if (++counter > APPROACHING_INFINITY) throw format (STRING_INFINITE_LOOP, APPROACHING_INFINITY); } if (!done) { // Perform all subs on annotations. counter = 0; std::map ::iterator i; for (i = annotations.begin (); i != annotations.end () && !done; ++i) { pos = 0; skew = 0; while ((pos = ::find (i->second, from, pos, Task::searchCaseSensitive)) != std::string::npos && !done) { i->second.replace (pos + skew, from.length (), to); skew += to.length () - from.length (); pos += to.length (); ++changes; if (!global) done = true; if (++counter > APPROACHING_INFINITY) throw format (STRING_INFINITE_LOOP, APPROACHING_INFINITY); } } } } if (changes) { set ("description", description); setAnnotations (annotations); recalc_urgency = true; } } #endif //////////////////////////////////////////////////////////////////////////////// // The purpose of Task::validate is three-fold: // 1) To provide missing attributes where possible // 2) To provide suitable warnings about odd states // 3) To generate errors when the inconsistencies are not fixable // void Task::validate (bool applyDefault /* = true */) { Task::status status = getStatus (); // 1) Provide missing attributes where possible // Provide a UUID if necessary. if (! has ("uuid") || get ("uuid") == "") set ("uuid", uuid ()); // Recurring tasks get a special status. if (status == Task::pending && has ("due") && has ("recur") && (! has ("parent") || get ("parent") == "")) status = Task::recurring; // Tasks with a wait: date get a special status. else if (status == Task::pending && has ("wait")) status = Task::waiting; // By default, tasks are pending. else if (! has ("status") || get ("status") == "") status = Task::pending; // Store the derived status. setStatus (status); #ifdef PRODUCT_TASKWARRIOR // Provide an entry date unless user already specified one. if (!has ("entry") || get ("entry") == "") setAsNow ("entry"); // Completed tasks need an end date, so inherit the entry date. if ((status == Task::completed || status == Task::deleted) && (! has ("end") || get ("end") == "")) setAsNow ("end"); // Provide an entry date unless user already specified one. if (!has ("modified") || get ("modified") == "") setAsNow ("modified"); if (applyDefault) { // Override with default.project, if not specified. if (Task::defaultProject != "" && ! has ("project")) { if (context.columns["project"]->validate (Task::defaultProject)) set ("project", Task::defaultProject); } // Override with default.priority, if not specified. if (Task::defaultPriority != "" && ! has ("priority")) { if (context.columns["priority"]->validate (Task::defaultPriority)) set ("priority", Task::defaultPriority); } // Override with default.due, if not specified. if (Task::defaultDue != "" && ! has ("due")) { if (context.columns["due"]->validate (Task::defaultDue)) set ("due", Date (Task::defaultDue).toEpoch ()); } // If a UDA has a default value in the configuration, // override with uda.(uda).default, if not specified. // Gather a list of all UDAs with a .default value std::vector udas; Config::const_iterator var; for (var = context.config.begin (); var != context.config.end (); ++var) { if (var->first.substr (0, 4) == "uda." && var->first.find (".default") != std::string::npos) { std::string::size_type period = var->first.find ('.', 4); if (period != std::string::npos) udas.push_back (var->first.substr (4, period - 4)); } } if (udas.size ()) { // For each of those, setup the default value on the task now, // of course only if we don't have one on the command line already std::vector ::iterator uda; for (uda = udas.begin (); uda != udas.end (); ++uda) { std::string defVal= context.config.get ("uda." + *uda + ".default"); // If the default is empty, or we already have a value, skip it if (defVal != "" && get (*uda) == "") set (*uda, defVal); } } } #endif // 2) To provide suitable warnings about odd states // Date relationships. validate_before ("wait", "due"); validate_before ("entry", "start"); validate_before ("entry", "end"); validate_before ("wait", "scheduled"); validate_before ("scheduled", "start"); validate_before ("scheduled", "due"); validate_before ("scheduled", "end"); // 3) To generate errors when the inconsistencies are not fixable // There is no fixing a missing description. if (!has ("description")) throw std::string (STRING_TASK_VALID_DESC); else if (get ("description") == "") throw std::string (STRING_TASK_VALID_BLANK); // Cannot have a recur frequency with no due date - when would it recur? if (! has ("due") && has ("recur")) throw std::string (STRING_TASK_VALID_REC_DUE); // Recur durations must be valid. if (has ("recur")) { std::string value = get ("recur"); Duration d; std::string::size_type i = 0; if (! d.parse (value, i)) { i = 0; ISO8601p p; if (! p.parse (value, i)) throw std::string (format (STRING_TASK_VALID_RECUR, value)); } } // Priorities must be valid. if (has ("priority")) { std::string priority = get ("priority"); if (priority != "H" && priority != "M" && priority != "L") throw format (STRING_TASK_VALID_PRIORITY, priority); } } //////////////////////////////////////////////////////////////////////////////// void Task::validate_before (const std::string& left, const std::string& right) { #ifdef PRODUCT_TASKWARRIOR if (has (left) && has (right)) { Date date_left (get_date (left)); Date date_right (get_date (right)); if (date_left > date_right) context.footnote (format (STRING_TASK_VALID_BEFORE, left, right)); } #endif } //////////////////////////////////////////////////////////////////////////////// // Encode values prior to serialization. // [ -> &open; // ] -> &close; const std::string Task::encode (const std::string& value) const { std::string modified = value; str_replace (modified, "[", "&open;"); str_replace (modified, "]", "&close;"); return modified; } //////////////////////////////////////////////////////////////////////////////// // Decode values after parse. // " <- &dquot; // ' <- &squot; or " // , <- , // [ <- &open; // ] <- &close; // : <- : const std::string Task::decode (const std::string& value) const { if (value.find ('&') != std::string::npos) { std::string modified = value; // Supported encodings. str_replace (modified, "&open;", "["); str_replace (modified, "&close;", "]"); // Support for deprecated encodings. These cannot be removed or old files // will not be parsable. Not just old files - completed.data can contain // tasks formatted/encoded using these. str_replace (modified, "&dquot;", "\""); str_replace (modified, """, "'"); str_replace (modified, "&squot;", "'"); // Deprecated 2.0 str_replace (modified, ",", ","); // Deprecated 2.0 str_replace (modified, ":", ":"); // Deprecated 2.0 return modified; } return value; } //////////////////////////////////////////////////////////////////////////////// int Task::determineVersion (const std::string& line) { // Version 2 looks like: // // uuid status [tags] [attributes] description\n // // Where uuid looks like: // // 27755d92-c5e9-4c21-bd8e-c3dd9e6d3cf7 // // Scan for the hyphens in the uuid, the following space, and a valid status // character. if (line[8] == '-' && line[13] == '-' && line[18] == '-' && line[23] == '-' && line[36] == ' ' && (line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r')) { // Version 3 looks like: // // uuid status [tags] [attributes] [annotations] description\n // // Scan for the number of [] pairs. std::string::size_type tagAtts = line.find ("] [", 0); std::string::size_type attsAnno = line.find ("] [", tagAtts + 1); std::string::size_type annoDesc = line.find ("] ", attsAnno + 1); if (tagAtts != std::string::npos && attsAnno != std::string::npos && annoDesc != std::string::npos) return 3; else return 2; } // Version 4 looks like: // // [name:"value" ...] // // Scan for [, ] and :". else if (line[0] == '[' && line[line.length () - 1] == ']' && line.find ("uuid:\"") != std::string::npos) return 4; // Version 1 looks like: // // [tags] [attributes] description\n // X [tags] [attributes] description\n // // Scan for the first character being either the bracket or X. else if (line.find ("X [") == 0 || line.find ("uuid") == std::string::npos || (line[0] == '[' && line.substr (line.length () - 1, 1) != "]")) return 1; // Version 5? // // Fortunately, with the hindsight that will come with version 5, the // identifying characteristics of 1, 2, 3 and 4 may be modified such that if 5 // has a UUID followed by a status, then there is still a way to differentiate // between 2, 3, 4 and 5. // // The danger is that a version 3 binary reads and misinterprets a version 4 // file. This is why it is a good idea to rely on an explicit version // declaration rather than chance positioning. // Zero means 'no idea'. return 0; } //////////////////////////////////////////////////////////////////////////////// // Urgency is defined as a polynomial, the value of which is calculated in this // function, according to: // // U = A.t + B.t + C.t ... // a b c // // U = urgency // A = coefficient for term a // t sub a = numeric scale from 0 -> 1, with 1 being the highest // urgency, derived from one task attribute and mapped // to the numeric scale // // See rfc31-urgency.txt for full details. // float Task::urgency_c () const { float value = 0.0; #ifdef PRODUCT_TASKWARRIOR value += fabsf (Task::urgencyPriorityCoefficient) > epsilon ? (urgency_priority () * Task::urgencyPriorityCoefficient) : 0.0; value += fabsf (Task::urgencyProjectCoefficient) > epsilon ? (urgency_project () * Task::urgencyProjectCoefficient) : 0.0; value += fabsf (Task::urgencyActiveCoefficient) > epsilon ? (urgency_active () * Task::urgencyActiveCoefficient) : 0.0; value += fabsf (Task::urgencyScheduledCoefficient) > epsilon ? (urgency_scheduled () * Task::urgencyScheduledCoefficient) : 0.0; value += fabsf (Task::urgencyWaitingCoefficient) > epsilon ? (urgency_waiting () * Task::urgencyWaitingCoefficient) : 0.0; value += fabsf (Task::urgencyBlockedCoefficient) > epsilon ? (urgency_blocked () * Task::urgencyBlockedCoefficient) : 0.0; value += fabsf (Task::urgencyAnnotationsCoefficient) > epsilon ? (urgency_annotations () * Task::urgencyAnnotationsCoefficient) : 0.0; value += fabsf (Task::urgencyTagsCoefficient) > epsilon ? (urgency_tags () * Task::urgencyTagsCoefficient) : 0.0; value += fabsf (Task::urgencyNextCoefficient) > epsilon ? (urgency_next () * Task::urgencyNextCoefficient) : 0.0; value += fabsf (Task::urgencyDueCoefficient) > epsilon ? (urgency_due () * Task::urgencyDueCoefficient) : 0.0; value += fabsf (Task::urgencyBlockingCoefficient) > epsilon ? (urgency_blocking () * Task::urgencyBlockingCoefficient) : 0.0; value += fabsf (Task::urgencyAgeCoefficient) > epsilon ? (urgency_age () * Task::urgencyAgeCoefficient) : 0.0; value += fabsf (Task::urgencyInheritCoefficient) > epsilon ? (urgency_inherit () * Task::urgencyInheritCoefficient) : 0.0; // Tag- and project-specific coefficients. std::map ::iterator var; for (var = Task::coefficients.begin (); var != Task::coefficients.end (); ++var) { if (fabs (var->second) > epsilon) { if (var->first.substr (0, 13) == "urgency.user.") { // urgency.user.project..coefficient std::string::size_type end = std::string::npos; if (var->first.substr (13, 8) == "project." && (end = var->first.find (".coefficient")) != std::string::npos) { std::string project = var->first.substr (21, end - 21); if (get ("project").find (project) == 0) value += var->second; } // urgency.user.tag..coefficient if (var->first.substr (13, 4) == "tag." && (end = var->first.find (".coefficient")) != std::string::npos) { std::string tag = var->first.substr (17, end - 17); if (hasTag (tag)) value += var->second; } } else if (var->first.substr (0, 12) == "urgency.uda.") { // urgency.uda..coefficient std::string::size_type end = var->first.find (".coefficient"); if (end != std::string::npos) if (has (var->first.substr (12, end - 12))) value += var->second; } } } #endif return value; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency () { if (recalc_urgency) { urgency_value = urgency_c (); // Return the sum of all terms. recalc_urgency = false; } return urgency_value; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_priority () const { const std::string& value = get_ref ("priority"); if (value == "H") return 1.0; else if (value == "M") return 0.65; else if (value == "L") return 0.3; return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_inherit () const { if (!is_blocking) return 0.0; // Calling dependencyGetBlocked is rather expensive. // It is called recursively for each dependency in the chain here. std::vector blocked; dependencyGetBlocked (*this, blocked); float v = 0.0; std::vector ::const_iterator it; for (it = blocked.begin (); it != blocked.end (); ++it) { // urgency_blocked, _blocking, _project and _tags left out. v += it->urgency_active (); v += it->urgency_age (); v += it->urgency_annotations (); v += it->urgency_due (); v += it->urgency_next (); v += it->urgency_priority (); v += it->urgency_scheduled (); v += it->urgency_waiting (); // Inherit from all parent tasks in the dependency chain recursively. v += it->urgency_inherit (); } return v; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_project () const { if (has ("project")) return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_active () const { if (has ("start")) return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_scheduled () const { if (has ("scheduled") && get_date ("scheduled") < time (NULL)) return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_waiting () const { if (get_ref ("status") == "waiting") return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// // A task is blocked only if the task it depends upon is pending/waiting. float Task::urgency_blocked () const { if (is_blocked) return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_annotations () const { if (annotation_count >= 3) return 1.0; else if (annotation_count == 2) return 0.9; else if (annotation_count == 1) return 0.8; return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_tags () const { switch (getTagCount ()) { case 0: return 0.0; case 1: return 0.8; case 2: return 0.9; default: return 1.0; } } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_next () const { if (hasTag ("next")) return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// // // Past Present Future // Overdue Due Due // // -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 days // // <-- 1.0 linear 0.2 --> // capped capped // // float Task::urgency_due () const { if (has ("due")) { Date now; Date due (get_date ("due")); // Map a range of 21 days to the value 0.2 - 1.0 float days_overdue = (now - due) / 86400.0; if (days_overdue >= 7.0) return 1.0; // < 1 wk ago else if (days_overdue >= -14.0) return ((days_overdue + 14.0) * 0.8 / 21.0) + 0.2; else return 0.2; // > 2 wks } return 0.0; } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_age () const { assert (has ("entry")); Date now; Date entry (get_date ("entry")); int age = (now - entry) / 86400; // in days if (Task::urgencyAgeMax == 0 || age > Task::urgencyAgeMax) return 1.0; return (1.0 * age / Task::urgencyAgeMax); } //////////////////////////////////////////////////////////////////////////////// float Task::urgency_blocking () const { if (is_blocking) return 1.0; return 0.0; } //////////////////////////////////////////////////////////////////////////////// // Arguably does not belong here. This method reads the parse tree and calls // Task methods. It could be a standalone function with no loss in access, as // well as reducing the object depdendencies of Task. // // It came from the Command base object, but doesn't really belong there either. void Task::modify (modType type, bool text_required /* = false */) { std::string text = ""; Tree* tree = context.parser.tree (); if (tree) context.debug (tree->dump ()); context.debug ("Task::modify"); std::string label = " MODIFICATION "; int modCount = 0; std::vector ::iterator i; for (i = tree->_branches.begin (); i != tree->_branches.end (); ++i) { if ((*i)->hasTag ("MODIFICATION")) { if ((*i)->hasTag ("ATTRIBUTE")) { // 'name' is canonical. // 'value' requires eval. std::string name = (*i)->attribute ("name"); std::string value = (*i)->attribute ("raw"); if (value == "" || value == "''" || value == "\"\"") { // ::composeF4 will skip if the value is blank, but the presence of // the attribute will prevent ::validate from applying defaults. set (name, ""); context.debug (label + name + " <-- ''"); ++modCount; } else { // Get the column info. Column* column = context.columns[name]; // Dependencies are specified as IDs. if (name == "depends") { // Parse IDs std::vector deps; split (deps, value, ','); // Apply or remove dendencies in turn. std::vector ::iterator i; for (i = deps.begin (); i != deps.end (); i++) { if ((*i)[0] == '-') { if ((*i).length () == 37) removeDependency (context.tdb2.pending.id ((*i).substr (1))); else removeDependency (strtol ((*i).substr (1).c_str (), NULL, 10)); } else { if ((*i).length () == 36) addDependency (context.tdb2.pending.id ((*i))); else addDependency (strtol ((*i).c_str (), NULL, 10)); } } ++modCount; } // Dates are special, maybe. else if (column->type () == "date") { Eval e; e.addSource (domSource); e.addSource (namedDates); e.ambiguity (false); contextTask = *this; Variant v; e.evaluateInfixExpression (value, v); // If v is duration, add 'now' to it, else store as date. if (v.type () == Variant::type_duration) { context.debug (label + name + " <-- '" + format ("{1}", v.get_duration ()) + "' <-- '" + (std::string) v + "' <-- '" + value + "'"); Variant now; if (namedDates ("now", now)) v += now; } else { v.cast (Variant::type_date); context.debug (label + name + " <-- '" + format ("{1}", v.get_date ()) + "' <-- '" + (std::string) v + "' <-- '" + value + "'"); } set (name, v.get_date ()); ++modCount; } // Special case: type duration. else if (name == "recur" || column->type () == "duration") { // The duration is stored in raw form, but it must still be valid, // and therefore is parsed first. Eval e; e.addSource (domSource); e.addSource (namedDates); e.ambiguity (false); contextTask = *this; Variant v; e.evaluateInfixExpression (value, v); if (v.type () == Variant::type_duration) { // Store the raw value, for 'recur'. set (name, value); ++modCount; } else throw format (STRING_TASK_INVALID_DUR, value); } // Need handling for numeric types, used by UDAs. else if (column->type () == "numeric") { Eval e; e.addSource (domSource); e.addSource (namedDates); e.ambiguity (false); contextTask = *this; Variant v; e.evaluateInfixExpression (value, v); context.debug (label + name + " <-- '" + v.get_string () + "' <-- '" + value + "'"); // If the result is not readily convertible to a numeric value, // then this is an error. if (v.type () == Variant::type_string) throw format (STRING_UDA_NUMERIC, v.get_string ()); v.cast (Variant::type_real); v.cast (Variant::type_string); set (name, v); ++modCount; } // String type columns may eval, or may not make sense to eval, and // the best way to determine this is to try. else if (column->type () == "string") { std::string evaluated = value; try { Eval e; e.addSource (domSource); e.addSource (namedDates); e.ambiguity (false); contextTask = *this; Variant v; e.evaluateInfixExpression (value, v); v.cast (Variant::type_string); evaluated = v.get_string (); } catch (...) { /* NOP */ } // Final default action if (column->validate (evaluated)) { if (column->can_modify ()) { std::string col_value = column->modify (evaluated); context.debug (label + name + " <-- '" + col_value + "' <-- '" + evaluated + "' <-- '" + value + "'"); (*this).set (name, col_value); } else { context.debug (label + name + " <-- '" + evaluated + "' <-- '" + value + "'"); (*this).set (name, evaluated); } ++modCount; } else throw format (STRING_INVALID_MOD, name, value); } else throw format (STRING_TASK_INVALID_COL_TYPE, column->type (), name); // Warn about deprecated/obsolete attribute usage. legacyAttributeCheck (name); } } // arg7 from='from' global='1' raw='/from/to/g' to='to' ORIGINAL SUBSTITUTION MODIFICATION else if ((*i)->hasTag ("SUBSTITUTION")) { context.debug (label + "substitute " + (*i)->attribute ("raw")); substitute ((*i)->attribute ("from"), (*i)->attribute ("to"), ((*i)->attribute ("global") == "1")); ++modCount; } // Tags need special handling because they are essentially a vector stored // in a single string, therefore Task::{add,remove}Tag must be called as // appropriate. else if ((*i)->hasTag ("TAG")) { std::string tag = (*i)->attribute ("tag"); if ((*i)->attribute ("sign") == "+") { context.debug (label + "tags <-- add '" + tag + "'"); addTag (tag); feedback_special_tags ((*this), tag); } else { context.debug (label + "tags <-- remove '" + tag + "'"); removeTag (tag); } ++modCount; } // WORD and TERMINATED args are accumulated. else if ((*i)->hasTag ("WORD") || (*i)->hasTag ("TERMINATED")) { if (text != "") text += ' '; text += (*i)->attribute ("raw"); } // Unknown args are accumulated as though they were WORDs. else { if (text != "") text += ' '; text += (*i)->attribute ("raw"); } } } // Task::modType determines what happens to the WORD arguments, if there are // any. if (text != "") { switch (type) { case modReplace: context.debug (label + "description <-- '" + text + "'"); set ("description", text); break; case modPrepend: context.debug (label + "description <-- '" + text + "' + description"); set ("description", text + " " + get ("description")); break; case modAppend: context.debug (label + "description <-- description + '" + text + "'"); set ("description", get ("description") + " " + text); break; case modAnnotate: context.debug (label + "new annotation <-- '" + text + "'"); addAnnotation (text); break; } } else if (modCount == 0 && text_required) { throw std::string (STRING_CMD_MODIFY_NEED_TEXT); } } //////////////////////////////////////////////////////////////////////////////// void Task::upgradeLegacyValues () { // 2.4.0 Update recurrence values. if (has ("recur")) { std::string value = get ("recur"); if (value != "") { std::string new_value = value; upgradeLegacyValue (new_value); if (new_value != value) { set ("recur", new_value); context.debug (format ("Legacy upgrade: recur {1} --> {2}", value, new_value)); } } } } //////////////////////////////////////////////////////////////////////////////// void Task::upgradeLegacyValue (std::string& value) { std::string::size_type len = value.length (); std::string::size_type p; if (value == "-") value = "0s"; else if ((p = value.find ("hr")) != std::string::npos && p == len - 2) value = value.substr (0, p) + "h"; else if ((p = value.find ("hrs")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "h"; else if ((p = value.find ("mins")) != std::string::npos && p == len - 4) value = value.substr (0, p) + "min"; else if ((p = value.find ("mnths")) != std::string::npos && p == len - 5) value = value.substr (0, p) + "mo"; else if ((p = value.find ("mos")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "mo"; else if ((p = value.find ("mth")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "mo"; else if ((p = value.find ("mths")) != std::string::npos && p == len - 4) value = value.substr (0, p) + "mo"; else if ((p = value.find ("qrtrs")) != std::string::npos && p == len - 5) value = value.substr (0, p) + "q"; else if ((p = value.find ("qtr")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "q"; else if ((p = value.find ("qtrs")) != std::string::npos && p == len - 4) value = value.substr (0, p) + "q"; else if ((p = value.find ("sec")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "s"; else if ((p = value.find ("secs")) != std::string::npos && p == len - 4) value = value.substr (0, p) + "s"; else if ((p = value.find ("wk")) != std::string::npos && p == len - 2) value = value.substr (0, p) + "w"; else if ((p = value.find ("wks")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "w"; else if ((p = value.find ("yr")) != std::string::npos && p == len - 2) value = value.substr (0, p) + "y"; else if ((p = value.find ("yrs")) != std::string::npos && p == len - 3) value = value.substr (0, p) + "y"; // It is not an error to have a non-legacy value. } ////////////////////////////////////////////////////////////////////////////////