• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GothenburgBitFactory / taskwarrior / 9689737168

27 Jun 2024 02:29AM UTC coverage: 84.216% (-0.3%) from 84.513%
9689737168

push

github

web-flow
Un-deprecate the non-1/0 boolean values (#3522)

As per https://github.com/GothenburgBitFactory/tw.org/issues/867

Co-authored-by: Sebastian Carlos <sebastiancarlos@gmail.com>

19251 of 22859 relevant lines covered (84.22%)

19999.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.17
/src/Task.cpp
1
////////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2006 - 2021, Tomas Babej, 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
// https://www.opensource.org/licenses/mit-license.php
24
//
25
////////////////////////////////////////////////////////////////////////////////
26

27
#include <cmake.h>
28
#include <Task.h>
29
#include <sstream>
30
#include <stdlib.h>
31
#include <assert.h>
32
#include <string>
33
#ifdef PRODUCT_TASKWARRIOR
34
#include <math.h>
35
#include <ctype.h>
36
#endif
37
#include <cfloat>
38
#include <algorithm>
39
#include <Lexer.h>
40
#ifdef PRODUCT_TASKWARRIOR
41
#include <Context.h>
42
#include <Pig.h>
43
#endif
44
#include <Duration.h>
45
#include <Datetime.h>
46
#ifdef PRODUCT_TASKWARRIOR
47
#include <RX.h>
48
#endif
49
#include <shared.h>
50
#include <format.h>
51
#include <util.h>
52

53
#ifdef PRODUCT_TASKWARRIOR
54
#include <main.h>
55

56
#include <Eval.h>
57
#include <Variant.h>
58
#include <Filter.h>
59

60

61
#define APPROACHING_INFINITY 1000   // Close enough.  This isn't rocket surgery.
62

63
static const float epsilon = 0.000001;
64
#endif
65

66
std::string Task::defaultProject   = "";
67
std::string Task::defaultDue       = "";
68
std::string Task::defaultScheduled = "";
69
bool Task::searchCaseSensitive     = true;
70
bool Task::regex                   = false;
71
std::map <std::string, std::string> Task::attributes;
72

73
std::map <std::string, float> Task::coefficients;
74
float Task::urgencyProjectCoefficient     = 0.0;
75
float Task::urgencyActiveCoefficient      = 0.0;
76
float Task::urgencyScheduledCoefficient   = 0.0;
77
float Task::urgencyWaitingCoefficient     = 0.0;
78
float Task::urgencyBlockedCoefficient     = 0.0;
79
float Task::urgencyAnnotationsCoefficient = 0.0;
80
float Task::urgencyTagsCoefficient        = 0.0;
81
float Task::urgencyDueCoefficient         = 0.0;
82
float Task::urgencyBlockingCoefficient    = 0.0;
83
float Task::urgencyAgeCoefficient         = 0.0;
84
float Task::urgencyAgeMax                 = 0.0;
85

86
std::map <std::string, std::vector <std::string>> Task::customOrder;
87

88
static const std::string dummy ("");
89

90
////////////////////////////////////////////////////////////////////////////////
91
// The uuid and id attributes must be exempt from comparison.
92
//
93
// This performs two tests which are sufficient and necessary for Task
94
// object equality (neglecting uuid and id):
95
//     - The attribute set sizes are the same
96
//     - For each attribute in the first set, there exists a same
97
//       attribute with a same value in the second set
98
//
99
// These two conditions are necessary. They are also sufficient, since there
100
// can be no extra data attribute in the second set, due to the same attribute
101
// set sizes.
102
bool Task::operator== (const Task& other)
144✔
103
{
104
  if (data.size () != other.data.size ())
144✔
105
    return false;
77✔
106

107
  for (const auto& i : data)
217✔
108
    if (i.first != "uuid" &&
384✔
109
        i.second != other.get (i.first))
384✔
110
      return false;
50✔
111

112
  return true;
17✔
113
}
114

115
////////////////////////////////////////////////////////////////////////////////
116
bool Task::operator!= (const Task& other)
141✔
117
{
118
  return !(*this == other);
141✔
119
}
120

121
////////////////////////////////////////////////////////////////////////////////
122
Task::Task (const std::string& input)
15✔
123
{
124
  id               = 0;
15✔
125
  urgency_value    = 0.0;
15✔
126
  recalc_urgency   = true;
15✔
127
  is_blocked       = false;
15✔
128
  is_blocking      = false;
15✔
129
  annotation_count = 0;
15✔
130

131
  parse (input);
15✔
132
}
15✔
133

134
////////////////////////////////////////////////////////////////////////////////
135
Task::Task (const json::object* obj)
1,599✔
136
{
137
  id               = 0;
1,599✔
138
  urgency_value    = 0.0;
1,599✔
139
  recalc_urgency   = true;
1,599✔
140
  is_blocked       = false;
1,599✔
141
  is_blocking      = false;
1,599✔
142
  annotation_count = 0;
1,599✔
143

144
  parseJSON (obj);
1,599✔
145
}
1,599✔
146

147
////////////////////////////////////////////////////////////////////////////////
148
Task::Task (tc::Task obj)
25,095✔
149
{
150
  id               = 0;
25,095✔
151
  urgency_value    = 0.0;
25,095✔
152
  recalc_urgency   = true;
25,095✔
153
  is_blocked       = false;
25,095✔
154
  is_blocking      = false;
25,095✔
155
  annotation_count = 0;
25,095✔
156

157
  parseTC (obj);
25,095✔
158
}
25,095✔
159

160
////////////////////////////////////////////////////////////////////////////////
161
Task::status Task::textToStatus (const std::string& input)
29,958✔
162
{
163
       if (input[0] == 'p') return Task::pending;
29,958✔
164
  else if (input[0] == 'c') return Task::completed;
2,645✔
165
  else if (input[0] == 'd') return Task::deleted;
1,681✔
166
  else if (input[0] == 'r') return Task::recurring;
1,005✔
167
  // for compatibility, parse `w` as pending; Task::getStatus will
168
  // apply the virtual waiting status if appropriate
169
  else if (input[0] == 'w') return Task::pending;
2✔
170

171
  throw format ("The status '{1}' is not valid.", input);
2✔
172
}
173

174
////////////////////////////////////////////////////////////////////////////////
175
std::string Task::statusToText (Task::status s)
7,299✔
176
{
177
       if (s == Task::pending)   return "pending";
7,299✔
178
  else if (s == Task::recurring) return "recurring";
868✔
179
  else if (s == Task::waiting)   return "waiting";
601✔
180
  else if (s == Task::completed) return "completed";
599✔
181
  else if (s == Task::deleted)   return "deleted";
221✔
182

183
  return "pending";
×
184
}
185

186
////////////////////////////////////////////////////////////////////////////////
187
// Returns a proper handle to the task. Tasks should not be referenced by UUIDs
188
// as long as they have non-zero ID.
189
const std::string Task::identifier (bool shortened /* = false */) const
871✔
190
{
191
  if (id != 0)
871✔
192
    return format (id);
840✔
193
  else if (shortened)
31✔
194
    return get ("uuid").substr (0, 8);
48✔
195
  else
196
    return get ("uuid");
7✔
197
}
198

199
////////////////////////////////////////////////////////////////////////////////
200
void Task::setAsNow (const std::string& att)
6,198✔
201
{
202
  char now[22];
203
  snprintf (now, 22, "%lli", (long long int) time (nullptr));
6,198✔
204
  set (att, now);
6,198✔
205

206
  recalc_urgency = true;
6,198✔
207
}
6,198✔
208

209
////////////////////////////////////////////////////////////////////////////////
210
bool Task::has (const std::string& name) const
191,017✔
211
{
212
  if (data.find (name) != data.end ())
191,017✔
213
    return true;
61,565✔
214

215
  return false;
129,452✔
216
}
217

218
////////////////////////////////////////////////////////////////////////////////
219
std::vector <std::string> Task::all () const
28,148✔
220
{
221
  std::vector <std::string> all;
28,148✔
222
  for (const auto& i : data)
192,116✔
223
    all.push_back (i.first);
163,968✔
224

225
  return all;
28,148✔
226
}
227

228
////////////////////////////////////////////////////////////////////////////////
229
const std::string Task::get (const std::string& name) const
103,610✔
230
{
231
  auto i = data.find (name);
103,610✔
232
  if (i != data.end ())
103,610✔
233
    return i->second;
85,945✔
234

235
  return "";
17,665✔
236
}
237

238
////////////////////////////////////////////////////////////////////////////////
239
const std::string& Task::get_ref (const std::string& name) const
5,642✔
240
{
241
  auto i = data.find (name);
5,642✔
242
  if (i != data.end ())
5,642✔
243
    return i->second;
3,142✔
244

245
  return dummy;
2,500✔
246
}
247

248
////////////////////////////////////////////////////////////////////////////////
249
int Task::get_int (const std::string& name) const
1✔
250
{
251
  auto i = data.find (name);
1✔
252
  if (i != data.end ())
1✔
253
    return strtol (i->second.c_str (), nullptr, 10);
1✔
254

255
  return 0;
×
256
}
257

258
////////////////////////////////////////////////////////////////////////////////
259
unsigned long Task::get_ulong (const std::string& name) const
1✔
260
{
261
  auto i = data.find (name);
1✔
262
  if (i != data.end ())
1✔
263
    return strtoul (i->second.c_str (), nullptr, 10);
1✔
264

265
  return 0;
×
266
}
267

268
////////////////////////////////////////////////////////////////////////////////
269
float Task::get_float (const std::string& name) const
7✔
270
{
271
  auto i = data.find (name);
7✔
272
  if (i != data.end ())
7✔
273
    return strtof (i->second.c_str (), nullptr);
7✔
274

275
  return 0.0;
×
276
}
277

278
////////////////////////////////////////////////////////////////////////////////
279
time_t Task::get_date (const std::string& name) const
6,073✔
280
{
281
  auto i = data.find (name);
6,073✔
282
  if (i != data.end ())
6,073✔
283
    return (time_t) strtoul (i->second.c_str (), nullptr, 10);
6,026✔
284

285
  return 0;
47✔
286
}
287

288
////////////////////////////////////////////////////////////////////////////////
289
void Task::set (const std::string& name, const std::string& value)
19,303✔
290
{
291
  data[name] = value;
19,303✔
292

293
  if (isAnnotationAttr (name))
19,303✔
294
    ++annotation_count;
1✔
295

296
  recalc_urgency = true;
19,303✔
297
}
19,303✔
298

299
////////////////////////////////////////////////////////////////////////////////
300
void Task::set (const std::string& name, long long value)
460✔
301
{
302
  data[name] = format (value);
460✔
303

304
  recalc_urgency = true;
460✔
305
}
460✔
306

307
////////////////////////////////////////////////////////////////////////////////
308
void Task::remove (const std::string& name)
207✔
309
{
310
  if (data.erase (name))
207✔
311
    recalc_urgency = true;
68✔
312

313
  if (isAnnotationAttr (name))
207✔
314
    --annotation_count;
×
315
}
207✔
316

317
////////////////////////////////////////////////////////////////////////////////
318
Task::status Task::getStatus () const
31,065✔
319
{
320
  if (! has ("status"))
31,065✔
321
    return Task::pending;
4,002✔
322

323
  auto status = textToStatus (get ("status"));
27,067✔
324

325
  // Implement the "virtual" Task::waiting status, which is not stored on-disk
326
  // but is defined as a pending task with a `wait` attribute in the future.
327
  // This is workaround for 2.6.0, remove in 3.0.0.
328
  if (status == Task::pending && is_waiting ()) {
27,061✔
329
      return Task::waiting;
103✔
330
  }
331

332
  return status;
26,958✔
333
}
334

335
////////////////////////////////////////////////////////////////////////////////
336
void Task::setStatus (Task::status status)
5,401✔
337
{
338
  // the 'waiting' status is a virtual version of 'pending', so translate
339
  // that back to 'pending' here
340
  if (status == Task::waiting)
5,401✔
341
      status = Task::pending;
25✔
342

343
  set ("status", statusToText (status));
5,401✔
344

345
  recalc_urgency = true;
5,401✔
346
}
5,401✔
347

348
#ifdef PRODUCT_TASKWARRIOR
349
////////////////////////////////////////////////////////////////////////////////
350
// Determines status of a date attribute.
351
Task::dateState Task::getDateState (const std::string& name) const
129✔
352
{
353
  std::string value = get (name);
129✔
354
  if (value.length ())
129✔
355
  {
356
    Datetime reference (value);
129✔
357
    Datetime now;
129✔
358
    Datetime today ("today");
129✔
359

360
    if (reference < today)
129✔
361
      return dateBeforeToday;
115✔
362

363
    if (reference.sameDay (now))
105✔
364
    {
365
      if (reference < now)
24✔
366
        return dateEarlierToday;
23✔
367
      else
368
        return dateLaterToday;
1✔
369
    }
370

371
    int imminentperiod = Context::getContext ().config.getInteger ("due");
81✔
372
    if (imminentperiod == 0)
81✔
373
      return dateAfterToday;
4✔
374

375
    Datetime imminentDay = today + imminentperiod * 86400;
77✔
376
    if (reference < imminentDay)
77✔
377
      return dateAfterToday;
63✔
378
  }
379

380
  return dateNotDue;
14✔
381
}
129✔
382

383
////////////////////////////////////////////////////////////////////////////////
384
// An empty task is typically a "dummy", such as in DOM evaluation, which may or
385
// may not occur in the context of a task.
386
bool Task::is_empty () const
×
387
{
388
  return data.size () == 0;
×
389
}
390

391
////////////////////////////////////////////////////////////////////////////////
392
// Ready means pending, not blocked and either not scheduled or scheduled before
393
// now.
394
bool Task::is_ready () const
233✔
395
{
396
  return getStatus () == Task::pending &&
645✔
397
         ! is_blocked                  &&
401✔
398
         (! has ("scheduled")          ||
401✔
399
          Datetime ("now").operator> (get_date ("scheduled")));
470✔
400
}
401

402
////////////////////////////////////////////////////////////////////////////////
403
bool Task::is_due () const
146✔
404
{
405
  if (has ("due"))
146✔
406
  {
407
    Task::status status = getStatus ();
33✔
408

409
    if (status != Task::completed &&
33✔
410
        status != Task::deleted)
411
    {
412
      Task::dateState state = getDateState ("due");
33✔
413
      if (state == dateAfterToday   ||
33✔
414
          state == dateEarlierToday ||
9✔
415
          state == dateLaterToday)
416
        return true;
24✔
417
    }
418
  }
419

420
  return false;
122✔
421
}
422

423
////////////////////////////////////////////////////////////////////////////////
424
bool Task::is_dueyesterday () const
108✔
425
{
426
  if (has ("due"))
108✔
427
  {
428
    Task::status status = getStatus ();
22✔
429

430
    if (status != Task::completed &&
22✔
431
        status != Task::deleted)
432
    {
433
      if (Datetime ("yesterday").sameDay (get_date ("due")))
22✔
434
        return true;
3✔
435
    }
436
  }
437

438
  return false;
105✔
439
}
440

441
////////////////////////////////////////////////////////////////////////////////
442
bool Task::is_duetoday () const
255✔
443
{
444
  if (has ("due"))
255✔
445
  {
446
    Task::status status = getStatus ();
56✔
447

448
    if (status != Task::completed &&
56✔
449
        status != Task::deleted)
450
    {
451
      Task::dateState state = getDateState ("due");
56✔
452
      if (state == dateEarlierToday ||
56✔
453
          state == dateLaterToday)
454
        return true;
12✔
455
    }
456
  }
457

458
  return false;
243✔
459
}
460

461
////////////////////////////////////////////////////////////////////////////////
462
bool Task::is_duetomorrow () const
108✔
463
{
464
  if (has ("due"))
108✔
465
  {
466
    Task::status status = getStatus ();
22✔
467

468
    if (status != Task::completed &&
22✔
469
        status != Task::deleted)
470
    {
471
      if (Datetime ("tomorrow").sameDay (get_date ("due")))
22✔
472
        return true;
9✔
473
    }
474
  }
475

476
  return false;
99✔
477
}
478

479
////////////////////////////////////////////////////////////////////////////////
480
bool Task::is_dueweek () const
125✔
481
{
482
  if (has ("due"))
125✔
483
  {
484
    Task::status status = getStatus ();
25✔
485

486
    if (status != Task::completed &&
25✔
487
        status != Task::deleted)
488
    {
489
      Datetime due (get_date ("due"));
25✔
490
      if (due >= Datetime ("sow") &&
75✔
491
          due <= Datetime ("eow"))
50✔
492
        return true;
21✔
493
    }
494
  }
495

496
  return false;
104✔
497
}
498

499
////////////////////////////////////////////////////////////////////////////////
500
bool Task::is_duemonth () const
121✔
501
{
502
  if (has ("due"))
121✔
503
  {
504
    Task::status status = getStatus ();
25✔
505

506
    if (status != Task::completed &&
25✔
507
        status != Task::deleted)
508
    {
509
      Datetime due (get_date ("due"));
25✔
510
      if (due >= Datetime ("som") &&
75✔
511
          due <= Datetime ("eom"))
50✔
512
        return true;
21✔
513
    }
514
  }
515

516
  return false;
100✔
517
}
518

519
////////////////////////////////////////////////////////////////////////////////
520
bool Task::is_duequarter () const
101✔
521
{
522
  if (has ("due"))
101✔
523
  {
524
    Task::status status = getStatus ();
15✔
525

526
    if (status != Task::completed &&
15✔
527
        status != Task::deleted)
528
    {
529
      Datetime due (get_date ("due"));
15✔
530
      if (due >= Datetime ("soq") &&
45✔
531
          due <= Datetime ("eoq"))
30✔
532
        return true;
15✔
533
    }
534
  }
535

536
  return false;
86✔
537
}
538

539
////////////////////////////////////////////////////////////////////////////////
540
bool Task::is_dueyear () const
125✔
541
{
542
  if (has ("due"))
125✔
543
  {
544
    Task::status status = getStatus ();
25✔
545

546
    if (status != Task::completed &&
25✔
547
        status != Task::deleted)
548
    {
549
      Datetime due (get_date ("due"));
25✔
550
      if (due >= Datetime ("soy") &&
75✔
551
          due <= Datetime ("eoy"))
50✔
552
        return true;
25✔
553
    }
554
  }
555

556
  return false;
100✔
557
}
558

559
////////////////////////////////////////////////////////////////////////////////
560
bool Task::is_udaPresent () const
103✔
561
{
562
  for (auto& col : Context::getContext ().columns)
2,427✔
563
    if (col.second->is_uda () &&
2,462✔
564
        has (col.first))
124✔
565
      return true;
14✔
566

567
  return false;
89✔
568
}
569

570
////////////////////////////////////////////////////////////////////////////////
571
bool Task::is_orphanPresent () const
107✔
572
{
573
  for (auto& att : data)
783✔
574
    if (! isAnnotationAttr (att.first) &&
680✔
575
        ! isTagAttr (att.first) &&
663✔
576
        ! isDepAttr (att.first) &&
1,985✔
577
        Context::getContext ().columns.find (att.first) == Context::getContext ().columns.end ())
1,322✔
578
      return true;
4✔
579

580
  return false;
103✔
581
}
582

583
////////////////////////////////////////////////////////////////////////////////
584
bool Task::is_overdue () const
170✔
585
{
586
  if (has ("due"))
170✔
587
  {
588
    Task::status status = getStatus ();
47✔
589

590
    if (status != Task::completed &&
47✔
591
        status != Task::deleted &&
47✔
592
        status != Task::recurring)
593
    {
594
      Task::dateState state = getDateState ("due");
39✔
595
      if (state == dateEarlierToday ||
39✔
596
          state == dateBeforeToday)
597
        return true;
14✔
598
    }
599
  }
600

601
  return false;
156✔
602
}
603
#endif
604

605
////////////////////////////////////////////////////////////////////////////////
606
// Task is considered waiting if it's pending and the wait attribute is set as
607
// future datetime value.
608
// While this is not consistent with other attribute-based virtual tags, such
609
// as +BLOCKED, it is more backwards compatible with how +WAITING virtual tag
610
// behaved in the past, when waiting had a dedicated status value.
611
bool Task::is_waiting () const
27,183✔
612
{
613
  if (has ("wait") && get ("status") == "pending")
27,183✔
614
  {
615
    Datetime now;
198✔
616
    Datetime wait (get_date ("wait"));
198✔
617
    if (wait > now)
198✔
618
      return true;
115✔
619
  }
620

621
  return false;
27,068✔
622
}
623

624
////////////////////////////////////////////////////////////////////////////////
625
// Try a JSON parse.
626
void Task::parse (const std::string& input)
15✔
627
{
628
  parseJSON (input);
15✔
629

630
  // for compatibility, include all tags in `tags` as `tag_..` attributes
631
  if (data.find ("tags") != data.end ()) {
14✔
632
    for (auto& tag : split(data["tags"], ',')) {
8✔
633
      data[tag2Attr(tag)] = "x";
5✔
634
    }
3✔
635
  }
636
  // ..and similarly, update `tags` to match the `tag_..` attributes
637
  fixTagsAttribute();
14✔
638

639
  // same for `depends` / `dep_..`
640
  if (data.find ("depends") != data.end ()) {
14✔
641
    for (auto& dep : split(data["depends"], ',')) {
11✔
642
      data[dep2Attr(dep)] = "x";
7✔
643
    }
4✔
644
  }
645
  fixDependsAttribute();
14✔
646

647
  recalc_urgency = true;
14✔
648
}
14✔
649

650
////////////////////////////////////////////////////////////////////////////////
651
// Note that all fields undergo encode/decode.
652
void Task::parseJSON (const std::string& line)
15✔
653
{
654
  // Parse the whole thing.
655
  json::value* root = json::parse (line);
15✔
656
  if (root &&
28✔
657
      root->type () == json::j_object)
14✔
658
    parseJSON ((json::object*) root);
14✔
659

660
  delete root;
14✔
661
}
14✔
662

663
////////////////////////////////////////////////////////////////////////////////
664
void Task::parseJSON (const json::object* root_obj)
1,613✔
665
{
666
  // For each object element...
667
  for (auto& i : root_obj->_data)
4,008✔
668
  {
669
    // If the attribute is a recognized column.
670
    std::string type = Task::attributes[i.first];
2,397✔
671
    if (type != "")
2,397✔
672
    {
673
      // Any specified id is ignored.
674
      if (i.first == "id")
2,372✔
675
        ;
676

677
      // Urgency, if present, is ignored.
678
      else if (i.first == "urgency")
2,367✔
679
        ;
680

681
      // TW-1274 Standardization.
682
      else if (i.first == "modification")
2,362✔
683
      {
684
        auto text = i.second->dump ();
×
685
        Lexer::dequote (text);
×
686
        Datetime d (text);
×
687
        set ("modified", d.toEpochString ());
×
688
      }
689

690
      // Dates are converted from ISO to epoch.
691
      else if (type == "date")
2,362✔
692
      {
693
        auto text = i.second->dump ();
330✔
694
        Lexer::dequote (text);
330✔
695
        Datetime d (text);
330✔
696
        set (i.first, text == "" ? "" : d.toEpochString ());
330✔
697
      }
330✔
698

699
      // Tags are an array of JSON strings.
700
      else if (i.first == "tags" && i.second->type() == json::j_array)
2,032✔
701
      {
702
        auto tags = (json::array*)i.second;
3✔
703
        for (auto& t : tags->_data)
8✔
704
        {
705
          auto tag = (json::string*)t;
5✔
706
          addTag (tag->_data);
5✔
707
        }
708
      }
709

710
      // Dependencies can be exported as an array of strings.
711
      // 2016-02-21: This will be the only option in future releases.
712
      //             See other 2016-02-21 comments for details.
713
      else if (i.first == "depends" && i.second->type() == json::j_array)
2,029✔
714
      {
715
        auto deps = (json::array*)i.second;
3✔
716
        for (auto& t : deps->_data)
8✔
717
        {
718
          auto dep = (json::string*)t;
5✔
719
          addDependency (dep->_data);
5✔
720
        }
721
      }
722

723
      // Dependencies can be exported as a single comma-separated string.
724
      // 2016-02-21: Deprecated - see other 2016-02-21 comments for details.
725
      else if (i.first == "depends" && i.second->type() == json::j_string)
2,026✔
726
      {
727
        auto deps = (json::string*)i.second;
3✔
728

729
        // Fix for issue#2689: taskserver sometimes encodes the depends
730
        // property as a string of the format `[\"uuid\",\"uuid\"]`
731
        // The string includes the backslash-escaped `"` characters, making
732
        // it invalid JSON.  Since we know the characters we're looking for,
733
        // we'll just filter out everything else.
734
        std::string deps_str = deps->_data;
3✔
735
        if (deps_str.front () == '[' && deps_str.back () == ']') {
3✔
736
          std::string filtered;
1✔
737
                  for (auto &c: deps_str) {
84✔
738
                        if ((c >= '0' && c <= '9') ||
83✔
739
                                (c >= 'a' && c <= 'f') ||
46✔
740
                                c == ',' || c == '-') {
19✔
741
                          filtered.push_back(c);
73✔
742
                        }
743
                  }
744
                  deps_str = filtered;
1✔
745
        }
1✔
746
        auto uuids = split (deps_str, ',');
3✔
747

748
        for (const auto& uuid : uuids)
9✔
749
          addDependency (uuid);
6✔
750
      }
3✔
751

752
      // Strings are decoded.
753
      else if (type == "string")
2,023✔
754
      {
755
        auto text = i.second->dump ();
2,023✔
756
        Lexer::dequote (text);
2,023✔
757
        set (i.first, json::decode (text));
2,023✔
758
      }
2,023✔
759

760
      // Other types are simply added.
761
      else
762
      {
763
        auto text = i.second->dump ();
×
764
        Lexer::dequote (text);
×
765
        set (i.first, text);
×
766
      }
767
    }
768

769
    // UDA orphans and annotations do not have columns.
770
    else
771
    {
772
      // Annotations are an array of JSON objects with 'entry' and
773
      // 'description' values and must be converted.
774
      if (i.first == "annotations")
25✔
775
      {
776
        std::map <std::string, std::string> annos;
2✔
777

778
        // Fail if 'annotations' is not an array
779
        if (i.second->type() != json::j_array) {
2✔
780
            throw format ("Annotations is malformed: {1}", i.second->dump ());
2✔
781
        }
782

783
        auto atts = (json::array*)i.second;
×
784
        for (auto& annotations : atts->_data)
×
785
        {
786
          auto annotation = (json::object*)annotations;
×
787

788
          // Extract description. Fail if not present.
789
          auto what = (json::string*)annotation->_data["description"];
×
790
          if (! what) {
×
791
            annotation->_data.erase ("description");  // Erase NULL description inserted by failed lookup above
×
792
            throw format ("Annotation is missing a description: {1}", annotation->dump ());
×
793
          }
794

795
          // Extract 64-bit annotation entry value
796
          // Time travelers from 2038, we have your back.
797
          long long ann_timestamp;
798

799
          // Extract entry. Use current time if not present.
800
          auto when = (json::string*)annotation->_data["entry"];
×
801
          if (when)
×
802
            ann_timestamp = (long long) (Datetime (when->_data).toEpoch ());
×
803
          else {
804
            annotation->_data.erase ("entry");  // Erase NULL entry inserted by failed lookup above
×
805
            ann_timestamp = (long long) (Datetime ().toEpoch ());
×
806
          }
807

808
          std::stringstream name;
×
809
          name << "annotation_" << ann_timestamp;
×
810

811
          // Increment the entry timestamp in case of a conflict. Same
812
          // behaviour as CmdAnnotate.
813
          while (annos.find(name.str ()) != annos.end ())
×
814
          {
815
              name.str ("");  // Clear
×
816
              ann_timestamp++;
×
817
              name << "annotation_" << ann_timestamp;
×
818
          }
819

820
          annos.emplace (name.str (), json::decode (what->_data));
×
821
        }
822

823
        setAnnotations (annos);
×
824
      }
2✔
825

826
      // UDA Orphan - must be preserved.
827
      else
828
      {
829
#ifdef PRODUCT_TASKWARRIOR
830
        std::stringstream message;
23✔
831
        message << "Task::parseJSON found orphan '"
832
                << i.first
23✔
833
                << "' with value '"
23✔
834
                << i.second
23✔
835
                << "' --> preserved\n";
23✔
836
        Context::getContext ().debug (message.str ());
23✔
837
#endif
838
        auto text = i.second->dump ();
23✔
839
        Lexer::dequote (text);
23✔
840
        set (i.first, json::decode (text));
23✔
841
      }
23✔
842
    }
843
  }
2,397✔
844
}
1,611✔
845

846
////////////////////////////////////////////////////////////////////////////////
847
// Note that all fields undergo encode/decode.
848
void Task::parseTC (const tc::Task& task)
25,095✔
849
{
850
  data = task.get_taskmap ();
25,095✔
851

852
  // count annotations
853
  annotation_count = 0;
25,095✔
854
  for (auto i : data)
146,733✔
855
  {
856
    if (isAnnotationAttr (i.first))
121,638✔
857
    {
858
      ++annotation_count;
1,009✔
859
    }
860
  }
121,638✔
861

862
  data["uuid"] = task.get_uuid ();
25,095✔
863
  id = Context::getContext ().tdb2.id (data["uuid"]);
25,095✔
864

865
  is_blocking = task.is_blocking();
25,095✔
866
  is_blocked = task.is_blocked();
25,095✔
867
}
25,095✔
868

869
////////////////////////////////////////////////////////////////////////////////
870
// No legacy formats are currently supported as of 2.4.0.
871
void Task::parseLegacy (const std::string& line)
×
872
{
873
  switch (determineVersion (line))
×
874
  {
875
  // File format version 1, from 2006-11-27 - 2007-12-31, v0.x+ - v0.9.3
876
  case 1: throw std::string ("Taskwarrior no longer supports file format 1, originally used between 27 November 2006 and 31 December 2007.");
×
877

878
  // File format version 2, from 2008-1-1 - 2009-3-23, v0.9.3 - v1.5.0
879
  case 2: throw std::string ("Taskwarrior no longer supports file format 2, originally used between 1 January 2008 and 12 April 2009.");
×
880

881
  // File format version 3, from 2009-3-23 - 2009-05-16, v1.6.0 - v1.7.1
882
  case 3: throw std::string ("Taskwarrior no longer supports file format 3, originally used between 23 March 2009 and 16 May 2009.");
×
883

884
  // File format version 4, from 2009-05-16 - today, v1.7.1+
885
  case 4:
×
886
    break;
×
887

888
  default:
×
889
#ifdef PRODUCT_TASKWARRIOR
890
    std::stringstream message;
×
891
    message << "Invalid fileformat at line '"
892
            << line
893
            << '\'';
×
894
    Context::getContext ().debug (message.str ());
×
895
#endif
896
    throw std::string ("Unrecognized Taskwarrior file format or blank line in data.");
×
897
    break;
898
  }
899

900
  recalc_urgency = true;
×
901
}
902

903
////////////////////////////////////////////////////////////////////////////////
904
std::string Task::composeJSON (bool decorate /*= false*/) const
155✔
905
{
906
  std::stringstream out;
155✔
907
  out << '{';
155✔
908

909
  // ID inclusion is optional, but not a good idea, because it remains correct
910
  // only until the next gc.
911
  if (decorate)
155✔
912
    out << "\"id\":" << id << ',';
122✔
913

914
  // First the non-annotations.
915
  int attributes_written = 0;
155✔
916
  for (auto& i : data)
1,206✔
917
  {
918
    // Annotations are not written out here.
919
    if (! i.first.compare (0, 11, "annotation_", 11))
1,051✔
920
      continue;
108✔
921

922
    // Tags and dependencies are handled below
923
    if (i.first == "tags" || isTagAttr (i.first))
1,051✔
924
      continue;
85✔
925
    if (i.first == "depends" || isDepAttr (i.first))
966✔
926
      continue;
23✔
927

928
    // If value is an empty string, do not ever output it
929
    if (i.second == "")
943✔
930
        continue;
×
931

932
    if (attributes_written)
943✔
933
      out << ',';
788✔
934

935
    std::string type = Task::attributes[i.first];
943✔
936
    if (type == "")
943✔
937
      type = "string";
9✔
938

939
    // Date fields are written as ISO 8601.
940
    if (type == "date")
943✔
941
    {
942
      Datetime d (i.second);
372✔
943
      out << '"'
944
          << (i.first == "modification" ? "modified" : i.first)
744✔
945
          << "\":\""
946
          // Date was deleted, do not export parsed empty string
947
          << (i.second == "" ? "" : d.toISO ())
744✔
948
          << '"';
1,116✔
949

950
      ++attributes_written;
372✔
951
    }
952

953
/*
954
    else if (type == "duration")
955
    {
956
      // TODO Emit Datetime
957
    }
958
*/
959
    else if (type == "numeric")
571✔
960
    {
961
      out << '"'
962
          << i.first
14✔
963
          << "\":"
964
          << i.second;
14✔
965

966
      ++attributes_written;
14✔
967
    }
968

969
    // Everything else is a quoted value.
970
    else
971
    {
972
      out << '"'
973
          << i.first
557✔
974
          << "\":\""
975
          << (type == "string" ? json::encode (i.second) : i.second)
1,114✔
976
          << '"';
1,114✔
977

978
      ++attributes_written;
557✔
979
    }
980
  }
943✔
981

982
  // Now the annotations, if any.
983
  if (annotation_count)
155✔
984
  {
985
    out << ','
986
        << "\"annotations\":[";
×
987

988
    int annotations_written = 0;
×
989
    for (auto& i : data)
×
990
    {
991
      if (! i.first.compare (0, 11, "annotation_", 11))
×
992
      {
993
        if (annotations_written)
×
994
          out << ',';
×
995

996
        Datetime d (i.first.substr (11));
×
997
        out << R"({"entry":")"
998
            << d.toISO ()
×
999
            << R"(","description":")"
1000
            << json::encode (i.second)
×
1001
            << "\"}";
×
1002

1003
        ++annotations_written;
×
1004
      }
1005
    }
1006

1007
    out << ']';
×
1008
  }
1009

1010
  auto tags = getTags();
155✔
1011
  if (tags.size() > 0)
155✔
1012
  {
1013
    out << ','
1014
        << "\"tags\":[";
36✔
1015

1016
    int count = 0;
36✔
1017
    for (const auto& tag : tags)
85✔
1018
    {
1019
      if (count++)
49✔
1020
        out << ',';
13✔
1021

1022
      out << '"' << tag << '"';
49✔
1023
    }
1024

1025
    out << ']';
36✔
1026
    ++attributes_written;
36✔
1027
  }
1028

1029
  auto depends = getDependencyUUIDs ();
155✔
1030
  if (depends.size() > 0)
155✔
1031
  {
1032
    out << ','
1033
        << "\"depends\":[";
10✔
1034

1035
    int count = 0;
10✔
1036
    for (const auto& dep : depends)
23✔
1037
    {
1038
      if (count++)
13✔
1039
        out << ',';
3✔
1040

1041
      out << '"' << dep << '"';
13✔
1042
    }
1043

1044
    out << ']';
10✔
1045
    ++attributes_written;
10✔
1046
  }
1047

1048
#ifdef PRODUCT_TASKWARRIOR
1049
  // Include urgency.
1050
  if (decorate)
155✔
1051
    out << ','
1052
        << "\"urgency\":"
122✔
1053
        << urgency_c ();
122✔
1054
#endif
1055

1056
  out << '}';
155✔
1057
  return out.str ();
310✔
1058
}
155✔
1059

