1 ////////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez.
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included
13 // in all copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22 //
23 // http://www.opensource.org/licenses/mit-license.php
24 //
25 ////////////////////////////////////////////////////////////////////////////////
26
27 #include <cmake.h>
28 #include <sstream>
29 #include <stdlib.h>
30 #include <assert.h>
31 #ifdef PRODUCT_TASKWARRIOR
32 #include <math.h>
33 #endif
34 #include <algorithm>
35 #ifdef PRODUCT_TASKWARRIOR
36 #include <Context.h>
37 #include <Nibbler.h>
38 #endif
39 #include <Date.h>
40 #include <Duration.h>
41 #include <Task.h>
42 #include <JSON.h>
43 #ifdef PRODUCT_TASKWARRIOR
44 #include <RX.h>
45 #endif
46 #include <text.h>
47 #include <util.h>
48
49 #include <i18n.h>
50 #ifdef PRODUCT_TASKWARRIOR
51 #include <main.h>
52
53 #define APPROACHING_INFINITY 1000 // Close enough. This isn't rocket surgery.
54
55 extern Context context;
56
57 static const float epsilon = 0.000001;
58 #endif
59
60 std::string Task::defaultProject = "";
61 std::string Task::defaultDue = "";
62 bool Task::searchCaseSensitive = true;
63 bool Task::regex = false;
64 std::map <std::string, std::string> Task::attributes;
65
66 std::map <std::string, float> Task::coefficients;
67 float Task::urgencyProjectCoefficient = 0.0;
68 float Task::urgencyActiveCoefficient = 0.0;
69 float Task::urgencyScheduledCoefficient = 0.0;
70 float Task::urgencyWaitingCoefficient = 0.0;
71 float Task::urgencyBlockedCoefficient = 0.0;
72 float Task::urgencyAnnotationsCoefficient = 0.0;
73 float Task::urgencyTagsCoefficient = 0.0;
74 float Task::urgencyNextCoefficient = 0.0;
75 float Task::urgencyDueCoefficient = 0.0;
76 float Task::urgencyBlockingCoefficient = 0.0;
77 float Task::urgencyAgeCoefficient = 0.0;
78 float Task::urgencyAgeMax = 0.0;
79
80 static const std::string dummy ("");
81
82 ////////////////////////////////////////////////////////////////////////////////
Task()83 Task::Task ()
84 : id (0)
85 , urgency_value (0.0)
86 , recalc_urgency (true)
87 , is_blocked (false)
88 , is_blocking (false)
89 , annotation_count (0)
90 {
91 }
92
93 ////////////////////////////////////////////////////////////////////////////////
Task(const Task & other)94 Task::Task (const Task& other)
95 {
96 *this = other;
97 }
98
99 ////////////////////////////////////////////////////////////////////////////////
operator =(const Task & other)100 Task& Task::operator= (const Task& other)
101 {
102 if (this != &other)
103 {
104 std::map <std::string, std::string>::operator= (other);
105 id = other.id;
106 urgency_value = other.urgency_value;
107 recalc_urgency = other.recalc_urgency;
108 is_blocked = other.is_blocked;
109 is_blocking = other.is_blocking;
110 annotation_count = other.annotation_count;
111 }
112
113 return *this;
114 }
115
116 ////////////////////////////////////////////////////////////////////////////////
117 // The uuid and id attributes must be exempt from comparison.
operator ==(const Task & other)118 bool Task::operator== (const Task& other)
119 {
120 if (size () != other.size ())
121 return false;
122
123 Task::iterator i;
124 for (i = this->begin (); i != this->end (); ++i)
125 if (i->first != "uuid" &&
126 i->second != other.get (i->first))
127 return false;
128
129 return true;
130 }
131
132 ////////////////////////////////////////////////////////////////////////////////
Task(const std::string & input)133 Task::Task (const std::string& input)
134 {
135 id = 0;
136 urgency_value = 0.0;
137 recalc_urgency = true;
138 is_blocked = false;
139 is_blocking = false;
140 annotation_count = 0;
141
142 parse (input);
143 }
144
145 ////////////////////////////////////////////////////////////////////////////////
~Task()146 Task::~Task ()
147 {
148 }
149
150 ////////////////////////////////////////////////////////////////////////////////
textToStatus(const std::string & input)151 Task::status Task::textToStatus (const std::string& input)
152 {
153 if (input[0] == 'p') return Task::pending;
154 else if (input[0] == 'c') return Task::completed;
155 else if (input[0] == 'd') return Task::deleted;
156 else if (input[0] == 'r') return Task::recurring;
157 else if (input[0] == 'w') return Task::waiting;
158
159 return Task::pending;
160 }
161
162 ////////////////////////////////////////////////////////////////////////////////
statusToText(Task::status s)163 std::string Task::statusToText (Task::status s)
164 {
165 if (s == Task::pending) return "pending";
166 else if (s == Task::recurring) return "recurring";
167 else if (s == Task::waiting) return "waiting";
168 else if (s == Task::completed) return "completed";
169 else if (s == Task::deleted) return "deleted";
170
171 return "pending";
172 }
173
174 ////////////////////////////////////////////////////////////////////////////////
setEntry()175 void Task::setEntry ()
176 {
177 char entryTime[16];
178 sprintf (entryTime, "%u", (unsigned int) time (NULL));
179 set ("entry", entryTime);
180
181 recalc_urgency = true;
182 }
183
184 ////////////////////////////////////////////////////////////////////////////////
setEnd()185 void Task::setEnd ()
186 {
187 char endTime[16];
188 sprintf (endTime, "%u", (unsigned int) time (NULL));
189 set ("end", endTime);
190
191 recalc_urgency = true;
192 }
193
194 ////////////////////////////////////////////////////////////////////////////////
setStart()195 void Task::setStart ()
196 {
197 char startTime[16];
198 sprintf (startTime, "%u", (unsigned int) time (NULL));
199 set ("start", startTime);
200
201 recalc_urgency = true;
202 }
203
204 ////////////////////////////////////////////////////////////////////////////////
setModified()205 void Task::setModified ()
206 {
207 char now[16];
208 sprintf (now, "%u", (unsigned int) time (NULL));
209 set ("modified", now);
210 }
211
212 ////////////////////////////////////////////////////////////////////////////////
has(const std::string & name) const213 bool Task::has (const std::string& name) const
214 {
215 Task::const_iterator i = this->find (name);
216 if (i != this->end ())
217 return true;
218
219 return false;
220 }
221
222 ////////////////////////////////////////////////////////////////////////////////
all()223 std::vector <std::string> Task::all ()
224 {
225 std::vector <std::string> all;
226 Task::iterator i;
227 for (i = this->begin (); i != this->end (); ++i)
228 all.push_back (i->first);
229
230 return all;
231 }
232
233 ////////////////////////////////////////////////////////////////////////////////
get(const std::string & name) const234 const std::string Task::get (const std::string& name) const
235 {
236 Task::const_iterator i = this->find (name);
237 if (i != this->end ())
238 return i->second;
239
240 return "";
241 }
242
243 ////////////////////////////////////////////////////////////////////////////////
get_ref(const std::string & name) const244 const std::string& Task::get_ref (const std::string& name) const
245 {
246 Task::const_iterator i = this->find (name);
247 if (i != this->end ())
248 return i->second;
249
250 return dummy;
251 }
252
253 ////////////////////////////////////////////////////////////////////////////////
get_int(const std::string & name) const254 int Task::get_int (const std::string& name) const
255 {
256 Task::const_iterator i = this->find (name);
257 if (i != this->end ())
258 return strtol (i->second.c_str (), NULL, 10);
259
260 return 0;
261 }
262
263 ////////////////////////////////////////////////////////////////////////////////
get_ulong(const std::string & name) const264 unsigned long Task::get_ulong (const std::string& name) const
265 {
266 Task::const_iterator i = this->find (name);
267 if (i != this->end ())
268 return strtoul (i->second.c_str (), NULL, 10);
269
270 return 0;
271 }
272
273 ////////////////////////////////////////////////////////////////////////////////
get_date(const std::string & name) const274 time_t Task::get_date (const std::string& name) const
275 {
276 Task::const_iterator i = this->find (name);
277 if (i != this->end ())
278 return (time_t) strtoul (i->second.c_str (), NULL, 10);
279
280 return 0;
281 }
282
283 ////////////////////////////////////////////////////////////////////////////////
set(const std::string & name,const std::string & value)284 void Task::set (const std::string& name, const std::string& value)
285 {
286 (*this)[name] = json::decode (value);
287
288 recalc_urgency = true;
289 }
290
291 ////////////////////////////////////////////////////////////////////////////////
set(const std::string & name,int value)292 void Task::set (const std::string& name, int value)
293 {
294 (*this)[name] = format (value);
295
296 recalc_urgency = true;
297 }
298
299 ////////////////////////////////////////////////////////////////////////////////
remove(const std::string & name)300 void Task::remove (const std::string& name)
301 {
302 Task::iterator it;
303 if ((it = this->find (name)) != this->end ())
304 {
305 this->erase (it);
306 recalc_urgency = true;
307 }
308 }
309
310 ////////////////////////////////////////////////////////////////////////////////
getStatus() const311 Task::status Task::getStatus () const
312 {
313 return textToStatus (get ("status"));
314 }
315
316 ////////////////////////////////////////////////////////////////////////////////
setStatus(Task::status status)317 void Task::setStatus (Task::status status)
318 {
319 set ("status", statusToText (status));
320
321 recalc_urgency = true;
322 }
323
324 #ifdef PRODUCT_TASKWARRIOR
325 ////////////////////////////////////////////////////////////////////////////////
326 // Ready means pending, not blocked and either not scheduled or scheduled before
327 // now.
is_ready() const328 bool Task::is_ready () const
329 {
330 return getStatus () == Task::pending &&
331 !is_blocked &&
332 (! has ("scheduled") ||
333 Date ("now").operator> (get_date ("scheduled")));
334 }
335
336 ////////////////////////////////////////////////////////////////////////////////
is_due() const337 bool Task::is_due () const
338 {
339 if (has ("due"))
340 {
341 Task::status status = getStatus ();
342
343 if (status != Task::completed &&
344 status != Task::deleted)
345 {
346 if (getDueState (get ("due")) == 1)
347 return true;
348 }
349 }
350
351 return false;
352 }
353
354 ////////////////////////////////////////////////////////////////////////////////
is_dueyesterday() const355 bool Task::is_dueyesterday () const
356 {
357 if (has ("due"))
358 {
359 Task::status status = getStatus ();
360
361 if (status != Task::completed &&
362 status != Task::deleted)
363 {
364 if (Date ("yesterday").sameDay (get_date ("due")))
365 return true;
366 }
367 }
368
369 return false;
370 }
371
372 ////////////////////////////////////////////////////////////////////////////////
is_duetoday() const373 bool Task::is_duetoday () const
374 {
375 if (has ("due"))
376 {
377 Task::status status = getStatus ();
378
379 if (status != Task::completed &&
380 status != Task::deleted)
381 {
382 if (getDueState (get ("due")) == 2)
383 return true;
384 }
385 }
386
387 return false;
388 }
389
390 ////////////////////////////////////////////////////////////////////////////////
is_duetomorrow() const391 bool Task::is_duetomorrow () const
392 {
393 if (has ("due"))
394 {
395 Task::status status = getStatus ();
396
397 if (status != Task::completed &&
398 status != Task::deleted)
399 {
400 if (Date ("tomorrow").sameDay (get_date ("due")))
401 return true;
402 }
403 }
404
405 return false;
406 }
407
408 ////////////////////////////////////////////////////////////////////////////////
is_dueweek() const409 bool Task::is_dueweek () const
410 {
411 if (has ("due"))
412 {
413 Task::status status = getStatus ();
414
415 if (status != Task::completed &&
416 status != Task::deleted)
417 {
418 Date now;
419 Date due (get_date ("due"));
420 if (now.year () == due.year () &&
421 now.week () == due.week ())
422 return true;
423 }
424 }
425
426 return false;
427 }
428
429 ////////////////////////////////////////////////////////////////////////////////
is_duemonth() const430 bool Task::is_duemonth () const
431 {
432 if (has ("due"))
433 {
434 Task::status status = getStatus ();
435
436 if (status != Task::completed &&
437 status != Task::deleted)
438 {
439 Date now;
440 Date due (get_date ("due"));
441 if (now.year () == due.year () &&
442 now.month () == due.month ())
443 return true;
444 }
445 }
446
447 return false;
448 }
449
450 ////////////////////////////////////////////////////////////////////////////////
is_dueyear() const451 bool Task::is_dueyear () const
452 {
453 if (has ("due"))
454 {
455 Task::status status = getStatus ();
456
457 if (status != Task::completed &&
458 status != Task::deleted)
459 {
460 Date now;
461 Date due (get_date ("due"));
462 if (now.year () == due.year ())
463 return true;
464 }
465 }
466
467 return false;
468 }
469
470 ////////////////////////////////////////////////////////////////////////////////
is_overdue() const471 bool Task::is_overdue () const
472 {
473 if (has ("due"))
474 {
475 Task::status status = getStatus ();
476
477 if (status != Task::completed &&
478 status != Task::deleted)
479 {
480 if (getDueState (get ("due")) == 3)
481 return true;
482 }
483 }
484
485 return false;
486 }
487 #endif
488
489 ////////////////////////////////////////////////////////////////////////////////
490 // Attempt an FF4 parse first, using Task::parse, and in the event of an error
491 // try a JSON parse, otherwise a legacy parse (FF3).
492 //
493 // Note that FF1 and FF2 are no longer supported.
494 //
495 // start --> [ --> Att --> ] --> end
496 // ^ |
497 // +-------+
498 //
parse(const std::string & input)499 void Task::parse (const std::string& input)
500 {
501 // TODO Is this simply a 'chomp'?
502 std::string copy;
503 if (input[input.length () - 1] == '\n')
504 copy = input.substr (0, input.length () - 1);
505 else
506 copy = input;
507
508 try
509 {
510 // File format version 4, from 2009-5-16 - now, v1.7.1+
511 // This is the parse format tried first, because it is most used.
512 clear ();
513
514 if (copy[0] == '[')
515 {
516 Nibbler n (copy);
517 std::string line;
518 if (n.skip ('[') &&
519 n.getUntil (']', line) &&
520 n.skip (']') &&
521 n.depleted ())
522 {
523 if (line.length () == 0)
524 throw std::string (STRING_RECORD_EMPTY);
525
526 Nibbler nl (line);
527 std::string name;
528 std::string value;
529 while (!nl.depleted ())
530 {
531 if (nl.getUntil (':', name) &&
532 nl.skip (':') &&
533 nl.getQuoted ('"', value))
534 {
535 // Experimental legacy value translation of 'recur:m' --> 'recur:mo'.
536 if (name == "recur" &&
537 digitsOnly (value.substr (0, value.length () - 1)) &&
538 value[value.length () - 1] == 'm')
539 value += 'o';
540
541 // TW-1274, Standardization.
542 if (name == "modification")
543 name = "modified";
544
545 if (name.substr (0, 11) == "annotation_")
546 ++annotation_count;
547
548 (*this)[name] = decode (json::decode (value));
549 }
550
551 nl.skip (' ');
552 }
553
554 std::string remainder;
555 nl.getUntilEOS (remainder);
556 if (remainder.length ())
557 throw std::string (STRING_RECORD_JUNK_AT_EOL);
558 }
559 }
560 else if (copy[0] == '{')
561 parseJSON (copy);
562 else
563 throw std::string (STRING_RECORD_NOT_FF4);
564
565 upgradeLegacyValues ();
566 }
567
568 catch (const std::string&)
569 {
570 parseLegacy (copy);
571 }
572
573 recalc_urgency = true;
574 }
575
576 ////////////////////////////////////////////////////////////////////////////////
577 // Note that all fields undergo encode/decode.
parseJSON(const std::string & line)578 void Task::parseJSON (const std::string& line)
579 {
580 // Parse the whole thing.
581 json::value* root = json::parse (line);
582 if (root->type () == json::j_object)
583 {
584 json::object* root_obj = (json::object*)root;
585
586 // For each object element...
587 json_object_iter i;
588 for (i = root_obj->_data.begin ();
589 i != root_obj->_data.end ();
590 ++i)
591 {
592 // If the attribute is a recognized column.
593 std::string type = Task::attributes[i->first];
594 if (type != "")
595 {
596 // Any specified id is ignored.
597 if (i->first == "id")
598 ;
599
600 // Urgency, if present, is ignored.
601 else if (i->first == "urgency")
602 ;
603
604 // TW-1274 Standardization.
605 else if (i->first == "modification")
606 {
607 Date d (unquoteText (i->second->dump ()));
608 set ("modified", d.toEpochString ());
609 }
610
611 // Dates are converted from ISO to epoch.
612 else if (type == "date")
613 {
614 std::string text = unquoteText (i->second->dump ());
615 Date d (text);
616 set (i->first, text == "" ? "" : d.toEpochString ());
617 }
618
619 // Tags are an array of JSON strings.
620 else if (i->first == "tags" && i->second->type() == json::j_array)
621 {
622 json::array* tags = (json::array*)i->second;
623 json_array_iter t;
624 for (t = tags->_data.begin ();
625 t != tags->_data.end ();
626 ++t)
627 {
628 json::string* tag = (json::string*)*t;
629 addTag (tag->_data);
630 }
631 }
632 // This is a temporary measure to allow Mirakel sync, and will be removed
633 // in a future release.
634 else if (i->first == "tags" && i->second->type() == json::j_string)
635 {
636 json::string* tag = (json::string*)i->second;
637 addTag (tag->_data);
638 }
639
640 // Strings are decoded.
641 else if (type == "string")
642 set (i->first, json::decode (unquoteText (i->second->dump ())));
643
644 // Other types are simply added.
645 else
646 set (i->first, unquoteText (i->second->dump ()));
647 }
648
649 // UDA orphans and annotations do not have columns.
650 else
651 {
652 // Annotations are an array of JSON objects with 'entry' and
653 // 'description' values and must be converted.
654 if (i->first == "annotations" && i->second->type() == json::j_array)
655 {
656 std::map <std::string, std::string> annos;
657
658 json::array* atts = (json::array*)i->second;
659 json_array_iter annotations;
660 for (annotations = atts->_data.begin ();
661 annotations != atts->_data.end ();
662 ++annotations)
663 {
664 json::object* annotation = (json::object*)*annotations;
665 json::string* when = (json::string*)annotation->_data["entry"];
666 json::string* what = (json::string*)annotation->_data["description"];
667
668 if (! when)
669 throw format (STRING_TASK_NO_ENTRY, line);
670
671 if (! what)
672 throw format (STRING_TASK_NO_DESC, line);
673
674 std::string name = "annotation_" + Date (when->_data).toEpochString ();
675 annos.insert (std::make_pair (name, json::decode (what->_data)));
676 }
677
678 setAnnotations (annos);
679 }
680
681 // UDA Orphan - must be preserved.
682 else
683 {
684 #ifdef PRODUCT_TASKWARRIOR
685 std::stringstream message;
686 message << "Task::parseJSON found orphan '"
687 << i->first
688 << "' with value '"
689 << i->second
690 << "' --> preserved\n";
691 context.debug (message.str ());
692 #endif
693 set (i->first, json::decode (unquoteText (i->second->dump ())));
694 }
695 }
696 }
697
698 upgradeLegacyValues ();
699 }
700 }
701
702 ////////////////////////////////////////////////////////////////////////////////
703 // Support FF2, FF3.
704 // Thankfully FF1 is no longer supported.
parseLegacy(const std::string & line)705 void Task::parseLegacy (const std::string& line)
706 {
707 switch (determineVersion (line))
708 {
709 // File format version 1, from 2006-11-27 - 2007-12-31, v0.x+ - v0.9.3
710 case 1: throw std::string (STRING_TASK_NO_FF1);
711
712 // File format version 2, from 2008-1-1 - 2009-3-23, v0.9.3 - v1.5.0
713 case 2: throw std::string (STRING_TASK_NO_FF2);
714
715 // File format version 3, from 2009-3-23 - 2009-05-16, v1.6.0 - v1.7.1
716 case 3:
717 {
718 if (line.length () > 49) // ^.{36} . \[\] \[\] \[\] \n
719 {
720 set ("uuid", line.substr (0, 36));
721
722 Task::status status = line[37] == '+' ? completed
723 : line[37] == 'X' ? deleted
724 : line[37] == 'r' ? recurring
725 : pending;
726
727 set ("status", statusToText (status));
728
729 size_t openTagBracket = line.find ("[");
730 size_t closeTagBracket = line.find ("]", openTagBracket);
731 if (openTagBracket != std::string::npos &&
732 closeTagBracket != std::string::npos)
733 {
734 size_t openAttrBracket = line.find ("[", closeTagBracket);
735 size_t closeAttrBracket = line.find ("]", openAttrBracket);
736 if (openAttrBracket != std::string::npos &&
737 closeAttrBracket != std::string::npos)
738 {
739 size_t openAnnoBracket = line.find ("[", closeAttrBracket);
740 size_t closeAnnoBracket = line.find ("]", openAnnoBracket);
741 if (openAnnoBracket != std::string::npos &&
742 closeAnnoBracket != std::string::npos)
743 {
744 std::string tags = line.substr (
745 openTagBracket + 1, closeTagBracket - openTagBracket - 1);
746 std::vector <std::string> tagSet;
747 split (tagSet, tags, ' ');
748 addTags (tagSet);
749
750 std::string attributes = line.substr (
751 openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
752 std::vector <std::string> pairs;
753 split (pairs, attributes, ' ');
754
755 for (size_t i = 0; i < pairs.size (); ++i)
756 {
757 std::vector <std::string> pair;
758 split (pair, pairs[i], ':');
759 if (pair.size () == 2)
760 set (pair[0], pair[1]);
761 }
762
763 // Extract and split the annotations, which are of the form:
764 // 1234:"..." 5678:"..."
765 std::string annotations = line.substr (
766 openAnnoBracket + 1, closeAnnoBracket - openAnnoBracket - 1);
767 pairs.clear ();
768
769 std::string::size_type start = 0;
770 std::string::size_type end = 0;
771 do
772 {
773 end = annotations.find ('"', start);
774 if (end != std::string::npos)
775 {
776 end = annotations.find ('"', end + 1);
777
778 if (start != std::string::npos &&
779 end != std::string::npos)
780 {
781 pairs.push_back (annotations.substr (start, end - start + 1));
782 start = end + 2;
783 }
784 }
785 }
786 while (start != std::string::npos &&
787 end != std::string::npos);
788
789 for (size_t i = 0; i < pairs.size (); ++i)
790 {
791 std::string pair = pairs[i];
792 std::string::size_type colon = pair.find (":");
793 if (colon != std::string::npos)
794 {
795 std::string name = pair.substr (0, colon);
796 std::string value = pair.substr (colon + 2, pair.length () - colon - 3);
797 set ("annotation_" + name, value);
798 ++annotation_count;
799 }
800 }
801
802 set ("description", line.substr (closeAnnoBracket + 2));
803 }
804 else
805 throw std::string (STRING_TASK_PARSE_ANNO_BRACK);
806 }
807 else
808 throw std::string (STRING_TASK_PARSE_ATT_BRACK);
809 }
810 else
811 throw std::string (STRING_TASK_PARSE_TAG_BRACK);
812 }
813 else
814 throw std::string (STRING_TASK_PARSE_TOO_SHORT);
815 }
816 break;
817
818 default:
819 throw std::string (STRING_TASK_PARSE_UNREC_FF);
820 break;
821 }
822
823 recalc_urgency = true;
824 }
825
826 ////////////////////////////////////////////////////////////////////////////////
827 // The format is:
828 //
829 // [ <name>:<value> ... ]
830 //
composeF4() const831 std::string Task::composeF4 () const
832 {
833 std::string ff4 = "[";
834
835 bool first = true;
836 Task::const_iterator it;
837 for (it = this->begin (); it != this->end (); ++it)
838 {
839 if (it->second != "")
840 {
841 ff4 += (first ? "" : " ")
842 + it->first
843 + ":\""
844 + encode (json::encode (it->second))
845 + "\"";
846
847 first = false;
848 }
849 }
850
851 ff4 += "]";
852 return ff4;
853 }
854
855 ////////////////////////////////////////////////////////////////////////////////
composeJSON(bool decorate) const856 std::string Task::composeJSON (bool decorate /*= false*/) const
857 {
858 std::stringstream out;
859 out << "{";
860
861 // ID inclusion is optional, but not a good idea, because it remains correct
862 // only until the next gc.
863 if (decorate)
864 out << "\"id\":" << id << ",";
865
866 // First the non-annotations.
867 int attributes_written = 0;
868 Task::const_iterator i;
869 for (i = this->begin (); i != this->end (); ++i)
870 {
871 // Annotations are not written out here.
872 if (i->first.substr (0, 11) == "annotation_")
873 continue;
874
875 // If value is an empty string, do not ever output it
876 if (i->second == "")
877 continue;
878
879 if (attributes_written)
880 out << ",";
881
882 std::string type = Task::attributes[i->first];
883 if (type == "")
884 type = "string";
885
886 // Date fields are written as ISO 8601.
887 if (type == "date")
888 {
889 Date d (i->second);
890 out << "\""
891 << (i->first == "modification" ? "modified" : i->first)
892 << "\":\""
893 // Date was deleted, do not export parsed empty string
894 << (i->second == "" ? "" : d.toISO ())
895 << "\"";
896
897 ++attributes_written;
898 }
899
900 else if (type == "numeric")
901 {
902 out << "\""
903 << i->first
904 << "\":"
905 << i->second;
906
907 ++attributes_written;
908 }
909
910 // Tags are converted to an array.
911 else if (i->first == "tags")
912 {
913 std::vector <std::string> tags;
914 split (tags, i->second, ',');
915
916 out << "\"tags\":[";
917
918 std::vector <std::string>::iterator i;
919 for (i = tags.begin (); i != tags.end (); ++i)
920 {
921 if (i != tags.begin ())
922 out << ",";
923
924 out << "\"" << *i << "\"";
925 }
926
927 out << "]";
928
929 ++attributes_written;
930 }
931
932 // Everything else is a quoted value.
933 else
934 {
935 out << "\""
936 << i->first
937 << "\":\""
938 << json::encode (i->second)
939 << "\"";
940
941 ++attributes_written;
942 }
943 }
944
945 // Now the annotations, if any.
946 if (annotation_count)
947 {
948 out << ","
949 << "\"annotations\":[";
950
951 int annotations_written = 0;
952 for (i = this->begin (); i != this->end (); ++i)
953 {
954 if (i->first.substr (0, 11) == "annotation_")
955 {
956 if (annotations_written)
957 out << ",";
958
959 Date d (i->first.substr (11));
960 out << "{\"entry\":\""
961 << d.toISO ()
962 << "\",\"description\":\""
963 << json::encode (i->second)
964 << "\"}";
965
966 ++annotations_written;
967 }
968 }
969
970 out << "]";
971 }
972
973 #ifdef PRODUCT_TASKWARRIOR
974 // Include urgency.
975 if (decorate)
976 out << ","
977 << "\"urgency\":"
978 << urgency_c ();
979 #endif
980
981 out << "}";
982 return out.str ();
983 }
984
985 ////////////////////////////////////////////////////////////////////////////////
hasAnnotations() const986 bool Task::hasAnnotations () const
987 {
988 return annotation_count ? true : false;
989 }
990
991 ////////////////////////////////////////////////////////////////////////////////
992 // The timestamp is part of the name:
993 // annotation_1234567890:"..."
994 //
995 // Note that the time is incremented (one second) in order to find a unique
996 // timestamp.
addAnnotation(const std::string & description)997 void Task::addAnnotation (const std::string& description)
998 {
999 time_t now = time (NULL);
1000 std::string key;
1001
1002 do
1003 {
1004 key = "annotation_" + format ((int) now);
1005 ++now;
1006 }
1007 while (has (key));
1008
1009 (*this)[key] = json::decode (description);
1010 ++annotation_count;
1011 recalc_urgency = true;
1012 }
1013
1014 ////////////////////////////////////////////////////////////////////////////////
removeAnnotations()1015 void Task::removeAnnotations ()
1016 {
1017 // Erase old annotations.
1018 Task::iterator i = this->begin ();
1019 while (i != this->end ())
1020 {
1021 if (i->first.substr (0, 11) == "annotation_")
1022 {
1023 --annotation_count;
1024 this->erase (i++);
1025 }
1026 else
1027 i++;
1028 }
1029
1030 recalc_urgency = true;
1031 }
1032
1033 ////////////////////////////////////////////////////////////////////////////////
getAnnotations(std::map<std::string,std::string> & annotations) const1034 void Task::getAnnotations (std::map <std::string, std::string>& annotations) const
1035 {
1036 annotations.clear ();
1037
1038 Task::const_iterator ci;
1039 for (ci = this->begin (); ci != this->end (); ++ci)
1040 if (ci->first.substr (0, 11) == "annotation_")
1041 annotations.insert (*ci);
1042 }
1043
1044 ////////////////////////////////////////////////////////////////////////////////
setAnnotations(const std::map<std::string,std::string> & annotations)1045 void Task::setAnnotations (const std::map <std::string, std::string>& annotations)
1046 {
1047 // Erase old annotations.
1048 removeAnnotations ();
1049
1050 std::map <std::string, std::string>::const_iterator ci;
1051 for (ci = annotations.begin (); ci != annotations.end (); ++ci)
1052 this->insert (*ci);
1053
1054 annotation_count = annotations.size ();
1055 recalc_urgency = true;
1056 }
1057
1058 #ifdef PRODUCT_TASKWARRIOR
1059 ////////////////////////////////////////////////////////////////////////////////
addDependency(int id)1060 void Task::addDependency (int id)
1061 {
1062 // Check that id is resolvable.
1063 std::string uuid = context.tdb2.pending.uuid (id);
1064 if (uuid == "")
1065 throw format (STRING_TASK_DEPEND_MISS_CREA, id);
1066
1067 std::string depends = get ("depends");
1068 if (depends.find (uuid) != std::string::npos)
1069 throw format (STRING_TASK_DEPEND_DUP, this->id, id);
1070
1071 addDependency(uuid);
1072 }
1073
1074 ////////////////////////////////////////////////////////////////////////////////
addDependency(const std::string & uuid)1075 void Task::addDependency (const std::string& uuid)
1076 {
1077 if (uuid == get ("uuid"))
1078 throw std::string (STRING_TASK_DEPEND_ITSELF);
1079
1080 // Check that uuid is resolvable.
1081 int id = context.tdb2.pending.id (uuid);
1082 if (id == 0)
1083 throw format (STRING_TASK_DEPEND_MISS_CREA, id);
1084
1085 // Store the dependency.
1086 std::string depends = get ("depends");
1087 if (depends != "")
1088 {
1089 // Check for extant dependency.
1090 if (depends.find (uuid) == std::string::npos)
1091 set ("depends", depends + "," + uuid);
1092 else
1093 throw format (STRING_TASK_DEPEND_DUP, this->get ("uuid"), uuid);
1094 }
1095 else
1096 set ("depends", uuid);
1097
1098 // Prevent circular dependencies.
1099 if (dependencyIsCircular (*this))
1100 throw std::string (STRING_TASK_DEPEND_CIRCULAR);
1101
1102 recalc_urgency = true;
1103 }
1104
1105 ////////////////////////////////////////////////////////////////////////////////
removeDependency(const std::string & uuid)1106 void Task::removeDependency (const std::string& uuid)
1107 {
1108 std::vector <std::string> deps;
1109 split (deps, get ("depends"), ',');
1110
1111 std::vector <std::string>::iterator i;
1112 i = std::find (deps.begin (), deps.end (), uuid);
1113 if (i != deps.end ())
1114 {
1115 deps.erase (i);
1116 std::string combined;
1117 join (combined, ",", deps);
1118 set ("depends", combined);
1119 recalc_urgency = true;
1120 }
1121 else
1122 throw format (STRING_TASK_DEPEND_MISS_DEL, uuid);
1123 }
1124
1125 ////////////////////////////////////////////////////////////////////////////////
removeDependency(int id)1126 void Task::removeDependency (int id)
1127 {
1128 std::string depends = get ("depends");
1129 std::string uuid = context.tdb2.pending.uuid (id);
1130 if (uuid != "" && depends.find (uuid) != std::string::npos)
1131 removeDependency (uuid);
1132 else
1133 throw format (STRING_TASK_DEPEND_MISS_DEL, id);
1134 }
1135
1136 ////////////////////////////////////////////////////////////////////////////////
getDependencies(std::vector<int> & all) const1137 void Task::getDependencies (std::vector <int>& all) const
1138 {
1139 std::vector <std::string> deps;
1140 split (deps, get ("depends"), ',');
1141
1142 all.clear ();
1143
1144 std::vector <std::string>::iterator i;
1145 for (i = deps.begin (); i != deps.end (); ++i)
1146 all.push_back (context.tdb2.pending.id (*i));
1147 }
1148
1149 ////////////////////////////////////////////////////////////////////////////////
getDependencies(std::vector<std::string> & all) const1150 void Task::getDependencies (std::vector <std::string>& all) const
1151 {
1152 all.clear ();
1153 split (all, get ("depends"), ',');
1154 }
1155 #endif
1156
1157 ////////////////////////////////////////////////////////////////////////////////
getTagCount() const1158 int Task::getTagCount () const
1159 {
1160 std::vector <std::string> tags;
1161 split (tags, get ("tags"), ',');
1162
1163 return (int) tags.size ();
1164 }
1165
1166 ////////////////////////////////////////////////////////////////////////////////
hasTag(const std::string & tag) const1167 bool Task::hasTag (const std::string& tag) const
1168 {
1169 // Synthetic tags - dynamically generated, but do not occupy storage space.
1170 if (tag == "BLOCKED") return is_blocked;
1171 if (tag == "UNBLOCKED") return !is_blocked;
1172 if (tag == "BLOCKING") return is_blocking;
1173 #ifdef PRODUCT_TASKWARRIOR
1174 if (tag == "READY") return is_ready ();
1175 if (tag == "DUE") return is_due ();
1176 if (tag == "DUETODAY") return is_duetoday ();
1177 if (tag == "TODAY") return is_duetoday ();
1178 if (tag == "YESTERDAY") return is_dueyesterday ();
1179 if (tag == "TOMORROW") return is_duetomorrow ();
1180 if (tag == "OVERDUE") return is_overdue ();
1181 if (tag == "WEEK") return is_dueweek ();
1182 if (tag == "MONTH") return is_duemonth ();
1183 if (tag == "YEAR") return is_dueyear ();
1184 #endif
1185 if (tag == "ACTIVE") return has ("start");
1186 if (tag == "SCHEDULED") return has ("scheduled");
1187 if (tag == "CHILD") return has ("parent");
1188 if (tag == "UNTIL") return has ("until");
1189 if (tag == "WAITING") return has ("wait");
1190 if (tag == "ANNOTATED") return hasAnnotations ();
1191 if (tag == "PARENT") return has ("mask");
1192
1193 // Concrete tags.
1194 std::vector <std::string> tags;
1195 split (tags, get ("tags"), ',');
1196
1197 if (std::find (tags.begin (), tags.end (), tag) != tags.end ())
1198 return true;
1199
1200 return false;
1201 }
1202
1203 ////////////////////////////////////////////////////////////////////////////////
addTag(const std::string & tag)1204 void Task::addTag (const std::string& tag)
1205 {
1206 std::vector <std::string> tags;
1207 split (tags, get ("tags"), ',');
1208
1209 if (std::find (tags.begin (), tags.end (), tag) == tags.end ())
1210 {
1211 tags.push_back (tag);
1212 std::string combined;
1213 join (combined, ",", tags);
1214 set ("tags", combined);
1215
1216 recalc_urgency = true;
1217 }
1218 }
1219
1220 ////////////////////////////////////////////////////////////////////////////////
addTags(const std::vector<std::string> & tags)1221 void Task::addTags (const std::vector <std::string>& tags)
1222 {
1223 remove ("tags");
1224
1225 std::vector <std::string>::const_iterator it;
1226 for (it = tags.begin (); it != tags.end (); ++it)
1227 addTag (*it);
1228
1229 recalc_urgency = true;
1230 }
1231
1232 ////////////////////////////////////////////////////////////////////////////////
getTags(std::vector<std::string> & tags) const1233 void Task::getTags (std::vector<std::string>& tags) const
1234 {
1235 split (tags, get ("tags"), ',');
1236 }
1237
1238 ////////////////////////////////////////////////////////////////////////////////
removeTag(const std::string & tag)1239 void Task::removeTag (const std::string& tag)
1240 {
1241 std::vector <std::string> tags;
1242 split (tags, get ("tags"), ',');
1243
1244 std::vector <std::string>::iterator i;
1245 i = std::find (tags.begin (), tags.end (), tag);
1246 if (i != tags.end ())
1247 {
1248 tags.erase (i);
1249 std::string combined;
1250 join (combined, ",", tags);
1251 set ("tags", combined);
1252 }
1253
1254 recalc_urgency = true;
1255 }
1256
1257 #ifdef PRODUCT_TASKWARRIOR
1258 ////////////////////////////////////////////////////////////////////////////////
1259 // A UDA is an attribute that has supporting config entries such as a data type:
1260 // 'uda.<name>.type'
getUDAs(std::vector<std::string> & names) const1261 void Task::getUDAs (std::vector <std::string>& names) const
1262 {
1263 Task::const_iterator it;
1264 for (it = this->begin (); it != this->end (); ++it)
1265 if (context.config.get ("uda." + it->first + ".type") != "")
1266 names.push_back (it->first);
1267 }
1268
1269 ////////////////////////////////////////////////////////////////////////////////
1270 // A UDA Orphan is an attribute that is not represented in context.columns.
getUDAOrphans(std::vector<std::string> & names) const1271 void Task::getUDAOrphans (std::vector <std::string>& names) const
1272 {
1273 Task::const_iterator it;
1274 for (it = this->begin (); it != this->end (); ++it)
1275 if (it->first.substr (0, 11) != "annotation_")
1276 if (context.columns.find (it->first) == context.columns.end ())
1277 names.push_back (it->first);
1278 }
1279
1280 ////////////////////////////////////////////////////////////////////////////////
substitute(const std::string & from,const std::string & to,bool global)1281 void Task::substitute (
1282 const std::string& from,
1283 const std::string& to,
1284 bool global)
1285 {
1286 // Get the data to modify.
1287 std::string description = get ("description");
1288 std::map <std::string, std::string> annotations;
1289 getAnnotations (annotations);
1290
1291 // Count the changes, so we know whether to proceed to annotations, after
1292 // modifying description.
1293 int changes = 0;
1294 bool done = false;
1295
1296 // Regex support is optional.
1297 if (Task::regex)
1298 {
1299 // Create the regex.
1300 RX rx (from, Task::searchCaseSensitive);
1301 std::vector <int> start;
1302 std::vector <int> end;
1303
1304 // Perform all subs on description.
1305 if (rx.match (start, end, description))
1306 {
1307 int skew = 0;
1308 for (unsigned int i = 0; i < start.size () && !done; ++i)
1309 {
1310 description.replace (start[i + skew], end[i] - start[i], to);
1311 skew += to.length () - (end[i] - start[i]);
1312 ++changes;
1313
1314 if (!global)
1315 done = true;
1316 }
1317 }
1318
1319 if (!done)
1320 {
1321 // Perform all subs on annotations.
1322 std::map <std::string, std::string>::iterator it;
1323 for (it = annotations.begin (); it != annotations.end () && !done; ++it)
1324 {
1325 start.clear ();
1326 end.clear ();
1327 if (rx.match (start, end, it->second))
1328 {
1329 int skew = 0;
1330 for (unsigned int i = 0; i < start.size () && !done; ++i)
1331 {
1332 it->second.replace (start[i + skew], end[i] - start[i], to);
1333 skew += to.length () - (end[i] - start[i]);
1334 ++changes;
1335
1336 if (!global)
1337 done = true;
1338 }
1339 }
1340 }
1341 }
1342 }
1343 else
1344 {
1345 // Perform all subs on description.
1346 int counter = 0;
1347 std::string::size_type pos = 0;
1348 int skew = 0;
1349
1350 while ((pos = ::find (description, from, pos, Task::searchCaseSensitive)) != std::string::npos && !done)
1351 {
1352 description.replace (pos + skew, from.length (), to);
1353 skew += to.length () - from.length ();
1354
1355 pos += to.length ();
1356 ++changes;
1357
1358 if (!global)
1359 done = true;
1360
1361 if (++counter > APPROACHING_INFINITY)
1362 throw format (STRING_INFINITE_LOOP, APPROACHING_INFINITY);
1363 }
1364
1365 if (!done)
1366 {
1367 // Perform all subs on annotations.
1368 counter = 0;
1369 std::map <std::string, std::string>::iterator i;
1370 for (i = annotations.begin (); i != annotations.end () && !done; ++i)
1371 {
1372 pos = 0;
1373 skew = 0;
1374 while ((pos = ::find (i->second, from, pos, Task::searchCaseSensitive)) != std::string::npos && !done)
1375 {
1376 i->second.replace (pos + skew, from.length (), to);
1377 skew += to.length () - from.length ();
1378
1379 pos += to.length ();
1380 ++changes;
1381
1382 if (!global)
1383 done = true;
1384
1385 if (++counter > APPROACHING_INFINITY)
1386 throw format (STRING_INFINITE_LOOP, APPROACHING_INFINITY);
1387 }
1388 }
1389 }
1390 }
1391
1392 if (changes)
1393 {
1394 set ("description", description);
1395 setAnnotations (annotations);
1396 recalc_urgency = true;
1397 }
1398 }
1399 #endif
1400
1401 ////////////////////////////////////////////////////////////////////////////////
1402 // The purpose of Task::validate is three-fold:
1403 // 1) To provide missing attributes where possible
1404 // 2) To provide suitable warnings about odd states
1405 // 3) To generate errors when the inconsistencies are not fixable
1406 //
validate(bool applyDefault)1407 void Task::validate (bool applyDefault /* = true */)
1408 {
1409 Task::status status = getStatus ();
1410
1411 // 1) Provide missing attributes where possible
1412 // Provide a UUID if necessary.
1413 if (! has ("uuid") || get ("uuid") == "")
1414 set ("uuid", uuid ());
1415
1416 // Recurring tasks get a special status.
1417 if (status == Task::pending &&
1418 has ("due") &&
1419 has ("recur") &&
1420 (! has ("parent") || get ("parent") == ""))
1421 status = Task::recurring;
1422
1423 // Tasks with a wait: date get a special status.
1424 else if (status == Task::pending &&
1425 has ("wait"))
1426 status = Task::waiting;
1427
1428 // By default, tasks are pending.
1429 else if (! has ("status") || get ("status") == "")
1430 status = Task::pending;
1431
1432 // Store the derived status.
1433 setStatus (status);
1434
1435 #ifdef PRODUCT_TASKWARRIOR
1436 // Provide an entry date unless user already specified one.
1437 if (!has ("entry"))
1438 setEntry ();
1439
1440 // Completed tasks need an end date, so inherit the entry date.
1441 if (! has ("end") &&
1442 (getStatus () == Task::completed ||
1443 getStatus () == Task::deleted))
1444 setEnd ();
1445
1446 // Override with default.project, if not specified.
1447 if (applyDefault && ! has ("project"))
1448 {
1449 if (Task::defaultProject != "" &&
1450 context.columns["project"]->validate (Task::defaultProject))
1451 set ("project", Task::defaultProject);
1452 }
1453
1454 // Override with default.due, if not specified.
1455 if (applyDefault && get ("due") == "")
1456 {
1457 if (Task::defaultDue != "" &&
1458 context.columns["due"]->validate (Task::defaultDue))
1459 set ("due", Date (Task::defaultDue).toEpoch ());
1460 }
1461
1462 // If a UDA has a default value in the configuration,
1463 // override with uda.(uda).default, if not specified.
1464 if (applyDefault)
1465 {
1466 // Gather a list of all UDAs with a .default value
1467 std::vector <std::string> udas;
1468 Config::const_iterator var;
1469 for (var = context.config.begin (); var != context.config.end (); ++var)
1470 {
1471 if (var->first.substr (0, 4) == "uda." &&
1472 var->first.find (".default") != std::string::npos)
1473 {
1474 std::string::size_type period = var->first.find ('.', 4);
1475 if (period != std::string::npos)
1476 udas.push_back (var->first.substr (4, period - 4));
1477 }
1478 }
1479
1480 if (udas.size ())
1481 {
1482 // For each of those, setup the default value on the task now,
1483 // of course only if we don't have one on the command line already
1484 std::vector <std::string>::iterator uda;
1485 for (uda = udas.begin (); uda != udas.end (); ++uda)
1486 {
1487 std::string type = context.config.get ("uda." + *uda + ".type");
1488 std::string defVal = context.config.get ("uda." + *uda + ".default");
1489
1490 // If the default is empty, or we already have a value, skip it
1491 if (defVal != "" && get (*uda) == "")
1492 set (*uda, defVal);
1493 }
1494 }
1495 }
1496 #endif
1497
1498 // 2) To provide suitable warnings about odd states
1499
1500 // Date relationships.
1501 validate_before ("wait", "due");
1502 validate_before ("entry", "start");
1503 validate_before ("entry", "end");
1504 validate_before ("wait", "scheduled");
1505 validate_before ("scheduled", "start");
1506 validate_before ("scheduled", "due");
1507 validate_before ("scheduled", "end");
1508
1509 // 3) To generate errors when the inconsistencies are not fixable
1510
1511 // There is no fixing a missing description.
1512 if (!has ("description"))
1513 throw std::string (STRING_TASK_VALID_DESC);
1514 else if (get ("description") == "")
1515 throw std::string (STRING_TASK_VALID_BLANK);
1516
1517 // Cannot have a recur frequency with no due date - when would it recur?
1518 if (! has ("due") && has ("recur"))
1519 throw std::string (STRING_TASK_VALID_REC_DUE);
1520
1521 // Recur durations must be valid.
1522 if (has ("recur"))
1523 {
1524 Duration d;
1525 if (! d.valid (get ("recur")))
1526 throw std::string (format (STRING_TASK_VALID_RECUR, get ("recur")));
1527 }
1528 }
1529
1530 ////////////////////////////////////////////////////////////////////////////////
validate_before(const std::string & left,const std::string & right)1531 void Task::validate_before (const std::string& left, const std::string& right)
1532 {
1533 #ifdef PRODUCT_TASKWARRIOR
1534 if (has (left) &&
1535 has (right))
1536 {
1537 Date date_left (get_date (left));
1538 Date date_right (get_date (right));
1539
1540 if (date_left > date_right)
1541 context.footnote (format (STRING_TASK_VALID_BEFORE, left, right));
1542 }
1543 #endif
1544 }
1545
1546 ////////////////////////////////////////////////////////////////////////////////
1547 // Encode values prior to serialization.
1548 // [ -> &open;
1549 // ] -> &close;
encode(const std::string & value) const1550 const std::string Task::encode (const std::string& value) const
1551 {
1552 std::string modified = value;
1553
1554 str_replace (modified, "[", "&open;");
1555 str_replace (modified, "]", "&close;");
1556
1557 return modified;
1558 }
1559
1560 ////////////////////////////////////////////////////////////////////////////////
1561 // Decode values after parse.
1562 // " <- &dquot;
1563 // ' <- &squot; or "
1564 // , <- ,
1565 // [ <- &open;
1566 // ] <- &close;
1567 // : <- :
decode(const std::string & value) const1568 const std::string Task::decode (const std::string& value) const
1569 {
1570 if (value.find ('&') != std::string::npos)
1571 {
1572 std::string modified = value;
1573
1574 // Supported encodings.
1575 str_replace (modified, "&open;", "[");
1576 str_replace (modified, "&close;", "]");
1577
1578 // Support for deprecated encodings. These cannot be removed or old files
1579 // will not be parsable. Not just old files - completed.data can contain
1580 // tasks formatted/encoded using these.
1581 str_replace (modified, "&dquot;", "\"");
1582 str_replace (modified, """, "'");
1583 str_replace (modified, "&squot;", "'"); // Deprecated 2.0
1584 str_replace (modified, ",", ","); // Deprecated 2.0
1585 str_replace (modified, ":", ":"); // Deprecated 2.0
1586
1587 return modified;
1588 }
1589
1590 return value;
1591 }
1592
1593 ////////////////////////////////////////////////////////////////////////////////
determineVersion(const std::string & line)1594 int Task::determineVersion (const std::string& line)
1595 {
1596 // Version 2 looks like:
1597 //
1598 // uuid status [tags] [attributes] description\n
1599 //
1600 // Where uuid looks like:
1601 //
1602 // 27755d92-c5e9-4c21-bd8e-c3dd9e6d3cf7
1603 //
1604 // Scan for the hyphens in the uuid, the following space, and a valid status
1605 // character.
1606 if (line[8] == '-' &&
1607 line[13] == '-' &&
1608 line[18] == '-' &&
1609 line[23] == '-' &&
1610 line[36] == ' ' &&
1611 (line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r'))
1612 {
1613 // Version 3 looks like:
1614 //
1615 // uuid status [tags] [attributes] [annotations] description\n
1616 //
1617 // Scan for the number of [] pairs.
1618 std::string::size_type tagAtts = line.find ("] [", 0);
1619 std::string::size_type attsAnno = line.find ("] [", tagAtts + 1);
1620 std::string::size_type annoDesc = line.find ("] ", attsAnno + 1);
1621 if (tagAtts != std::string::npos &&
1622 attsAnno != std::string::npos &&
1623 annoDesc != std::string::npos)
1624 return 3;
1625 else
1626 return 2;
1627 }
1628
1629 // Version 4 looks like:
1630 //
1631 // [name:"value" ...]
1632 //
1633 // Scan for [, ] and :".
1634 else if (line[0] == '[' &&
1635 line[line.length () - 1] == ']' &&
1636 line.find ("uuid:\"") != std::string::npos)
1637 return 4;
1638
1639 // Version 1 looks like:
1640 //
1641 // [tags] [attributes] description\n
1642 // X [tags] [attributes] description\n
1643 //
1644 // Scan for the first character being either the bracket or X.
1645 else if (line.find ("X [") == 0 ||
1646 line.find ("uuid") == std::string::npos ||
1647 (line[0] == '[' &&
1648 line.substr (line.length () - 1, 1) != "]"))
1649 return 1;
1650
1651 // Version 5?
1652 //
1653 // Fortunately, with the hindsight that will come with version 5, the
1654 // identifying characteristics of 1, 2, 3 and 4 may be modified such that if 5
1655 // has a UUID followed by a status, then there is still a way to differentiate
1656 // between 2, 3, 4 and 5.
1657 //
1658 // The danger is that a version 3 binary reads and misinterprets a version 4
1659 // file. This is why it is a good idea to rely on an explicit version
1660 // declaration rather than chance positioning.
1661
1662 // Zero means 'no idea'.
1663 return 0;
1664 }
1665
1666 ////////////////////////////////////////////////////////////////////////////////
1667 // Urgency is defined as a polynomial, the value of which is calculated in this
1668 // function, according to:
1669 //
1670 // U = A.t + B.t + C.t ...
1671 // a b c
1672 //
1673 // U = urgency
1674 // A = coefficient for term a
1675 // t sub a = numeric scale from 0 -> 1, with 1 being the highest
1676 // urgency, derived from one task attribute and mapped
1677 // to the numeric scale
1678 //
1679 // See rfc31-urgency.txt for full details.
1680 //
urgency_c() const1681 float Task::urgency_c () const
1682 {
1683 float value = 0.0;
1684 #ifdef PRODUCT_TASKWARRIOR
1685 value += fabsf (Task::urgencyProjectCoefficient) > epsilon ? (urgency_project () * Task::urgencyProjectCoefficient) : 0.0;
1686 value += fabsf (Task::urgencyActiveCoefficient) > epsilon ? (urgency_active () * Task::urgencyActiveCoefficient) : 0.0;
1687 value += fabsf (Task::urgencyScheduledCoefficient) > epsilon ? (urgency_scheduled () * Task::urgencyScheduledCoefficient) : 0.0;
1688 value += fabsf (Task::urgencyWaitingCoefficient) > epsilon ? (urgency_waiting () * Task::urgencyWaitingCoefficient) : 0.0;
1689 value += fabsf (Task::urgencyBlockedCoefficient) > epsilon ? (urgency_blocked () * Task::urgencyBlockedCoefficient) : 0.0;
1690 value += fabsf (Task::urgencyAnnotationsCoefficient) > epsilon ? (urgency_annotations () * Task::urgencyAnnotationsCoefficient) : 0.0;
1691 value += fabsf (Task::urgencyTagsCoefficient) > epsilon ? (urgency_tags () * Task::urgencyTagsCoefficient) : 0.0;
1692 value += fabsf (Task::urgencyNextCoefficient) > epsilon ? (urgency_next () * Task::urgencyNextCoefficient) : 0.0;
1693 value += fabsf (Task::urgencyDueCoefficient) > epsilon ? (urgency_due () * Task::urgencyDueCoefficient) : 0.0;
1694 value += fabsf (Task::urgencyBlockingCoefficient) > epsilon ? (urgency_blocking () * Task::urgencyBlockingCoefficient) : 0.0;
1695 value += fabsf (Task::urgencyAgeCoefficient) > epsilon ? (urgency_age () * Task::urgencyAgeCoefficient) : 0.0;
1696
1697 // Tag- and project-specific coefficients.
1698 std::map <std::string, float>::iterator var;
1699 for (var = Task::coefficients.begin (); var != Task::coefficients.end (); ++var)
1700 {
1701 if (fabs (var->second) > epsilon)
1702 {
1703 if (var->first.substr (0, 13) == "urgency.user.")
1704 {
1705 // urgency.user.project.<project>.coefficient
1706 std::string::size_type end = std::string::npos;
1707 if (var->first.substr (13, 8) == "project." &&
1708 (end = var->first.find (".coefficient")) != std::string::npos)
1709 {
1710 std::string project = var->first.substr (21, end - 21);
1711
1712 if (get ("project").find (project) == 0)
1713 value += var->second;
1714 }
1715
1716 // urgency.user.tag.<tag>.coefficient
1717 if (var->first.substr (13, 4) == "tag." &&
1718 (end = var->first.find (".coefficient")) != std::string::npos)
1719 {
1720 std::string tag = var->first.substr (17, end - 17);
1721
1722 if (hasTag (tag))
1723 value += var->second;
1724 }
1725 }
1726 else if (var->first.substr (0, 12) == "urgency.uda.")
1727 {
1728 // urgency.uda.<name>.coefficient
1729 std::string::size_type end = var->first.find (".coefficient");
1730 if (end != std::string::npos)
1731 if (has (var->first.substr (12, end - 12)))
1732 value += var->second;
1733 }
1734 }
1735 }
1736 #endif
1737
1738 return value;
1739 }
1740
1741 ////////////////////////////////////////////////////////////////////////////////
urgency()1742 float Task::urgency ()
1743 {
1744 if (recalc_urgency)
1745 {
1746 urgency_value = urgency_c ();
1747
1748 // Return the sum of all terms.
1749 recalc_urgency = false;
1750 }
1751
1752 return urgency_value;
1753 }
1754
1755 ////////////////////////////////////////////////////////////////////////////////
urgency_project() const1756 float Task::urgency_project () const
1757 {
1758 if (has ("project"))
1759 return 1.0;
1760
1761 return 0.0;
1762 }
1763
1764 ////////////////////////////////////////////////////////////////////////////////
urgency_active() const1765 float Task::urgency_active () const
1766 {
1767 if (has ("start"))
1768 return 1.0;
1769
1770 return 0.0;
1771 }
1772
1773 ////////////////////////////////////////////////////////////////////////////////
urgency_scheduled() const1774 float Task::urgency_scheduled () const
1775 {
1776 if (has ("scheduled") &&
1777 get_date ("scheduled") < time (NULL))
1778 return 1.0;
1779
1780 return 0.0;
1781 }
1782
1783 ////////////////////////////////////////////////////////////////////////////////
urgency_waiting() const1784 float Task::urgency_waiting () const
1785 {
1786 if (get_ref ("status") == "waiting")
1787 return 1.0;
1788
1789 return 0.0;
1790 }
1791
1792 ////////////////////////////////////////////////////////////////////////////////
1793 // A task is blocked only if the task it depends upon is pending/waiting.
urgency_blocked() const1794 float Task::urgency_blocked () const
1795 {
1796 if (is_blocked)
1797 return 1.0;
1798
1799 return 0.0;
1800 }
1801
1802 ////////////////////////////////////////////////////////////////////////////////
urgency_annotations() const1803 float Task::urgency_annotations () const
1804 {
1805 if (annotation_count >= 3) return 1.0;
1806 else if (annotation_count == 2) return 0.9;
1807 else if (annotation_count == 1) return 0.8;
1808
1809 return 0.0;
1810 }
1811
1812 ////////////////////////////////////////////////////////////////////////////////
urgency_tags() const1813 float Task::urgency_tags () const
1814 {
1815 switch (getTagCount ())
1816 {
1817 case 0: return 0.0;
1818 case 1: return 0.8;
1819 case 2: return 0.9;
1820 default: return 1.0;
1821 }
1822 }
1823
1824 ////////////////////////////////////////////////////////////////////////////////
urgency_next() const1825 float Task::urgency_next () const
1826 {
1827 if (hasTag ("next"))
1828 return 1.0;
1829
1830 return 0.0;
1831 }
1832
1833 ////////////////////////////////////////////////////////////////////////////////
1834 //
1835 // Past Present Future
1836 // Overdue Due Due
1837 //
1838 // -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 days
1839 //
1840 // <-- 1.0 linear 0.2 -->
1841 // capped capped
1842 //
1843 //
urgency_due() const1844 float Task::urgency_due () const
1845 {
1846 if (has ("due"))
1847 {
1848 Date now;
1849 Date due (get_date ("due"));
1850
1851 // Map a range of 21 days to the value 0.2 - 1.0
1852 float days_overdue = (now - due) / 86400.0;
1853 if (days_overdue >= 7.0) return 1.0; // < 1 wk ago
1854 else if (days_overdue >= -14.0) return ((days_overdue + 14.0) * 0.8 / 21.0) + 0.2;
1855 else return 0.2; // > 2 wks
1856 }
1857
1858 return 0.0;
1859 }
1860
1861 ////////////////////////////////////////////////////////////////////////////////
urgency_age() const1862 float Task::urgency_age () const
1863 {
1864 assert (has ("entry"));
1865
1866 Date now;
1867 Date entry (get_date ("entry"));
1868 int age = (now - entry) / 86400; // in days
1869
1870 if (Task::urgencyAgeMax == 0 || age > Task::urgencyAgeMax)
1871 return 1.0;
1872
1873 return (1.0 * age / Task::urgencyAgeMax);
1874 }
1875
1876 ////////////////////////////////////////////////////////////////////////////////
urgency_blocking() const1877 float Task::urgency_blocking () const
1878 {
1879 if (is_blocking)
1880 return 1.0;
1881
1882 return 0.0;
1883 }
1884
1885 ////////////////////////////////////////////////////////////////////////////////
upgradeLegacyValues()1886 void Task::upgradeLegacyValues ()
1887 {
1888 // 2.4.0 Update recurrence values.
1889 if (has ("recur"))
1890 {
1891 std::string value = get ("recur");
1892 std::string new_value = "";
1893 std::string::size_type len = value.length ();
1894 std::string::size_type p;
1895
1896 if (value == "-") new_value = "0s";
1897 else if ((p = value.find ("hr")) != std::string::npos && p == len - 2) new_value = value.substr (0, p) + "h";
1898 else if ((p = value.find ("hrs")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "h";
1899 else if ((p = value.find ("mins")) != std::string::npos && p == len - 4) new_value = value.substr (0, p) + "min";
1900 else if ((p = value.find ("mnths")) != std::string::npos && p == len - 5) new_value = value.substr (0, p) + "mo";
1901 else if ((p = value.find ("mos")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "mo";
1902 else if ((p = value.find ("mth")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "mo";
1903 else if ((p = value.find ("mths")) != std::string::npos && p == len - 4) new_value = value.substr (0, p) + "mo";
1904 else if ((p = value.find ("qrtrs")) != std::string::npos && p == len - 5) new_value = value.substr (0, p) + "q";
1905 else if ((p = value.find ("qtr")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "q";
1906 else if ((p = value.find ("qtrs")) != std::string::npos && p == len - 4) new_value = value.substr (0, p) + "q";
1907 else if ((p = value.find ("sec")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "s";
1908 else if ((p = value.find ("secs")) != std::string::npos && p == len - 4) new_value = value.substr (0, p) + "s";
1909 else if ((p = value.find ("wk")) != std::string::npos && p == len - 2) new_value = value.substr (0, p) + "w";
1910 else if ((p = value.find ("wks")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "w";
1911 else if ((p = value.find ("yr")) != std::string::npos && p == len - 2) new_value = value.substr (0, p) + "y";
1912 else if ((p = value.find ("yrs")) != std::string::npos && p == len - 3) new_value = value.substr (0, p) + "y";
1913
1914 if (new_value != "" &&
1915 new_value != value)
1916 {
1917 set ("recur", new_value);
1918 }
1919 }
1920 }
1921
1922 ////////////////////////////////////////////////////////////////////////////////
1923