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

GothenburgBitFactory / taskwarrior / 9827424216

07 Jul 2024 12:51PM UTC coverage: 84.083% (-0.1%) from 84.196%
9827424216

push

github

web-flow
Do not create recurring tasks before today (#3542)

Tasks can be due "today", as `task add foo due:today ..` is a common
form. However, recurrences before that are just not created.

This avoids a lengthy "hang" when recurrences are updated on an old task
database, as many tasks in the past are created.

3 of 3 new or added lines in 1 file covered. (100.0%)

29 existing lines in 3 files now uncovered.

19223 of 22862 relevant lines covered (84.08%)

20063.52 hits per line

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

65.42
/src/recur.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 <iostream>
29
#include <iomanip>
30
#include <fstream>
31
#include <sstream>
32
#include <sys/types.h>
33
#include <inttypes.h>
34
#include <stdio.h>
35
#include <string.h>
36
#include <unistd.h>
37
#include <stdlib.h>
38
#include <pwd.h>
39
#include <time.h>
40
#include <Context.h>
41
#include <Lexer.h>
42
#include <Datetime.h>
43
#include <Duration.h>
44
#include <format.h>
45
#include <unicode.h>
46
#include <util.h>
47
#include <main.h>
48

49
////////////////////////////////////////////////////////////////////////////////
50
// Scans all tasks, and for any recurring tasks, determines whether any new
51
// child tasks need to be generated to fill gaps.
52
void handleRecurrence ()
859✔
53
{
54
  // Recurrence can be disabled.
55
  // Note: This is currently a workaround for TD-44, TW-1520.
56
  if (! Context::getContext ().config.getBoolean ("recurrence"))
859✔
57
    return;
2✔
58

59
  auto tasks = Context::getContext ().tdb2.pending_tasks ();
857✔
60
  Datetime now;
857✔
61

62
  // Look at all tasks and find any recurring ones.
63
  for (auto& t : tasks)
7,224✔
64
  {
65
    if (t.getStatus () == Task::recurring)
6,367✔
66
    {
67
      // Generate a list of due dates for this recurring task, regardless of
68
      // the mask.
69
      std::vector <Datetime> due;
144✔
70
      if (! generateDueDates (t, due))
144✔
71
      {
72
        // Determine the end date.
73
        t.setStatus (Task::deleted);
×
74
        Context::getContext ().tdb2.modify (t);
×
75
        Context::getContext ().footnote (onExpiration (t));
×
76
        continue;
×
77
      }
78

79
      // Get the mask from the parent task.
80
      auto mask = t.get ("mask");
288✔
81

82
      // Iterate over the due dates, and check each against the mask.
83
      auto changed = false;
144✔
84
      unsigned int i = 0;
144✔
85
      for (auto& d : due)
338✔
86
      {
87
        if (mask.length () <= i)
194✔
88
        {
89
          changed = true;
104✔
90

91
          Task rec (t);                          // Clone the parent.
104✔
92
          rec.setStatus (Task::pending);         // Change the status.
104✔
93
          rec.set ("uuid", uuid ());             // New UUID.
104✔
94
          rec.set ("parent", t.get ("uuid"));    // Remember mom.
104✔
95
          rec.setAsNow ("entry");                // New entry date.
104✔
96
          rec.set ("due", format (d.toEpoch ()));
104✔
97

98
          if (t.has ("wait"))
104✔
99
          {
100
            Datetime old_wait (t.get_date ("wait"));
1✔
101
            Datetime old_due (t.get_date ("due"));
1✔
102
            Datetime due (d);
1✔
103
            rec.set ("wait", format ((due + (old_wait - old_due)).toEpoch ()));
1✔
104
            rec.setStatus (Task::waiting);
1✔
105
            mask += 'W';
1✔
106
          }
107
          else
108
          {
109
            mask += '-';
103✔
110
            rec.setStatus (Task::pending);
103✔
111
          }
112

113
          rec.set ("imask", i);
104✔
114
          rec.remove ("mask");                   // Remove the mask of the parent.
104✔
115

116
          // Add the new task to the DB.
117
          Context::getContext ().tdb2.add (rec);
104✔
118
        }
104✔
119

120
        ++i;
194✔
121
      }
122

123
      // Only modify the parent if necessary.
124
      if (changed)
144✔
125
      {
126
        t.set ("mask", mask);
67✔
127
        Context::getContext ().tdb2.modify (t);
67✔
128

129
        if (Context::getContext ().verbose ("recur"))
67✔
130
          Context::getContext ().footnote (format ("Creating recurring task instance '{1}'", t.get ("description")));
53✔
131
      }
132
    }
144✔
133
  }
134
}
857✔
135

136
////////////////////////////////////////////////////////////////////////////////
137
// Determine a start date (due), an optional end date (until), and an increment
138
// period (recur).  Then generate a set of corresponding dates. Only recurrences
139
// in the future are returned; see #3501.
140
//
141
// Returns false if the parent recurring task is deleted.
142
bool generateDueDates (Task& parent, std::vector <Datetime>& allDue)
144✔
143
{
144
  // Determine due date, recur period and until date.
145
  Datetime due (parent.get_date ("due"));
144✔
146
  if (due._date == 0)
144✔
147
    return false;
×
148

149
  std::string recur = parent.get ("recur");
288✔
150

151
  bool specificEnd = false;
144✔
152
  Datetime until;
144✔
153
  if (parent.get ("until") != "")
144✔
154
  {
155
    until = Datetime (parent.get ("until"));
5✔
156
    specificEnd = true;
5✔
157
  }
158

159
  auto recurrence_limit = Context::getContext ().config.getInteger ("recurrence.limit");
144✔
160
  int recurrence_counter = 0;
144✔
161
  Datetime now;
144✔
162
  Datetime today = now.startOfDay();
144✔
163
  for (Datetime i = due; ; i = getNextRecurrence (i, recur))
203✔
164
  {
165
    // Do not add tasks before today (but allow today for the common `due:today` form).
166
    if (i >= today)
203✔
167
      allDue.push_back (i);
194✔
168

169
    if (specificEnd && i > until)
203✔
170
    {
171
      // If i > until, it means there are no more tasks to generate, and if the
172
      // parent mask contains all + or X, then there never will be another task
173
      // to generate, and this parent task may be safely reaped.
174
      auto mask = parent.get ("mask");
6✔
175
      if (mask.length () == allDue.size () &&
3✔
UNCOV
176
          mask.find ('-') == std::string::npos)
×
177
        return false;
×
178

179
      return true;
3✔
180
    }
3✔
181

182
    if (i > now)
200✔
183
      ++recurrence_counter;
177✔
184

185
    if (recurrence_counter >= recurrence_limit)
200✔
186
      return true;
141✔
187
  }
59✔
188

189
  return true;
190
}
144✔
191

192
////////////////////////////////////////////////////////////////////////////////
193
Datetime getNextRecurrence (Datetime& current, std::string& period)
59✔
194
{
195
  auto m = current.month ();
59✔
196
  auto d = current.day ();
59✔
197
  auto y = current.year ();
59✔
198
  auto ho = current.hour ();
59✔
199
  auto mi = current.minute ();
59✔
200
  auto se = current.second ();
59✔
201

202
  // Some periods are difficult, because they can be vague.
203
  if (period == "monthly" ||
118✔
204
      period == "P1M")
59✔
205
  {
206
    if (++m > 12)
×
207
    {
208
       m -= 12;
×
209
       ++y;
×
210
    }
211

212
    while (! Datetime::valid (y, m, d))
×
213
      --d;
×
214

215
    return Datetime (y, m, d, ho, mi, se);
×
216
  }
217

218
  else if (period == "weekdays")
59✔
219
  {
220
    auto dow = current.dayOfWeek ();
1✔
221
    int days;
222

223
         if (dow == 5) days = 3;
1✔
224
    else if (dow == 6) days = 2;
×
225
    else               days = 1;
×
226

227
    return current + (days * 86400);
1✔
228
  }
229

230
  else if (unicodeLatinDigit (period[0]) &&
63✔
231
           period[period.length () - 1] == 'm')
5✔
232
  {
233
    int increment = strtol (period.substr (0, period.length () - 1).c_str (), nullptr, 10);
1✔
234

235
    if (increment <= 0)
1✔
236
      throw format ("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment);
×
237

238
    m += increment;
1✔
239
    while (m > 12)
1✔
240
    {
241
       m -= 12;
×
242
       ++y;
×
243
    }
244

245
    while (! Datetime::valid (y, m, d))
1✔
246
      --d;
×
247

248
    return Datetime (y, m, d, ho, mi, se);
1✔
249
  }
250

251
  else if (period[0] == 'P'                                            &&
183✔
252
           Lexer::isAllDigits (period.substr (1, period.length () - 2)) &&
57✔
253
           period[period.length () - 1] == 'M')
×
254
  {
255
    int increment = strtol (period.substr (1, period.length () - 2).c_str (), nullptr, 10);
×
256

257
    if (increment <= 0)
×
258
      throw format ("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment);
×
259

260
    m += increment;
×
261
    while (m > 12)
×
262
    {
263
       m -= 12;
×
264
       ++y;
×
265
    }
266

267
    while (! Datetime::valid (y, m, d))
×
268
      --d;
×
269

270
    return Datetime (y, m, d);
×
271
  }
272

273
  else if (period == "quarterly" ||
114✔
274
           period == "P3M")
57✔
275
  {
276
    m += 3;
×
277
    if (m > 12)
×
278
    {
279
       m -= 12;
×
280
       ++y;
×
281
    }
282

283
    while (! Datetime::valid (y, m, d))
×
284
      --d;
×
285

286
    return Datetime (y, m, d, ho, mi, se);
×
287
  }
288

289
  else if (unicodeLatinDigit (period[0]) && period[period.length () - 1] == 'q')
57✔
290
  {
291
    int increment = strtol (period.substr (0, period.length () - 1).c_str (), nullptr, 10);
×
292

293
    if (increment <= 0)
×
294
      throw format ("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period, increment);
×
295

296
    m += 3 * increment;
×
297
    while (m > 12)
×
298
    {
299
       m -= 12;
×
300
       ++y;
×
301
    }
302

303
    while (! Datetime::valid (y, m, d))
×
304
      --d;
×
305

306
    return Datetime (y, m, d, ho, mi, se);
×
307
  }
308

309
  else if (period == "semiannual" ||
114✔
310
           period == "P6M")
57✔
311
  {
312
    m += 6;
×
313
    if (m > 12)
×
314
    {
315
       m -= 12;
×
316
       ++y;
×
317
    }
318

319
    while (! Datetime::valid (y, m, d))
×
320
      --d;
×
321

322
    return Datetime (y, m, d, ho, mi, se);
×
323
  }
324

325
  else if (period == "bimonthly" ||
114✔
326
           period == "P2M")
57✔
327
  {
328
    m += 2;
×
329
    if (m > 12)
×
330
    {
331
       m -= 12;
×
332
       ++y;
×
333
    }
334

335
    while (! Datetime::valid (y, m, d))
×
336
      --d;
×
337

338
    return Datetime (y, m, d, ho, mi, se);
×
339
  }
340

341
  else if (period == "biannual" ||
114✔
342
           period == "biyearly" ||
114✔
343
           period == "P2Y")
57✔
344
  {
345
    y += 2;
×
346

347
    return Datetime (y, m, d, ho, mi, se);
×
348
  }
349

350
  else if (period == "annual" ||
98✔
351
           period == "yearly" ||
98✔
352
           period == "P1Y")
41✔
353
  {
354
    y += 1;
16✔
355

356
    // If the due data just happens to be 2/29 in a leap year, then simply
357
    // incrementing y is going to create an invalid date.
358
    if (m == 2 && d == 29)
16✔
359
      d = 28;
×
360

361
    return Datetime (y, m, d, ho, mi, se);
16✔
362
  }
363

364
  // Add the period to current, and we're done.
365
  std::string::size_type idx = 0;
41✔
366
  Duration p;
41✔
367
  if (! p.parse (period, idx))
41✔
368
    throw std::string (format ("The recurrence value '{1}' is not valid.", period));
×
369

370
  return current + p.toTime_t ();
41✔
371
}
372

373
////////////////////////////////////////////////////////////////////////////////
374
// When the status of a recurring child task changes, the parent task must
375
// update it's mask.
376
void updateRecurrenceMask (Task& task)
307✔
377
{
378
  auto uuid = task.get ("parent");
614✔
379
  Task parent;
307✔
380

381
  if (uuid != "" &&
330✔
382
      Context::getContext ().tdb2.get (uuid, parent))
23✔
383
  {
384
    unsigned int index = strtol (task.get ("imask").c_str (), nullptr, 10);
23✔
385
    auto mask = parent.get ("mask");
46✔
386
    if (mask.length () > index)
23✔
387
    {
388
      mask[index] = (task.getStatus () == Task::pending)   ? '-'
35✔
389
                  : (task.getStatus () == Task::completed) ? '+'
24✔
390
                  : (task.getStatus () == Task::deleted)   ? 'X'
12✔
391
                  : (task.getStatus () == Task::waiting)   ? 'W'
×
392
                  :                                          '?';
393
    }
394
    else
395
    {
396
      std::string mask;
×
397
      for (unsigned int i = 0; i < index; ++i)
×
398
        mask += "?";
×
399

400
      mask += (task.getStatus () == Task::pending)   ? '-'
×
401
            : (task.getStatus () == Task::completed) ? '+'
×
402
            : (task.getStatus () == Task::deleted)   ? 'X'
×
403
            : (task.getStatus () == Task::waiting)   ? 'W'
×
404
            :                                          '?';
×
405
    }
406

407
    parent.set ("mask", mask);
23✔
408
    Context::getContext ().tdb2.modify (parent);
23✔
409
  }
23✔
410
}
307✔
411

412
////////////////////////////////////////////////////////////////////////////////
413
// Delete expired tasks.
414
void handleUntil ()
859✔
415
{
416
  Datetime now;
859✔
417
  auto tasks = Context::getContext ().tdb2.pending_tasks ();
859✔
418
  for (auto& t : tasks)
7,228✔
419
  {
420
    // TODO What about expiring template tasks?
421
    if (t.getStatus () == Task::pending &&
12,573✔
422
        t.has ("until"))
12,573✔
423
    {
424
      auto until = Datetime (t.get_date ("until"));
13✔
425
      if (until < now)
13✔
426
      {
427
        Context::getContext ().debug (format ("handleUntil: recurrence expired until {1} < now {2}", until.toISOLocalExtended (), now.toISOLocalExtended ()));
4✔
428
        t.setStatus (Task::deleted);
4✔
429
        Context::getContext ().tdb2.modify(t);
4✔
430
        Context::getContext ().footnote (onExpiration (t));
4✔
431
      }
432
    }
433
  }
434
}
859✔
435

436
////////////////////////////////////////////////////////////////////////////////
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