1060
////////////////////////////////////////////////////////////////////////////////
1061
int Task::getAnnotationCount () const
2✔
1062
{
1063
  int count = 0;
2✔
1064
  for (auto& ann : data)
19✔
1065
    if (! ann.first.compare (0, 11, "annotation_", 11))
17✔
1066
      ++count;
1✔
1067

1068
  return count;
2✔
1069
}
1070

1071
////////////////////////////////////////////////////////////////////////////////
1072
bool Task::hasAnnotations () const
121✔
1073
{
1074
  return annotation_count ? true : false;
121✔
1075
}
1076

1077
////////////////////////////////////////////////////////////////////////////////
1078
// The timestamp is part of the name:
1079
//    annotation_1234567890:"..."
1080
//
1081
// Note that the time is incremented (one second) in order to find a unique
1082
// timestamp.
1083
void Task::addAnnotation (const std::string& description)
90✔
1084
{
1085
  time_t now = time (nullptr);
90✔
1086
  std::string key;
90✔
1087

1088
  do
1089
  {
1090
    key = "annotation_" + format ((long long int) now);
168✔
1091
    ++now;
168✔
1092
  }
1093
  while (has (key));
168✔
1094

1095
  data[key] = json::decode (description);
90✔
1096
  ++annotation_count;
90✔
1097
  recalc_urgency = true;
90✔
1098
}
90✔
1099

1100
////////////////////////////////////////////////////////////////////////////////
1101
void Task::removeAnnotations ()
41✔
1102
{
1103
  // Erase old annotations.
1104
  auto i = data.begin ();
41✔
1105
  while (i != data.end ())
293✔
1106
  {
1107
    if (! i->first.compare (0, 11, "annotation_", 11))
252✔
1108
    {
1109
      --annotation_count;
23✔
1110
      data.erase (i++);
23✔
1111
    }
1112
    else
1113
      i++;
229✔
1114
  }
1115

1116
  recalc_urgency = true;
41✔
1117
}
41✔
1118

1119
////////////////////////////////////////////////////////////////////////////////
1120
std::map <std::string, std::string> Task::getAnnotations () const
796✔
1121
{
1122
  std::map <std::string, std::string> a;
796✔
1123
  for (auto& ann : data)
6,093✔
1124
    if (! ann.first.compare (0, 11, "annotation_", 11))
5,297✔
1125
      a.insert (ann);
223✔
1126

1127
  return a;
796✔
1128
}
1129

1130
////////////////////////////////////////////////////////////////////////////////
1131
void Task::setAnnotations (const std::map <std::string, std::string>& annotations)
41✔
1132
{
1133
  // Erase old annotations.
1134
  removeAnnotations ();
41✔
1135

1136
  for (auto& anno : annotations)
58✔
1137
    data.insert (anno);
17✔
1138

1139
  annotation_count = annotations.size ();
41✔
1140
  recalc_urgency = true;
41✔
1141
}
41✔
1142

1143
#ifdef PRODUCT_TASKWARRIOR
1144
////////////////////////////////////////////////////////////////////////////////
1145
void Task::addDependency (int depid)
52✔
1146
{
1147
  // Check that id is resolvable.
1148
  std::string uuid = Context::getContext ().tdb2.uuid (depid);
52✔
1149
  if (uuid == "")
52✔
1150
    throw format ("Could not create a dependency on task {1} - not found.", depid);
1✔
1151

1152
  // the addDependency(&std::string) overload will check this, too, but here we
1153
  // can give an more natural error message containing the id the user
1154
  // provided.
1155
  if (hasDependency (uuid))
51✔
1156
  {
1157
    Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", id, depid));
2✔
1158
    return;
2✔
1159
  }
1160

1161
  addDependency(uuid);
49✔
1162
}
52✔
1163
#endif
1164

1165
////////////////////////////////////////////////////////////////////////////////
1166
void Task::addDependency (const std::string& uuid)
66✔
1167
{
1168
  if (uuid == get ("uuid"))
66✔
1169
    throw std::string ("A task cannot be dependent on itself.");
1✔
1170

1171
  if (hasDependency (uuid))
65✔
1172
  {
1173
#ifdef PRODUCT_TASKWARRIOR
1174
    Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", get ("uuid"), uuid));
×
1175
#endif
1176
    return;
×
1177
  }
1178

1179
  // Store the dependency.
1180
  set (dep2Attr (uuid), "x");
65✔
1181

1182
  // Prevent circular dependencies.
1183
#ifdef PRODUCT_TASKWARRIOR
1184
  if (dependencyIsCircular (*this))
65✔
1185
    throw std::string ("Circular dependency detected and disallowed.");
2✔
1186
#endif
1187

1188
  recalc_urgency = true;
63✔
1189
  fixDependsAttribute();
63✔
1190
}
1191

1192
#ifdef PRODUCT_TASKWARRIOR
1193
////////////////////////////////////////////////////////////////////////////////
1194
void Task::removeDependency (int id)
4✔
1195
{
1196
  std::string uuid = Context::getContext ().tdb2.uuid (id);
4✔
1197

1198
  // The removeDependency(std::string&) method will check this too, but here we
1199
  // can give a more natural error message containing the id provided by the user
1200
  if (uuid == "" || !has (dep2Attr (uuid)))
4✔
1201
    throw format ("Could not delete a dependency on task {1} - not found.", id);
1✔
1202
  removeDependency (uuid);
3✔
1203
}
4✔
1204

1205
////////////////////////////////////////////////////////////////////////////////
1206
void Task::removeDependency (const std::string& uuid)
7✔
1207
{
1208
  auto depattr = dep2Attr (uuid);
7✔
1209
  if (has (depattr))
7✔
1210
    remove (depattr);
7✔
1211
  else
1212
    throw format ("Could not delete a dependency on task {1} - not found.", uuid);
×
1213

1214
  recalc_urgency = true;
7✔
1215
  fixDependsAttribute();
7✔
1216
}
7✔
1217

1218
////////////////////////////////////////////////////////////////////////////////
1219
bool Task::hasDependency (const std::string& uuid) const
1,166✔
1220
{
1221
  auto depattr = dep2Attr (uuid);
1,166✔
1222
  return has (depattr);
2,332✔
1223
}
1,166✔
1224

1225
////////////////////////////////////////////////////////////////////////////////
1226
std::vector <int> Task::getDependencyIDs () const
×
1227
{
1228
  std::vector <int> ids;
×
1229
  for (auto& attr : all ()) {
×
1230
    if (!isDepAttr (attr))
×
1231
      continue;
×
1232
    auto dep = attr2Dep (attr);
×
1233
    ids.push_back (Context::getContext ().tdb2.id (dep));
×
1234
  }
1235

1236
  return ids;
×
1237
}
1238

1239
////////////////////////////////////////////////////////////////////////////////
1240
std::vector <std::string> Task::getDependencyUUIDs () const
24,650✔
1241
{
1242
  std::vector <std::string> uuids;
24,650✔
1243
  for (auto& attr : all ()) {
167,863✔
1244
    if (!isDepAttr (attr))
143,213✔
1245
      continue;
142,280✔
1246
    auto dep = attr2Dep (attr);
933✔
1247
    uuids.push_back (dep);
933✔
1248
  }
25,583✔
1249

1250
  return uuids;
24,650✔
1251
}
1252

1253
////////////////////////////////////////////////////////////////////////////////
1254
std::vector <Task> Task::getDependencyTasks () const
1,438✔
1255
{
1256
  auto uuids = getDependencyUUIDs ();
1,438✔
1257

1258
  // NOTE: this may seem inefficient, but note that `TDB2::get` performs a
1259
  // linear search on each invocation, so scanning *once* is quite a bit more
1260
  // efficient.
1261
  std::vector <Task> blocking;
1,438✔
1262
  if (uuids.size() > 0)
1,438✔
1263
    for (auto& it : Context::getContext ().tdb2.pending_tasks ())
470✔
1264
      if (it.getStatus () != Task::completed &&
1,601✔
1265
          it.getStatus () != Task::deleted   &&
790✔
1266
          std::find (uuids.begin (), uuids.end (), it.get ("uuid")) != uuids.end ())
790✔
1267
        blocking.push_back (it);
126✔
1268

1269
  return blocking;
2,876✔
1270
}
1,438✔
1271

1272
////////////////////////////////////////////////////////////////////////////////
1273
std::vector <Task> Task::getBlockedTasks () const
360✔
1274
{
1275
  auto uuid = get ("uuid");
720✔
1276

1277
  std::vector <Task> blocked;
360✔
1278
  for (auto& it : Context::getContext ().tdb2.pending_tasks ())
1,693✔
1279
    if (it.getStatus () != Task::completed &&
1,333✔
1280
        it.getStatus () != Task::deleted   &&
2,383✔
1281
        it.hasDependency (uuid))
1,050✔
1282
      blocked.push_back (it);
377✔
1283

1284
  return blocked;
720✔
1285
}
360✔
1286
#endif
1287

1288
////////////////////////////////////////////////////////////////////////////////
1289
int Task::getTagCount () const
1,121✔
1290
{
1291
  auto count = 0;
1,121✔
1292
  for (auto& attr : data) {
8,661✔
1293
    if (isTagAttr (attr.first)) {
7,540✔
1294
      count++;
226✔
1295
    }
1296
  }
1297
  return count;
1,121✔
1298
}
1299

1300
////////////////////////////////////////////////////////////////////////////////
1301
//
1302
//              OVERDUE YESTERDAY DUE TODAY TOMORROW WEEK MONTH YEAR
1303
// due:-1week      Y       -       -    -       -      ?    ?     ?
1304
// due:-1day       Y       Y       -    -       -      ?    ?     ?
1305
// due:today       Y       -       Y    Y       -      ?    ?     ?
1306
// due:tomorrow    -       -       Y    -       Y      ?    ?     ?
1307
// due:3days       -       -       Y    -       -      ?    ?     ?
1308
// due:1month      -       -       -    -       -      -    -     ?
1309
// due:1year       -       -       -    -       -      -    -     -
1310
//
1311
bool Task::hasTag (const std::string& tag) const
7,009✔
1312
{
1313
  // Synthetic tags - dynamically generated, but do not occupy storage space.
1314
  // Note: This list must match that in CmdInfo::execute.
1315
  // Note: This list must match that in ::feedback_reserved_tags.
1316
  if (isupper (tag[0]))
7,009✔
1317
  {
1318
    // NOTE: This list should be kept synchronized with:
1319
    // * the list in CmdTags.cpp for the _tags command.
1320
    // * the list in CmdInfo.cpp for the info command.
1321
    if (tag == "BLOCKED")   return is_blocked;
5,548✔
1322
    if (tag == "UNBLOCKED") return !is_blocked;
5,416✔
1323
    if (tag == "BLOCKING")  return is_blocking;
5,292✔
1324
#ifdef PRODUCT_TASKWARRIOR
1325
    if (tag == "READY")     return is_ready ();
5,160✔
1326
    if (tag == "DUE")       return is_due ();
4,927✔
1327
    if (tag == "DUETODAY")  return is_duetoday ();                     // 2016-03-29: Deprecated in 2.6.0
4,819✔
1328
    if (tag == "TODAY")     return is_duetoday ();
4,711✔
1329
    if (tag == "YESTERDAY") return is_dueyesterday ();
4,602✔
1330
    if (tag == "TOMORROW")  return is_duetomorrow ();
4,494✔
1331
    if (tag == "OVERDUE")   return is_overdue ();
4,386✔
1332
    if (tag == "WEEK")      return is_dueweek ();
4,254✔
1333
    if (tag == "MONTH")     return is_duemonth ();
4,129✔
1334
    if (tag == "QUARTER")   return is_duequarter ();
4,008✔
1335
    if (tag == "YEAR")      return is_dueyear ();
3,907✔
1336
#endif
1337
    if (tag == "ACTIVE")    return has ("start");
3,782✔
1338
    if (tag == "SCHEDULED") return has ("scheduled");
3,655✔
1339
    if (tag == "CHILD")     return has ("parent") || has ("template"); // 2017-01-07: Deprecated in 2.6.0
3,532✔
1340
    if (tag == "INSTANCE")  return has ("template") || has ("parent");
3,428✔
1341
    if (tag == "UNTIL")     return has ("until");
3,311✔
1342
    if (tag == "ANNOTATED") return hasAnnotations ();
3,186✔
1343
    if (tag == "TAGGED")    return getTagCount() > 0;
3,065✔
1344
    if (tag == "PARENT")    return has ("mask") || has ("last");       // 2017-01-07: Deprecated in 2.6.0
2,944✔
1345
    if (tag == "TEMPLATE")  return has ("last") || has ("mask");
2,840✔
1346
    if (tag == "WAITING")   return is_waiting ();
2,723✔
1347
    if (tag == "PENDING")   return getStatus () == Task::pending;
1,128✔
1348
    if (tag == "COMPLETED") return getStatus () == Task::completed;
987✔
1349
    if (tag == "DELETED")   return getStatus () == Task::deleted;
844✔
1350
#ifdef PRODUCT_TASKWARRIOR
1351
    if (tag == "UDA")       return is_udaPresent ();
721✔
1352
    if (tag == "ORPHAN")    return is_orphanPresent ();
618✔
1353
    if (tag == "LATEST")    return id == Context::getContext ().tdb2.latest_id ();
511✔
1354
#endif
1355
    if (tag == "PROJECT")   return has ("project");
345✔
1356
    if (tag == "PRIORITY")  return has ("priority");
244✔
1357
  }
1358

1359
  // Concrete tags.
1360
  if (has (tag2Attr (tag)))
1,604✔
1361
    return true;
122✔
1362

1363
  return false;
1,482✔
1364
}
1365

1366
////////////////////////////////////////////////////////////////////////////////
1367
void Task::addTag (const std::string& tag)
171✔
1368
{
1369
  auto attr = tag2Attr (tag);
171✔
1370
  if (!has (attr)) {
171✔
1371
    set (attr, "x");
167✔
1372
    recalc_urgency = true;
167✔
1373
    fixTagsAttribute();
167✔
1374
  }
1375
}
171✔
1376

1377
////////////////////////////////////////////////////////////////////////////////
1378
void Task::setTags (const std::vector <std::string>& tags)
8✔
1379
{
1380
  auto existing = getTags();
8✔
1381

1382
  // edit in-place, determining which should be
1383
  // added and which should be removed
1384
  std::vector <std::string> toAdd;
8✔
1385
  std::vector <std::string> toRemove;
8✔
1386

1387
  for (auto& tag : tags) {
20✔
1388
    if (std::find (existing.begin (), existing.end (), tag) == existing.end ())
12✔
1389
      toAdd.push_back(tag);
10✔
1390
  }
1391

1392
  for (auto& tag : getTags ()) {
11✔
1393
    if (std::find (tags.begin (), tags.end (), tag) == tags.end ()) {
3✔
1394
      toRemove.push_back (tag);
1✔
1395
    }
1396
  }
8✔
1397

1398
  for (auto& tag : toRemove) {
9✔
1399
    removeTag (tag);
1✔
1400
  }
1401
  for (auto& tag : toAdd) {
18✔
1402
    addTag (tag);
10✔
1403
  }
1404

1405
  // (note: addTag / removeTag took care of recalculating urgency)
1406
}
8✔
1407

1408
////////////////////////////////////////////////////////////////////////////////
1409
std::vector <std::string> Task::getTags () const
769✔
1410
{
1411
  std::vector <std::string> tags;
769✔
1412

1413
  for (auto& attr : data) {
5,423✔
1414
    if (!isTagAttr (attr.first)) {
4,654✔
1415
      continue;
4,218✔
1416
    }
1417
    auto tag = attr2Tag (attr.first);
436✔
1418
    tags.push_back (tag);
436✔
1419
  }
436✔
1420

1421
  return tags;
769✔
1422
}
1423

1424
////////////////////////////////////////////////////////////////////////////////
1425
void Task::removeTag (const std::string& tag)
12✔
1426
{
1427
  auto attr = tag2Attr (tag);
12✔
1428
  if (has (attr)) {
12✔
1429
    data.erase (attr);
11✔
1430
    recalc_urgency = true;
11✔
1431
    fixTagsAttribute();
11✔
1432
  }
1433
}
12✔
1434

1435
////////////////////////////////////////////////////////////////////////////////
1436
void Task::fixTagsAttribute ()
192✔
1437
{
1438
  // Fix up the old `tags` attribute to match the `tag_..` attributes (or
1439
  // remove it if there are no tags)
1440
  auto tags = getTags ();
192✔
1441
  if (tags.size () > 0) {
192✔
1442
    set ("tags", join (",", tags));
177✔
1443
  } else {
1444
    remove ("tags");
15✔
1445
  }
1446
}
192✔
1447

1448
////////////////////////////////////////////////////////////////////////////////
1449
bool Task::isTagAttr(const std::string& attr)
14,766✔
1450
{
1451
  return attr.compare(0, 4, "tag_") == 0;
14,766✔
1452
}
1453

1454
////////////////////////////////////////////////////////////////////////////////
1455
const std::string Task::tag2Attr (const std::string& tag) const
1,792✔
1456
{
1457
  std::stringstream tag_attr;
1,792✔
1458
  tag_attr << "tag_" << tag;
1,792✔
1459
  return tag_attr.str();
3,584✔
1460
}
1,792✔
1461

1462
////////////////////////////////////////////////////////////////////////////////
1463
const std::string Task::attr2Tag (const std::string& attr) const
479✔
1464
{
1465
  assert (isTagAttr (attr));
479✔
1466
  return attr.substr(4);
479✔
1467
}
1468

1469
////////////////////////////////////////////////////////////////////////////////
1470
void Task::fixDependsAttribute ()
84✔
1471
{
1472
  // Fix up the old `depends` attribute to match the `dep_..` attributes (or
1473
  // remove it if there are no deps)
1474
  auto deps = getDependencyUUIDs ();
84✔
1475
  if (deps.size () > 0) {
84✔
1476
    set ("depends", join (",", deps));
70✔
1477
  } else {
1478
    remove ("depends");
14✔
1479
  }
1480
}
84✔
1481

1482
////////////////////////////////////////////////////////////////////////////////
1483
bool Task::isDepAttr(const std::string& attr)
146,154✔
1484
{
1485
  return attr.compare(0, 4, "dep_") == 0;
146,154✔
1486
}
1487

1488
////////////////////////////////////////////////////////////////////////////////
1489
const std::string Task::dep2Attr (const std::string& tag) const
1,249✔
1490
{
1491
  std::stringstream tag_attr;
1,249✔
1492
  tag_attr << "dep_" << tag;
1,249✔
1493
  return tag_attr.str();
2,498✔
1494
}
1,249✔
1495

1496
////////////////////////////////////////////////////////////////////////////////
1497
const std::string Task::attr2Dep (const std::string& attr) const
972✔
1498
{
1499
  assert (isDepAttr (attr));
972✔
1500
  return attr.substr(4);
972✔
1501
}
1502

1503
////////////////////////////////////////////////////////////////////////////////
1504
bool Task::isAnnotationAttr(const std::string& attr)
142,342✔
1505
{
1506
  return attr.compare(0, 11, "annotation_") == 0;
142,342✔
1507
}
1508

1509
#ifdef PRODUCT_TASKWARRIOR
1510
////////////////////////////////////////////////////////////////////////////////
1511
// A UDA Orphan is an attribute that is not represented in context.columns.
1512
std::vector <std::string> Task::getUDAOrphans () const
9✔
1513
{
1514
  std::vector <std::string> orphans;
9✔
1515
  for (auto& it : data)
84✔
1516
    if (Context::getContext ().columns.find (it.first) == Context::getContext ().columns.end ())
75✔
1517
      if (not (isAnnotationAttr (it.first) || isTagAttr (it.first) || isDepAttr (it.first)))
12✔
1518
        orphans.push_back (it.first);
3✔
1519

1520
  return orphans;
9✔
1521
}
1522

1523
////////////////////////////////////////////////////////////////////////////////
1524
void Task::substitute (
35✔
1525
  const std::string& from,
1526
  const std::string& to,
1527
  const std::string& flags)
1528
{
1529
  bool global = (flags.find ('g') != std::string::npos ? true : false);
35✔
1530

1531
  // Get the data to modify.
1532
  std::string description = get ("description");
70✔
1533
  auto annotations = getAnnotations ();
35✔
1534

1535
  // Count the changes, so we know whether to proceed to annotations, after
1536
  // modifying description.
1537
  int changes = 0;
35✔
1538
  bool done = false;
35✔
1539

1540
  // Regex support is optional.
1541
  if (Task::regex)
35✔
1542
  {
1543
    // Create the regex.
1544
    RX rx (from, Task::searchCaseSensitive);
29✔
1545
    std::vector <int> start;
29✔
1546
    std::vector <int> end;
29✔
1547

1548
    // Perform all subs on description.
1549
    if (rx.match (start, end, description))
29✔
1550
    {
1551
      int skew = 0;
21✔
1552
      for (unsigned int i = 0; i < start.size () && !done; ++i)
43✔
1553
      {
1554
        description.replace (start[i] + skew, end[i] - start[i], to);
22✔
1555
        skew += to.length () - (end[i] - start[i]);
22✔
1556
        ++changes;
22✔
1557

1558
        if (!global)
22✔
1559
          done = true;
20✔
1560
      }
1561
    }
1562

1563
    if (!done)
29✔
1564
    {
1565
      // Perform all subs on annotations.
1566
      for (auto& it : annotations)
17✔
1567
      {
1568
        start.clear ();
8✔
1569
        end.clear ();
8✔
1570
        if (rx.match (start, end, it.second))
8✔
1571
        {
1572
          int skew = 0;
4✔
1573
          for (unsigned int i = 0; i < start.size () && !done; ++i)
9✔
1574
          {
1575
            it.second.replace (start[i + skew], end[i] - start[i], to);
5✔
1576
            skew += to.length () - (end[i] - start[i]);
5✔
1577
            ++changes;
5✔
1578

1579
            if (!global)
5✔
1580
              done = true;
3✔
1581
          }
1582
        }
1583
      }
1584
    }
1585
  }
29✔
1586
  else
1587
  {
1588
    // Perform all subs on description.
1589
    int counter = 0;
6✔
1590
    std::string::size_type pos = 0;
6✔
1591
    int skew = 0;
6✔
1592

1593
    while ((pos = ::find (description, from, pos, Task::searchCaseSensitive)) != std::string::npos && !done)
13✔
1594
    {
1595
      description.replace (pos + skew, from.length (), to);
7✔
1596
      skew += to.length () - from.length ();
7✔
1597

1598
      pos += to.length ();
7✔
1599
      ++changes;
7✔
1600

1601
      if (!global)
7✔
1602
        done = true;
5✔
1603

1604
      if (++counter > APPROACHING_INFINITY)
7✔
1605
        throw format ("Terminated substitution because more than {1} changes were made - infinite loop protection.", APPROACHING_INFINITY);
×
1606
    }
1607

1608
    if (!done)
6✔
1609
    {
1610
      // Perform all subs on annotations.
1611
      counter = 0;
1✔
1612
      for (auto& anno : annotations)
1✔
1613
      {
1614
        pos = 0;
×
1615
        skew = 0;
×
1616
        while ((pos = ::find (anno.second, from, pos, Task::searchCaseSensitive)) != std::string::npos && !done)
×
1617
        {
1618
          anno.second.replace (pos + skew, from.length (), to);
×
1619
          skew += to.length () - from.length ();
×
1620

1621
          pos += to.length ();
×
1622
          ++changes;
×
1623

1624
          if (!global)
×
1625
            done = true;
×
1626

1627
          if (++counter > APPROACHING_INFINITY)
×
1628
            throw format ("Terminated substitution because more than {1} changes were made - infinite loop protection.", APPROACHING_INFINITY);
×
1629
        }
1630
      }
1631
    }
1632
  }
1633

1634
  if (changes)
35✔
1635
  {
1636
    set ("description", description);
31✔
1637
    setAnnotations (annotations);
31✔
1638
    recalc_urgency = true;
31✔
1639
  }
1640
}
35✔
1641
#endif
1642

1643
////////////////////////////////////////////////////////////////////////////////
1644
// The purpose of Task::validate is three-fold:
1645
//   1) To provide missing attributes where possible
1646
//   2) To provide suitable warnings about odd states
1647
//   3) To generate errors when the inconsistencies are not fixable
1648
//   4) To update status depending on other attributes
1649
//
1650
// Critically, note that despite the name this is not a read-only function.
1651
//
1652
void Task::validate (bool applyDefault /* = true */)
5,007✔
1653
{
1654
  Task::status status = Task::pending;
5,007✔
1655
  if (get ("status") != "")
5,007✔
1656
    status = getStatus ();
2,409✔
1657

1658
  // 1) Provide missing attributes where possible
1659
  // Provide a UUID if necessary. Validate if present.
1660
  std::string uid = get ("uuid");
10,010✔
1661
  if (has ("uuid") && uid != "")
5,005✔
1662
  {
1663
    Lexer lex (uid);
2,400✔
1664
    std::string token;
2,400✔
1665
    Lexer::Type type;
1666
    if (! lex.isUUID (token, type, true))
2,400✔
1667
      throw format ("Not a valid UUID '{1}'.", uid);
5✔
1668
  }
2,405✔
1669
  else
1670
    set ("uuid", uuid ());
2,605✔
1671

1672
  // TODO Obsolete remove for 3.0.0
1673
  // Recurring tasks get a special status.
1674
  if (status == Task::pending                     &&
14,072✔
1675
      has ("due")                                 &&
9,536✔
1676
      has ("recur")                               &&
5,386✔
1677
      (! has ("parent") || get ("parent") == "")  &&
14,606✔
1678
      (! has ("template") || get ("template") == ""))
5,070✔
1679
  {
1680
    status = Task::recurring;
70✔
1681
  }
1682
/*
1683
  // TODO Add for 3.0.0
1684
  if (status == Task::pending &&
1685
      has ("due")             &&
1686
      has ("recur")          &&
1687
      (! has ("template") || get ("template") == ""))
1688
  {
1689
    status = Task::recurring;
1690
  }
1691
*/
1692

1693
  // Tasks with a wait: date get a special status.
1694
  else if (status == Task::pending &&
13,862✔
1695
           has ("wait")            &&
9,882✔
1696
           get ("wait") != "")
4,952✔
1697
    status = Task::waiting;
22✔
1698

1699
  // By default, tasks are pending.
1700
  else if (! has ("status") || get ("status") == "")
4,908✔
1701
    status = Task::pending;
2,515✔
1702

1703
  // Default to 'periodic' type recurrence.
1704
  if (status == Task::recurring &&
5,174✔
1705
      (! has ("rtype") || get ("rtype") == ""))
5,174✔
1706
  {
1707
    set ("rtype", "periodic");
70✔
1708
  }
1709

1710
  // Store the derived status.
1711
  setStatus (status);
5,000✔
1712

1713
#ifdef PRODUCT_TASKWARRIOR
1714
  // Provide an entry date unless user already specified one.
1715
  if (! has ("entry") || get ("entry") == "")
5,000✔
1716
    setAsNow ("entry");
2,590✔
1717

1718
  // Completed tasks need an end date, so inherit the entry date.
1719
  if ((status == Task::completed || status == Task::deleted) &&
5,358✔
1720
      (! has ("end") || get ("end") == ""))
5,358✔
1721
    setAsNow ("end");
16✔
1722

1723
  // Pending tasks cannot have an end date, remove if present
1724
  if ((status == Task::pending) && (get ("end") != ""))
5,000✔
1725
    remove ("end");
11✔
1726

1727
  // Provide a modified date unless user already specified one.
1728
  if (! has ("modified") || get ("modified") == "")
5,000✔
1729
    setAsNow ("modified");
2,787✔
1730

1731
  if (applyDefault && (! has ("parent") || get ("parent") == ""))
5,000✔
1732
  {
1733
    // Override with default.project, if not specified.
1734
    if (Task::defaultProject != "" &&
4,392✔
1735
        ! has ("project"))
4,392✔
1736
    {
1737
      if (Context::getContext ().columns["project"]->validate (Task::defaultProject))
5✔
1738
        set ("project", Task::defaultProject);
5✔
1739
    }
1740

1741
    // Override with default.due, if not specified.
1742
    if (Task::defaultDue != "" &&
4,386✔
1743
        ! has ("due"))
4,386✔
1744
    {
1745
      if (Context::getContext ().columns["due"]->validate (Task::defaultDue))
4✔
1746
      {
1747
        Duration dur (Task::defaultDue);
4✔
1748
        if (dur.toTime_t () != 0)
4✔
1749
          set ("due", (Datetime () + dur.toTime_t ()).toEpoch ());
×
1750
        else
1751
          set ("due", Datetime (Task::defaultDue).toEpoch ());
4✔
1752
      }
1753
    }
1754

1755
    // Override with default.scheduled, if not specified.
1756
    if (Task::defaultScheduled != "" &&
4,386✔
1757
        ! has ("scheduled"))
4,386✔
1758
    {
1759
      if (Context::getContext ().columns["scheduled"]->validate (Task::defaultScheduled))
5✔
1760
      {
1761
        Duration dur (Task::defaultScheduled);
5✔
1762
        if (dur.toTime_t () != 0)
5✔
1763
          set ("scheduled", (Datetime () + dur.toTime_t ()).toEpoch ());
×
1764
        else
1765
          set ("scheduled", Datetime (Task::defaultScheduled).toEpoch ());
5✔
1766
      }
1767
    }
1768

1769
    // If a UDA has a default value in the configuration,
1770
    // override with uda.(uda).default, if not specified.
1771
    // Gather a list of all UDAs with a .default value
1772
    std::vector <std::string> udas;
4,381✔
1773
    for (auto& var : Context::getContext ().config)
1,078,692✔
1774
    {
1775
      if (! var.first.compare (0, 4, "uda.", 4) &&
1,087,708✔
1776
          var.first.find (".default") != std::string::npos)
13,397✔
1777
      {
1778
        auto period = var.first.find ('.', 4);
8✔
1779
        if (period != std::string::npos)
8✔
1780
          udas.push_back (var.first.substr (4, period - 4));
8✔
1781
      }
1782
    }
1783

1784
    if (udas.size ())
4,381✔
1785
    {
1786
      // For each of those, setup the default value on the task now,
1787
      // of course only if we don't have one on the command line already
1788
      for (auto& uda : udas)
16✔
1789
      {
1790
        std::string defVal= Context::getContext ().config.get ("uda." + uda + ".default");
16✔
1791

1792
        // If the default is empty, or we already have a value, skip it
1793
        if (defVal != "" && get (uda) == "")
8✔
1794
          set (uda, defVal);
5✔
1795
      }
8✔
1796
    }
1797
  }
4,381✔
1798
#endif
1799

1800
  // 2) To provide suitable warnings about odd states
1801

1802
  // Date relationships.
1803
  validate_before ("wait",      "due");
5,000✔
1804
  validate_before ("entry",     "start");
5,000✔
1805
  validate_before ("entry",     "end");
5,000✔
1806
  validate_before ("wait",      "scheduled");
5,000✔
1807
  validate_before ("scheduled", "start");
5,000✔
1808
  validate_before ("scheduled", "due");
5,000✔
1809
  validate_before ("scheduled", "end");
5,000✔
1810

1811
  // 3) To generate errors when the inconsistencies are not fixable
1812

1813
  // There is no fixing a missing description.
1814
  if (! has ("description"))
5,000✔
1815
    throw std::string ("A task must have a description.");
2✔
1816
  else if (get ("description") == "")
4,998✔
1817
    throw std::string ("Cannot add a task that is blank.");
×
1818

1819
  // Cannot have a recur frequency with no due date - when would it recur?
1820
  if (has ("recur") && (! has ("due") || get ("due") == ""))
4,998✔
1821
    throw std::string ("A recurring task must also have a 'due' date.");
1✔
1822

1823
  // Recur durations must be valid.
1824
  if (has ("recur"))
4,997✔
1825
  {
1826
    std::string value = get ("recur");
638✔
1827
    if (value != "")
319✔
1828
    {
1829
      Duration p;
319✔
1830
      std::string::size_type i = 0;
319✔
1831
      if (! p.parse (value, i))
319✔
1832
        // TODO Ideal location to map unsupported old recurrence periods to supported values.
1833
        throw format ("The recurrence value '{1}' is not valid.", value);
×
1834
    }
1835
  }
319✔
1836
}
5,005✔
1837

1838
////////////////////////////////////////////////////////////////////////////////
1839
void Task::validate_before (const std::string& left, const std::string& right)
35,000✔
1840
{
1841
#ifdef PRODUCT_TASKWARRIOR
1842
  if (has (left) &&
45,127✔
1843
      has (right))
10,127✔
1844
  {
1845
    Datetime date_left (get_date (left));
485✔
1846
    Datetime date_right (get_date (right));
485✔
1847

1848
    // if date is zero, then it is being removed (e.g. "due: wait:1day")
1849
    if (date_left > date_right && date_right.toEpoch () != 0)
485✔
1850
      Context::getContext ().footnote (format ("Warning: You have specified that the '{1}' date is after the '{2}' date.", left, right));
10✔
1851
  }
1852
#endif
1853
}
35,000✔
1854

1855
////////////////////////////////////////////////////////////////////////////////
1856
// Encode values prior to serialization.
1857
//   [  -> &open;
1858
//   ]  -> &close;
1859
const std::string Task::encode (const std::string& value) const
×
1860
{
1861
  auto modified = str_replace (value,    "[", "&open;");
×
1862
  return          str_replace (modified, "]", "&close;");
×
1863
}
1864

1865
////////////////////////////////////////////////////////////////////////////////
1866
// Decode values after parse.
1867
//   [  <- &open;
1868
//   ]  <- &close;
1869
const std::string Task::decode (const std::string& value) const
×
1870
{
1871
  if (value.find ('&') == std::string::npos)
×
1872
    return value;
×
1873

1874
  auto modified = str_replace (value,    "&open;",  "[");
×
1875
  return          str_replace (modified, "&close;", "]");
×
1876
}
1877

1878
////////////////////////////////////////////////////////////////////////////////
1879
tc::Status Task::status2tc (const Task::status status)
2,891✔
1880
{
1881
  switch (status) {
2,891✔
1882
    case Task::pending: return tc::Status::Pending;
2,709✔
1883
    case Task::completed: return tc::Status::Completed;
103✔
1884
    case Task::deleted: return tc::Status::Deleted;
12✔
1885
    case Task::waiting: return tc::Status::Pending; // waiting is no longer a status
×
1886
    case Task::recurring: return tc::Status::Recurring;
67✔
1887
    default: return tc::Status::Unknown;
×
1888
  }
1889
}
1890

1891
////////////////////////////////////////////////////////////////////////////////
1892
Task::status Task::tc2status (const tc::Status status)
×
1893
{
1894
  switch (status) {
×
1895
    case tc::Status::Pending: return Task::pending;
×
1896
    case tc::Status::Completed: return Task::completed;
×
1897
    case tc::Status::Deleted: return Task::deleted;
×
1898
    case tc::Status::Recurring: return Task::recurring;
×
1899
    default: return Task::pending;
×
1900
  }
1901
}
1902

1903
////////////////////////////////////////////////////////////////////////////////
1904
int Task::determineVersion (const std::string& line)
×
1905
{
1906
  // Version 2 looks like:
1907
  //
1908
  //   uuid status [tags] [attributes] description\n
1909
  //
1910
  // Where uuid looks like:
1911
  //
1912
  //   27755d92-c5e9-4c21-bd8e-c3dd9e6d3cf7
1913
  //
1914
  // Scan for the hyphens in the uuid, the following space, and a valid status
1915
  // character.
1916
  if (line[8]  == '-' &&
×
1917
      line[13] == '-' &&
×
1918
      line[18] == '-' &&
×
1919
      line[23] == '-' &&
×
1920
      line[36] == ' ' &&
×
1921
      (line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r'))
×
1922
  {
1923
    // Version 3 looks like:
1924
    //
1925
    //   uuid status [tags] [attributes] [annotations] description\n
1926
    //
1927
    // Scan for the number of [] pairs.
1928
    auto tagAtts  = line.find ("] [", 0);
×
1929
    auto attsAnno = line.find ("] [", tagAtts + 1);
×
1930
    auto annoDesc = line.find ("] ",  attsAnno + 1);
×
1931
    if (tagAtts  != std::string::npos &&
×
1932
        attsAnno != std::string::npos &&
×
1933
        annoDesc != std::string::npos)
1934
      return 3;
×
1935
    else
1936
      return 2;
×
1937
  }
1938

1939
  // Version 4 looks like:
1940
  //
1941
  //   [name:"value" ...]
1942
  //
1943
  // Scan for [, ] and :".
1944
  else if (line[0] == '[' &&
×
1945
           line[line.length () - 1] == ']' &&
×
1946
           line.find ("uuid:\"") != std::string::npos)
×
1947
    return 4;
×
1948

1949
  // Version 1 looks like:
1950
  //
1951
  //   [tags] [attributes] description\n
1952
  //   X [tags] [attributes] description\n
1953
  //
1954
  // Scan for the first character being either the bracket or X.
1955
  else if (line.find ("X [") == 0                      ||
×
1956
           (line[0] == '['                             &&
×
1957
            line.substr (line.length () - 1, 1) != "]" &&
×
1958
            line.length () > 3))
×
1959
    return 1;
×
1960

1961
  // Version 5?
1962
  //
1963
  // Fortunately, with the hindsight that will come with version 5, the
1964
  // identifying characteristics of 1, 2, 3 and 4 may be modified such that if 5
1965
  // has a UUID followed by a status, then there is still a way to differentiate
1966
  // between 2, 3, 4 and 5.
1967
  //
1968
  // The danger is that a version 3 binary reads and misinterprets a version 4
1969
  // file.  This is why it is a good idea to rely on an explicit version
1970
  // declaration rather than chance positioning.
1971

1972
  // Zero means 'no idea'.
1973
  return 0;
×
1974
}
1975

1976
////////////////////////////////////////////////////////////////////////////////
1977
// Urgency is defined as a polynomial, the value of which is calculated in this
1978
// function, according to:
1979
//
1980
//   U = A.t  + B.t  + C.t  ...
1981
//          a      b      c
1982
//
1983
//   U       = urgency
1984
//   A       = coefficient for term a
1985
//   t sub a = numeric scale from 0 -> 1, with 1 being the highest
1986
//             urgency, derived from one task attribute and mapped
1987
//             to the numeric scale
1988
//
1989
// See rfc31-urgency.txt for full details.
1990
//
1991
float Task::urgency_c () const
928✔
1992
{
1993
  float value = 0.0;
928✔
1994
#ifdef PRODUCT_TASKWARRIOR
1995
  value += fabsf (Task::urgencyProjectCoefficient)     > epsilon ? (urgency_project ()     * Task::urgencyProjectCoefficient)     : 0.0;
928✔
1996
  value += fabsf (Task::urgencyActiveCoefficient)      > epsilon ? (urgency_active ()      * Task::urgencyActiveCoefficient)      : 0.0;
928✔
1997
  value += fabsf (Task::urgencyScheduledCoefficient)   > epsilon ? (urgency_scheduled ()   * Task::urgencyScheduledCoefficient)   : 0.0;
928✔
1998
  value += fabsf (Task::urgencyWaitingCoefficient)     > epsilon ? (urgency_waiting ()     * Task::urgencyWaitingCoefficient)     : 0.0;
928✔
1999
  value += fabsf (Task::urgencyBlockedCoefficient)     > epsilon ? (urgency_blocked ()     * Task::urgencyBlockedCoefficient)     : 0.0;
928✔
2000
  value += fabsf (Task::urgencyAnnotationsCoefficient) > epsilon ? (urgency_annotations () * Task::urgencyAnnotationsCoefficient) : 0.0;
928✔
2001
  value += fabsf (Task::urgencyTagsCoefficient)        > epsilon ? (urgency_tags ()        * Task::urgencyTagsCoefficient)        : 0.0;
928✔
2002
  value += fabsf (Task::urgencyDueCoefficient)         > epsilon ? (urgency_due ()         * Task::urgencyDueCoefficient)         : 0.0;
928✔
2003
  value += fabsf (Task::urgencyBlockingCoefficient)    > epsilon ? (urgency_blocking ()    * Task::urgencyBlockingCoefficient)    : 0.0;
928✔
2004
  value += fabsf (Task::urgencyAgeCoefficient)         > epsilon ? (urgency_age ()         * Task::urgencyAgeCoefficient)         : 0.0;
928✔
2005

2006
  const std::string taskProjectName = get("project");
1,856✔
2007
  // Tag- and project-specific coefficients.
2008
  for (auto& var : Task::coefficients)
4,839✔
2009
  {
2010
    if (fabs (var.second) > epsilon)
3,911✔
2011
    {
2012
      if (! var.first.compare (0, 13, "urgency.user.", 13))
3,911✔
2013
      {
2014
        // urgency.user.project.<project>.coefficient
2015
        auto end = std::string::npos;
1,135✔
2016
        if (var.first.substr (13, 8) == "project." &&
1,234✔
2017
            (end = var.first.find (".coefficient")) != std::string::npos)
99✔
2018
        {
2019
          std::string project = var.first.substr (21, end - 21);
99✔
2020

2021
          if (taskProjectName == project ||
195✔
2022
              taskProjectName.find(project + '.') == 0)
195✔
2023
          {
2024
            value += var.second;
3✔
2025
          }
2026
        }
99✔
2027

2028
        // urgency.user.tag.<tag>.coefficient
2029
        if (var.first.substr (13, 4) == "tag." &&
2,170✔
2030
            (end = var.first.find (".coefficient")) != std::string::npos)
1,035✔
2031
        {
2032
          std::string tag = var.first.substr (17, end - 17);
1,035✔
2033

2034
          if (hasTag (tag))
1,035✔
2035
            value += var.second;
14✔
2036
        }
1,035✔
2037

2038
        // urgency.user.keyword.<keyword>.coefficient
2039
        if (var.first.substr (13, 8) == "keyword." &&
1,136✔
2040
            (end = var.first.find (".coefficient")) != std::string::npos)
1✔
2041
        {
2042
          std::string keyword = var.first.substr (21, end - 21);
1✔
2043

2044
          if (get ("description").find (keyword) != std::string::npos)
1✔
2045
            value += var.second;
1✔
2046
        }
1✔
2047
      }
2048
      else if (var.first.substr (0, 12) == "urgency.uda.")
2,776✔
2049
      {
2050
        // urgency.uda.<name>.coefficient
2051
        // urgency.uda.<name>.<value>.coefficient
2052
        auto end = var.first.find (".coefficient");
2,776✔
2053
        if (end != std::string::npos)
2,776✔
2054
        {
2055
          const std::string uda = var.first.substr (12, end - 12);
2,776✔
2056
          auto dot = uda.find ('.');
2,776✔
2057
          if (dot == std::string::npos)
2,776✔
2058
          {
2059
            // urgency.uda.<name>.coefficient
2060
            if (has (uda))
1✔
2061
              value += var.second;
1✔
2062
          }
2063
          else
2064
          {
2065
            // urgency.uda.<name>.<value>.coefficient
2066
            if (get (uda.substr(0, dot)) == uda.substr(dot+1))
2,775✔
2067
              value += var.second;
118✔
2068
          }
2069
        }
2,776✔
2070
      }
2071
    }
2072
  }
2073

2074
  if (is_blocking && Context::getContext ().config.getBoolean ("urgency.inherit"))
928✔
2075
  {
2076
    float prev = value;
3✔
2077
    value = std::max (value, urgency_inherit ());
3✔
2078

2079
    // This is a hackish way of making sure parent tasks are sorted above
2080
    // child tasks.  For reports that hide blocked tasks, this is not needed.
2081
    if (prev <= value)
3✔
2082
      value += 0.01;
3✔
2083
  }
2084
#endif
2085

2086
  return value;
928✔
2087
}
928✔
2088

2089
////////////////////////////////////////////////////////////////////////////////
2090
float Task::urgency ()
1,633✔
2091
{
2092
  if (recalc_urgency)
1,633✔
2093
  {
2094
    urgency_value = urgency_c ();
724✔
2095

2096
    // Return the sum of all terms.
2097
    recalc_urgency = false;
724✔
2098
  }
2099

2100
  return urgency_value;
1,633✔
2101
}
2102

2103
////////////////////////////////////////////////////////////////////////////////
2104
float Task::urgency_inherit () const
3✔
2105
{
2106
  float v = -FLT_MAX;
3✔
2107
#ifdef PRODUCT_TASKWARRIOR
2108
  // Calling getBlockedTasks is rather expensive.
2109
  // It is called recursively for each dependency in the chain here.
2110
  for (auto& task : getBlockedTasks ())
6✔
2111
  {
2112
    // Find highest urgency in all blocked tasks.
2113
    v = std::max (v, task.urgency ());
3✔
2114
  }
3✔
2115
#endif
2116

2117
  return v;
3✔
2118
}
2119

2120
////////////////////////////////////////////////////////////////////////////////
2121
float Task::urgency_project () const
985✔
2122
{
2123
  if (has ("project"))
985✔
2124
    return 1.0;
252✔
2125

2126
  return 0.0;
733✔
2127
}
2128

2129
////////////////////////////////////////////////////////////////////////////////
2130
float Task::urgency_active () const
985✔
2131
{
2132
  if (has ("start"))
985✔
2133
    return 1.0;
41✔
2134

2135
  return 0.0;
944✔
2136
}
2137

2138
////////////////////////////////////////////////////////////////////////////////
2139
float Task::urgency_scheduled () const
985✔
2140
{
2141
  if (has ("scheduled") &&
1,993✔
2142
      get_date ("scheduled") < time (nullptr))
1,008✔
2143
    return 1.0;
3✔
2144

2145
  return 0.0;
982✔
2146
}
2147

2148
////////////////////////////////////////////////////////////////////////////////
2149
float Task::urgency_waiting () const
985✔
2150
{
2151
  if (is_waiting ())
985✔
2152
    return 1.0;
6✔
2153

2154
  return 0.0;
979✔
2155
}
2156

2157
////////////////////////////////////////////////////////////////////////////////
2158
// A task is blocked only if the task it depends upon is pending/waiting.
2159
float Task::urgency_blocked () const
973✔
2160
{
2161
  if (is_blocked)
973✔
2162
    return 1.0;
13✔
2163

2164
  return 0.0;
960✔
2165
}
2166

2167
////////////////////////////////////////////////////////////////////////////////
2168
float Task::urgency_annotations () const
985✔
2169
{
2170
       if (annotation_count >= 3) return 1.0;
985✔
2171
  else if (annotation_count == 2) return 0.9;
981✔
2172
  else if (annotation_count == 1) return 0.8;
975✔
2173

2174
  return 0.0;
936✔
2175
}
2176

2177
////////////////////////////////////////////////////////////////////////////////
2178
float Task::urgency_tags () const
985✔
2179
{
2180
  switch (getTagCount ())
985✔
2181
  {
2182
  case 0:  return 0.0;
820✔
2183
  case 1:  return 0.8;
134✔
2184
  case 2:  return 0.9;
27✔
2185
  default: return 1.0;
4✔
2186
  }
2187
}
2188

2189
////////////////////////////////////////////////////////////////////////////////
2190
//
2191
//     Past                  Present                              Future
2192
//     Overdue               Due                                     Due
2193
//
2194
//     -7 -6 -5 -4 -3 -2 -1  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 days
2195
//
2196
// <-- 1.0                         linear                            0.2 -->
2197
//     capped                                                        capped
2198
//
2199
//
2200
float Task::urgency_due () const
985✔
2201
{
2202
  if (has ("due"))
985✔
2203
  {
2204
    Datetime now;
260✔
2205
    Datetime due (get_date ("due"));
260✔
2206

2207
    // Map a range of 21 days to the value 0.2 - 1.0
2208
    float days_overdue = (now - due) / 86400.0;
260✔
2209
         if (days_overdue >= 7.0)   return 1.0;   // < 1 wk ago
260✔
2210
    else if (days_overdue >= -14.0) return ((days_overdue + 14.0) * 0.8 / 21.0) + 0.2;
242✔
2211
    else                            return 0.2;   // > 2 wks
15✔
2212
  }
2213

2214
  return 0.0;
725✔
2215
}
2216

2217
////////////////////////////////////////////////////////////////////////////////
2218
float Task::urgency_age () const
973✔
2219
{
2220
  if (!has ("entry"))
973✔
2221
    return 1.0;
×
2222

2223
  Datetime now;
973✔
2224
  Datetime entry (get_date ("entry"));
973✔
2225
  int age = (now - entry) / 86400;  // in days
973✔
2226

2227
  if (Task::urgencyAgeMax == 0 || age > Task::urgencyAgeMax)
973✔
2228
    return 1.0;
49✔
2229

2230
  return (1.0 * age / Task::urgencyAgeMax);
924✔
2231
}
2232

2233
////////////////////////////////////////////////////////////////////////////////
2234
float Task::urgency_blocking () const
973✔
2235
{
2236
  if (is_blocking)
973✔
2237
    return 1.0;
12✔
2238

2239
  return 0.0;
961✔
2240
}
2241

2242
#ifdef PRODUCT_TASKWARRIOR
2243
////////////////////////////////////////////////////////////////////////////////
2244
// Arguably does not belong here. This method reads the parse tree and calls
2245
// Task methods. It could be a standalone function with no loss in access, as
2246
// well as reducing the object depdendencies of Task.
2247
//
2248
// It came from the Command base object, but doesn't really belong there either.
2249
void Task::modify (modType type, bool text_required /* = false */)
2,039✔
2250
{
2251
  std::string label = "   [1;37;43mMODIFICATION [0m ";
2,039✔
2252

2253
  // while reading the parse tree, consider DOM references in the context of
2254
  // this task
2255
  auto currentTask = Context::getContext ().withCurrentTask(this);
2,039✔
2256

2257
  // Need this for later comparison.
2258
  auto originalStatus = getStatus ();
2,039✔
2259

2260
  std::string text = "";
2,039✔
2261
  bool mods = false;
2,039✔
2262
  for (auto& a : Context::getContext ().cli2._args)
13,125✔
2263
  {
2264
    if (a.hasTag ("MODIFICATION"))
11,480✔
2265
    {
2266
      if (a._lextype == Lexer::Type::pair)
3,069✔
2267
      {
2268
        // 'canonical' is the canonical name. Needs to be said.
2269
        // 'value' requires eval.
2270
        std::string name  = a.attribute ("canonical");
2,486✔
2271
        std::string value = a.attribute ("value");
2,486✔
2272
        if (value == ""     ||
2,473✔
2273
            value == "''"   ||
2,473✔
2274
            value == "\"\"")
1,230✔
2275
        {
2276
          // Special case: Handle bulk removal of 'tags' and 'depends" virtual
2277
          // attributes
2278
          if (name == "depends")
13✔
2279
          {
2280
            for (auto dep: getDependencyUUIDs ())
3✔
2281
              removeDependency(dep);
3✔
2282
          }
2283
          else if (name == "tags")
12✔
2284
          {
2285
            for (auto tag: getTags ())
4✔
2286
              removeTag(tag);
4✔
2287
          }
2288

2289
          // ::composeF4 will skip if the value is blank, but the presence of
2290
          // the attribute will prevent ::validate from applying defaults.
2291
          if ((has (name) && get (name) != "") ||
26✔
2292
              (name == "due"       && Context::getContext ().config.has ("default.due")) ||
24✔
2293
              (name == "scheduled" && Context::getContext ().config.has ("default.scheduled")) ||
46✔
2294
              (name == "project"   && Context::getContext ().config.has ("default.project")))
20✔
2295
          {
2296
            mods = true;
10✔
2297
            set (name, "");
10✔
2298
          }
2299

2300
          Context::getContext ().debug (label + name + " <-- ''");
13✔
2301
        }
2302
        else
2303
        {
2304
          Lexer::dequote (value);
1,230✔
2305

2306
          // Get the column info. Some columns are not modifiable.
2307
          Column* column = Context::getContext ().columns[name];
1,230✔
2308
          if (! column ||
2,460✔
2309
              ! column->modifiable ())
1,230✔
2310
            throw format ("The '{1}' attribute does not allow a value of '{2}'.", name, value);
×
2311

2312
          // Delegate modification to the column object or their base classes.
2313
          if (name == "depends"             ||
2,409✔
2314
              name == "tags"                ||
2,354✔
2315
              name == "recur"               ||
2,277✔
2316
              column->type () == "date"     ||
1,482✔
2317
              column->type () == "duration" ||
750✔
2318
              column->type () == "numeric"  ||
2,779✔
2319
              column->type () == "string")
353✔
2320
          {
2321
            column->modify (*this, value);
1,230✔
2322
            mods = true;
836✔
2323
          }
2324

2325
          else
2326
            throw format ("Unrecognized column type '{1}' for column '{2}'", column->type (), name);
×
2327
        }
2328
      }
1,637✔
2329

2330
      // Perform description/annotation substitution.
2331
      else if (a._lextype == Lexer::Type::substitution)
1,826✔
2332
      {
2333
        Context::getContext ().debug (label + "substitute " + a.attribute ("raw"));
35✔
2334
        substitute (a.attribute ("from"),
35✔
2335
                    a.attribute ("to"),
70✔
2336
                    a.attribute ("flags"));
70✔
2337
        mods = true;
35✔
2338
      }
2339

2340
      // Tags need special handling because they are essentially a vector stored
2341
      // in a single string, therefore Task::{add,remove}Tag must be called as
2342
      // appropriate.
2343
      else if (a._lextype == Lexer::Type::tag)
1,791✔
2344
      {
2345
        std::string tag = a.attribute ("name");
324✔
2346
        feedback_reserved_tags (tag);
162✔
2347

2348
        if (a.attribute ("sign") == "+")
162✔
2349
        {
2350
          Context::getContext ().debug (label + "tags <-- add '" + tag + '\'');
154✔
2351
          addTag (tag);
154✔
2352
          feedback_special_tags (*this, tag);
154✔
2353
        }
2354
        else
2355
        {
2356
          Context::getContext ().debug (label + "tags <-- remove '" + tag + '\'');
8✔
2357
          removeTag (tag);
8✔
2358
        }
2359

2360
        mods = true;
162✔
2361
      }
162✔
2362

2363
      // Unknown args are accumulated as though they were WORDs.
2364
      else
2365
      {
2366
        if (text != "")
1,629✔
2367
          text += ' ';
311✔
2368
        text += a.attribute ("raw");
1,629✔
2369
      }
2370
    }
2371
  }
2372

2373
  // Task::modType determines what happens to the WORD arguments, if there are
2374
  //  any.
2375
  if (text != "")
1,645✔
2376
  {
2377
    Lexer::dequote (text);
1,309✔
2378

2379
    switch (type)
1,309✔
2380
    {
2381
    case modReplace:
1,215✔
2382
      Context::getContext ().debug (label + "description <-- '" + text + '\'');
1,215✔
2383
      set ("description", text);
1,215✔
2384
      break;
1,215✔
2385

2386
    case modPrepend:
4✔
2387
      Context::getContext ().debug (label + "description <-- '" + text + "' + description");
4✔
2388
      set ("description", text + ' ' + get ("description"));
4✔
2389
      break;
4✔
2390

2391
    case modAppend:
6✔
2392
      Context::getContext ().debug (label + "description <-- description + '" + text + '\'');
6✔
2393
      set ("description", get ("description") + ' ' + text);
6✔
2394
      break;
6✔
2395

2396
    case modAnnotate:
84✔
2397
      Context::getContext ().debug (label + "new annotation <-- '" + text + '\'');
84✔
2398
      addAnnotation (text);
84✔
2399
      break;
84✔
2400
    }
2401
  }
2402
  else if (! mods && text_required)
336✔
2403
    throw std::string ("Additional text must be provided.");
4✔
2404

2405
  // Modifying completed/deleted tasks generates a message, if the modification
2406
  // does not change status.
2407
  if ((getStatus () == Task::completed || getStatus () == Task::deleted) &&
1,646✔
2408
      getStatus () == originalStatus)
5✔
2409
  {
2410
    auto uuid = get ("uuid").substr (0, 8);
10✔
2411
    Context::getContext ().footnote (format ("Note: Modified task {1} is {2}. You may wish to make this task pending with: task {3} modify status:pending", uuid, get ("status"), uuid));
5✔
2412
  }
5✔
2413
}
2,835✔
2414
#endif
2415

2416
////////////////////////////////////////////////////////////////////////////////
2417
// Compare this task to another and summarize the differences for display, in
2418
// the future tense ("Foo will be set to ..").
2419
std::string Task::diff (const Task& after) const
315✔
2420
{
2421
  // Attributes are all there is, so figure the different attribute names
2422
  // between this (before) and after.
2423
  std::vector <std::string> beforeAtts;
315✔
2424
  for (auto& att : data)
2,234✔
2425
    beforeAtts.push_back (att.first);
1,919✔
2426

2427
  std::vector <std::string> afterAtts;
315✔
2428
  for (auto& att : after.data)
2,519✔
2429
    afterAtts.push_back (att.first);
2,204✔
2430

2431
  std::vector <std::string> beforeOnly;
315✔
2432
  std::vector <std::string> afterOnly;
315✔
2433
  listDiff (beforeAtts, afterAtts, beforeOnly, afterOnly);
315✔
2434

2435
  // Now start generating a description of the differences.
2436
  std::stringstream out;
315✔
2437
  for (auto& name : beforeOnly)
361✔
2438
  {
2439
    if (isAnnotationAttr (name))
46✔
2440
    {
2441
      out << "  - "
2442
          << format ("Annotation {1} will be removed.", name)
12✔
2443
          << "\n";
12✔
2444
    }
2445
    else if (isTagAttr (name))
40✔
2446
    {
2447
      out << "  - "
2448
          << format ("Tag {1} will be removed.", attr2Tag (name))
20✔
2449
          << "\n";
20✔
2450
    }
2451
    else if (isDepAttr (name))
30✔
2452
    {
2453
      out << "  - "
2454
          << format ("Depenency on {1} will be removed.", attr2Dep (name))
12✔
2455
          << "\n";
12✔
2456
    }
2457
    else if (name == "depends" || name == "tags")
24✔
2458
    {
2459
      // do nothing for legacy attributes
2460
    }
2461
    else
2462
    {
2463
      out << "  - "
2464
          << format ("{1} will be deleted.", Lexer::ucFirst (name))
36✔
2465
          << "\n";
36✔
2466
    }
2467
  }
2468

2469
  for (auto& name : afterOnly)
646✔
2470
  {
2471
    if (isAnnotationAttr (name))
331✔
2472
    {
2473
      out << format ("Annotation of {1} will be added.\n", after.get (name));
88✔
2474
    }
2475
    else if (isTagAttr (name))
243✔
2476
    {
2477
      out << format ("Tag {1} will be added.\n", attr2Tag (name));
33✔
2478
    }
2479
    else if (isDepAttr (name))
210✔
2480
    {
2481
      out << format ("Dependency on {1} will be added.\n", attr2Dep (name));
33✔
2482
    }
2483
    else if (name == "depends" || name == "tags")
177✔
2484
    {
2485
      // do nothing for legacy attributes
2486
    }
2487
    else
2488
      out << "  - "
2489
          << format ("{1} will be set to '{2}'.",
258✔
2490
                     Lexer::ucFirst (name),
258✔
2491
                     renderAttribute (name, after.get (name)))
258✔
2492
          << "\n";
258✔
2493
  }
2494

2495
  for (auto& name : beforeAtts)
2,234✔
2496
  {
2497
    // Ignore UUID differences, and find values that changed, but are not also
2498
    // in the beforeOnly and afterOnly lists, which have been handled above..
2499
    if (name              != "uuid" &&
1,606✔
2500
        get (name)        != after.get (name) &&
3,705✔
2501
        std::find (beforeOnly.begin (), beforeOnly.end (), name) == beforeOnly.end () &&
3,705✔
2502
        std::find (afterOnly.begin (),  afterOnly.end (),  name) == afterOnly.end ())
2,053✔
2503
    {
2504
      if (name == "depends" || name == "tags")
134✔
2505
      {
2506
        // do nothing for legacy attributes
2507
      }
2508
      else if (isTagAttr (name) || isDepAttr (name))
125✔
2509
      {
2510
        // ignore new attributes
2511
      }
2512
      else if (isAnnotationAttr (name))
125✔
2513
      {
2514
        out << format ("Annotation will be changed to {1}.\n", after.get (name));
4✔
2515
      }
2516
      else
2517
        out << "  - "
2518
            << format ("{1} will be changed from '{2}' to '{3}'.",
242✔
2519
                       Lexer::ucFirst (name),
242✔
2520
                       renderAttribute (name, get (name)),
242✔
2521
                       renderAttribute (name, after.get (name)))
242✔
2522
            << "\n";
242✔
2523
    }
2524
  }
2525

2526
  // Shouldn't just say nothing.
2527
  if (out.str ().length () == 0)
315✔
2528
    out << "  - No changes will be made.\n";
1✔
2529

2530
  return out.str ();
630✔
2531
}
315✔
2532

2533
////////////////////////////////////////////////////////////////////////////////
2534
// Similar to diff, but formatted for inclusion in the output of the info command
2535
std::string Task::diffForInfo (
×
2536
  const Task& after,
2537
  const std::string& dateformat,
2538
  long& last_timestamp,
2539
  const long current_timestamp) const
2540
{
2541
  // Attributes are all there is, so figure the different attribute names
2542
  // between before and after.
2543
  std::vector <std::string> beforeAtts;
×
2544
  for (auto& att : data)
×
2545
    beforeAtts.push_back (att.first);
×
2546

2547
  std::vector <std::string> afterAtts;
×
2548
  for (auto& att : after.data)
×
2549
    afterAtts.push_back (att.first);
×
2550

2551
  std::vector <std::string> beforeOnly;
×
2552
  std::vector <std::string> afterOnly;
×
2553
  listDiff (beforeAtts, afterAtts, beforeOnly, afterOnly);
×
2554

2555
  // Now start generating a description of the differences.
2556
  std::stringstream out;
×
2557
  for (auto& name : beforeOnly)
×
2558
  {
2559
    if (isAnnotationAttr (name))
×
2560
    {
2561
      out << format ("Annotation '{1}' deleted.\n", get (name));
×
2562
    }
2563
    else if (isTagAttr (name))
×
2564
    {
2565
      out << format ("Tag '{1}' deleted.\n", attr2Tag(name));
×
2566
    }
2567
    else if (isDepAttr (name))
×
2568
    {
2569
      out << format ("Dependency on '{1}' deleted.\n", attr2Dep(name));
×
2570
    }
2571
    else if (name == "depends" || name == "tags")
×
2572
    {
2573
      // do nothing for legacy attributes
2574
    }
2575
    else if (name == "start")
×
2576
    {
2577
      Datetime started (get ("start"));
×
2578
      Datetime stopped;
×
2579

2580
      if (after.has ("end"))
×
2581
        // Task was marked as finished, use end time
2582
        stopped = Datetime (after.get ("end"));
×
2583
      else
2584
        // Start attribute was removed, use modification time
2585
        stopped = Datetime (current_timestamp);
×
2586

2587
      out << format ("{1} deleted (duration: {2}).",
×
2588
                     Lexer::ucFirst (name),
×
2589
                     Duration (stopped - started).format ())
×
2590
          << "\n";
×
2591
    }
2592
    else
2593
    {
2594
      out << format ("{1} deleted.\n", Lexer::ucFirst (name));
×
2595
    }
2596
  }
2597

2598
  for (auto& name : afterOnly)
×
2599
  {
2600
    if (isAnnotationAttr (name))
×
2601
    {
2602
      out << format ("Annotation of '{1}' added.\n", after.get (name));
×
2603
    }
2604
    else if (isTagAttr (name))
×
2605
    {
2606
      out << format ("Tag '{1}' added.\n", attr2Tag (name));
×
2607
    }
2608
    else if (isDepAttr (name))
×
2609
    {
2610
      out << format ("Dependency on '{1}' added.\n", attr2Dep (name));
×
2611
    }
2612
    else if (name == "depends" || name == "tags")
×
2613
    {
2614
      // do nothing for legacy attributes
2615
    }
2616
    else
2617
    {
2618
      if (name == "start")
×
2619
          last_timestamp = current_timestamp;
×
2620

2621
      out << format ("{1} set to '{2}'.",
×
2622
                     Lexer::ucFirst (name),
×
2623
                     renderAttribute (name, after.get (name), dateformat))
×
2624
          << "\n";
×
2625
    }
2626
  }
2627

2628
  for (auto& name : beforeAtts)
×
2629
    if (name              != "uuid" &&
×
2630
        name              != "modified" &&
×
2631
        get (name)        != after.get (name) &&
×
2632
        get (name)        != "" &&
×
2633
        after.get (name)  != "")
×
2634
    {
2635
      if (name == "depends" || name == "tags")
×
2636
      {
2637
        // do nothing for legacy attributes
2638
      }
2639
      else if (isTagAttr (name) || isDepAttr (name))
×
2640
      {
2641
        // ignore new attributes
2642
      }
2643
      else if (isAnnotationAttr (name))
×
2644
      {
2645
        out << format ("Annotation changed to '{1}'.\n", after.get (name));
×
2646
      }
2647
      else
2648
        out << format ("{1} changed from '{2}' to '{3}'.",
×
2649
                       Lexer::ucFirst (name),
×
2650
                       renderAttribute (name, get (name), dateformat),
×
2651
                       renderAttribute (name, after.get (name), dateformat))
×
2652
            << "\n";
×
2653
    }
2654

2655
  // Shouldn't just say nothing.
2656
  if (out.str ().length () == 0)
×
2657
    out << "No changes made.\n";
×
2658

2659
  return out.str ();
×
2660
}
2661

2662
////////////////////////////////////////////////////////////////////////////////
2663
// Similar to diff, but formatted as a side-by-side table for an Undo preview
2664
Table Task::diffForUndoSide (
×
2665
  const Task& after) const
2666
{
2667
  // Set the colors.
2668
  Color color_red   (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.before") : "");
×
2669
  Color color_green (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.after") : "");
×
2670

2671
  // Attributes are all there is, so figure the different attribute names
2672
  // between before and after.
2673
  Table view;
×
2674
  view.width (Context::getContext ().getWidth ());
×
2675
  view.intraPadding (2);
×
2676
  view.add ("");
×
2677
  view.add ("Prior Values");
×
2678
  view.add ("Current Values");
×
2679
  setHeaderUnderline (view);
×
2680

2681
  if (!is_empty ())
×
2682
  {
2683
    const Task &before = *this;
×
2684

2685
    std::vector <std::string> beforeAtts;
×
2686
    for (auto& att : before.data)
×
2687
      beforeAtts.push_back (att.first);
×
2688

2689
    std::vector <std::string> afterAtts;
×
2690
    for (auto& att : after.data)
×
2691
      afterAtts.push_back (att.first);
×
2692

2693
    std::vector <std::string> beforeOnly;
×
2694
    std::vector <std::string> afterOnly;
×
2695
    listDiff (beforeAtts, afterAtts, beforeOnly, afterOnly);
×
2696

2697
    int row;
2698
    for (auto& name : beforeOnly)
×
2699
    {
2700
      row = view.addRow ();
×
2701
      view.set (row, 0, name);
×
2702
      view.set (row, 1, renderAttribute (name, before.get (name)), color_red);
×
2703
    }
2704

2705
    for (auto& att : before.data)
×
2706
    {
2707
      std::string priorValue   = before.get (att.first);
×
2708
      std::string currentValue = after.get  (att.first);
×
2709

2710
      if (currentValue != "")
×
2711
      {
2712
        row = view.addRow ();
×
2713
        view.set (row, 0, att.first);
×
2714
        view.set (row, 1, renderAttribute (att.first, priorValue),
×
2715
                  (priorValue != currentValue ? color_red : Color ()));
×
2716
        view.set (row, 2, renderAttribute (att.first, currentValue),
×
2717
                  (priorValue != currentValue ? color_green : Color ()));
×
2718
      }
2719
    }
2720

2721
    for (auto& name : afterOnly)
×
2722
    {
2723
      row = view.addRow ();
×
2724
      view.set (row, 0, name);
×
2725
      view.set (row, 2, renderAttribute (name, after.get (name)), color_green);
×
2726
    }
2727
  }
2728
  else
2729
  {
2730
    int row;
2731
    for (auto& att : after.data)
×
2732
    {
2733
      row = view.addRow ();
×
2734
      view.set (row, 0, att.first);
×
2735
      view.set (row, 2, renderAttribute (att.first, after.get (att.first)), color_green);
×
2736
    }
2737
  }
2738

2739
  return view;
×
2740
}
2741

2742
////////////////////////////////////////////////////////////////////////////////
2743
// Similar to diff, but formatted as a diff for an Undo preview
2744
Table Task::diffForUndoPatch (
×
2745
  const Task& after,
2746
  const Datetime& lastChange) const
2747
{
2748
  // This style looks like this:
2749
  //  --- before    2009-07-04 00:00:25.000000000 +0200
2750
  //  +++ after    2009-07-04 00:00:45.000000000 +0200
2751
  //
2752
  // - name: old           // att deleted
2753
  // + name:
2754
  //
2755
  // - name: old           // att changed
2756
  // + name: new
2757
  //
2758
  // - name:
2759
  // + name: new           // att added
2760
  //
2761

2762
  // Set the colors.
2763
  Color color_red   (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.before") : "");
×
2764
  Color color_green (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.after") : "");
×
2765

2766
  const Task &before = *this;
×
2767

2768
  // Generate table header.
2769
  Table view;
×
2770
  view.width (Context::getContext ().getWidth ());
×
2771
  view.intraPadding (2);
×
2772
  view.add ("");
×
2773
  view.add ("");
×
2774

2775
  int row = view.addRow ();
×
2776
  view.set (row, 0, "--- previous state", color_red);
×
2777
  view.set (row, 1, "Undo will restore this state", color_red);
×
2778

2779
  row = view.addRow ();
×
2780
  view.set (row, 0, "+++ current state ", color_green);
×
2781
  view.set (row, 1, format ("Change made {1}",
×
2782
                            lastChange.toString (Context::getContext ().config.get ("dateformat"))),
×
2783
                    color_green);
2784

2785
  view.addRow ();
×
2786

2787
  // Add rows to table showing diffs.
2788
  std::vector <std::string> all = Context::getContext ().getColumns ();
×
2789

2790
  // Now factor in the annotation attributes.
2791
  for (auto& it : before.data)
×
2792
    if (it.first.substr (0, 11) == "annotation_")
×
2793
      all.push_back (it.first);
×
2794

2795
  for (auto& it : after.data)
×
2796
    if (it.first.substr (0, 11) == "annotation_")
×
2797
      all.push_back (it.first);
×
2798

2799
  // Now render all the attributes.
2800
  std::sort (all.begin (), all.end ());
×
2801

2802
  std::string before_att;
×
2803
  std::string after_att;
×
2804
  std::string last_att;
×
2805
  for (auto& a : all)
×
2806
  {
2807
    if (a != last_att)  // Skip duplicates.
×
2808
    {
2809
      last_att = a;
×
2810

2811
      before_att = before.get (a);
×
2812
      after_att  = after.get (a);
×
2813

2814
      // Don't report different uuid.
2815
      // Show nothing if values are the unchanged.
2816
      if (a == "uuid" ||
×
2817
          before_att == after_att)
×
2818
      {
2819
        // Show nothing - no point displaying that which did not change.
2820

2821
        // row = view.addRow ();
2822
        // view.set (row, 0, *a + ":");
2823
        // view.set (row, 1, before_att);
2824
      }
2825

2826
      // Attribute deleted.
2827
      else if (before_att != "" && after_att == "")
×
2828
      {
2829
        row = view.addRow ();
×
2830
        view.set (row, 0, '-' + a + ':', color_red);
×
2831
        view.set (row, 1, before_att, color_red);
×
2832

2833
        row = view.addRow ();
×
2834
        view.set (row, 0, '+' + a + ':', color_green);
×
2835
      }
2836

2837
      // Attribute added.
2838
      else if (before_att == "" && after_att != "")
×
2839
      {
2840
        row = view.addRow ();
×
2841
        view.set (row, 0, '-' + a + ':', color_red);
×
2842

2843
        row = view.addRow ();
×
2844
        view.set (row, 0, '+' + a + ':', color_green);
×
2845
        view.set (row, 1, after_att, color_green);
×
2846
      }
2847

2848
      // Attribute changed.
2849
      else
2850
      {
2851
        row = view.addRow ();
×
2852
        view.set (row, 0, '-' + a + ':', color_red);
×
2853
        view.set (row, 1, before_att, color_red);
×
2854

2855
        row = view.addRow ();
×
2856
        view.set (row, 0, '+' + a + ':', color_green);
×
2857
        view.set (row, 1, after_att, color_green);
×
2858
      }
2859
    }
2860
  }
2861

2862
  return view;
×
2863
}
2864

2865
////////////////////////////////////////////////////////////////////////////////
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc