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

GothenburgBitFactory / taskwarrior / 12358478612

16 Dec 2024 05:59PM UTC coverage: 84.898% (-0.6%) from 85.522%
12358478612

push

github

web-flow
[pre-commit.ci] pre-commit autoupdate (#3725)

updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.4 → v19.1.5](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.4...v19.1.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

19276 of 22705 relevant lines covered (84.9%)

23265.72 hits per line

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

68.47
/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
// cmake.h include header must come first
29

30
#include <Context.h>
31
#include <Datetime.h>
32
#include <Duration.h>
33
#include <Lexer.h>
34
#include <format.h>
35
#include <inttypes.h>
36
#include <main.h>
37
#include <pwd.h>
38
#include <stdio.h>
39
#include <stdlib.h>
40
#include <string.h>
41
#include <sys/types.h>
42
#include <time.h>
43
#include <unicode.h>
44
#include <unistd.h>
45
#include <util.h>
46

47
#include <fstream>
48
#include <iomanip>
49
#include <iostream>
50
#include <limits>
51
#include <optional>
52
#include <sstream>
53

54
// Add a `time_t` delta to a Datetime, checking for and returning nullopt on integer overflow.
55
std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta) {
50✔
56
  // Datetime::operator+ takes an integer delta, so check that range
57
  if (static_cast<time_t>(std::numeric_limits<int>::max()) < delta) {
50✔
58
    return std::nullopt;
1✔
59
  }
60

61
  // Check for time_t overflow in the Datetime.
62
  if (std::numeric_limits<time_t>::max() - base.toEpoch() < delta) {
49✔
63
    return std::nullopt;
1✔
64
  }
65
  return base + delta;
48✔
66
}
67

68
////////////////////////////////////////////////////////////////////////////////
69
// Scans all tasks, and for any recurring tasks, determines whether any new
70
// child tasks need to be generated to fill gaps.
71
void handleRecurrence() {
909✔
72
  // Recurrence can be disabled.
73
  // Note: This is currently a workaround for TD-44, TW-1520.
74
  if (!Context::getContext().config.getBoolean("recurrence")) return;
2,727✔
75

76
  auto tasks = Context::getContext().tdb2.pending_tasks();
907✔
77
  Datetime now;
907✔
78

79
  // Look at all tasks and find any recurring ones.
80
  for (auto& t : tasks) {
7,359✔
81
    if (t.getStatus() == Task::recurring) {
6,452✔
82
      // Generate a list of due dates for this recurring task, regardless of
83
      // the mask.
84
      std::vector<Datetime> due;
146✔
85
      if (!generateDueDates(t, due)) {
146✔
86
        // Determine the end date.
87
        t.setStatus(Task::deleted);
×
88
        Context::getContext().tdb2.modify(t);
×
89
        Context::getContext().footnote(onExpiration(t));
×
90
        continue;
×
91
      }
92

93
      // Get the mask from the parent task.
94
      auto mask = t.get("mask");
146✔
95

96
      // Iterate over the due dates, and check each against the mask.
97
      auto changed = false;
146✔
98
      unsigned int i = 0;
146✔
99
      for (auto& d : due) {
355✔
100
        if (mask.length() <= i) {
209✔
101
          changed = true;
114✔
102

103
          Task rec(t);                       // Clone the parent.
114✔
104
          rec.setStatus(Task::pending);      // Change the status.
114✔
105
          rec.set("uuid", uuid());           // New UUID.
342✔
106
          rec.set("parent", t.get("uuid"));  // Remember mom.
456✔
107
          rec.setAsNow("entry");             // New entry date.
114✔
108
          rec.set("due", format(d.toEpoch()));
342✔
109

110
          if (t.has("wait")) {
228✔
111
            Datetime old_wait(t.get_date("wait"));
2✔
112
            Datetime old_due(t.get_date("due"));
1✔
113
            Datetime due(d);
1✔
114
            auto wait = checked_add_datetime(due, old_wait - old_due);
1✔
115
            if (wait) {
1✔
116
              rec.set("wait", format(wait->toEpoch()));
3✔
117
            } else {
118
              rec.remove("wait");
×
119
            }
120
            rec.setStatus(Task::waiting);
1✔
121
            mask += 'W';
1✔
122
          } else {
123
            mask += '-';
113✔
124
            rec.setStatus(Task::pending);
113✔
125
          }
126

127
          rec.set("imask", i);
342✔
128
          rec.remove("mask");  // Remove the mask of the parent.
114✔
129

130
          // Add the new task to the DB.
131
          Context::getContext().tdb2.add(rec);
114✔
132
        }
114✔
133

134
        ++i;
209✔
135
      }
136

137
      // Only modify the parent if necessary.
138
      if (changed) {
146✔
139
        t.set("mask", mask);
72✔
140
        Context::getContext().tdb2.modify(t);
72✔
141

142
        if (Context::getContext().verbose("recur"))
216✔
143
          Context::getContext().footnote(
116✔
144
              format("Creating recurring task instance '{1}'", t.get("description")));
290✔
145
      }
146
    }
146✔
147
  }
148
}
907✔
149

150
////////////////////////////////////////////////////////////////////////////////
151
// Determine a start date (due), an optional end date (until), and an increment
152
// period (recur).  Then generate a set of corresponding dates.
153
//
154
// Returns false if the parent recurring task is depleted.
155
bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
146✔
156
  // Determine due date, recur period and until date.
157
  Datetime due(parent.get_date("due"));
146✔
158
  if (due._date == 0) return false;
146✔
159

160
  std::string recur = parent.get("recur");
146✔
161

162
  bool specificEnd = false;
146✔
163
  Datetime until;
146✔
164
  if (parent.get("until") != "") {
292✔
165
    until = Datetime(parent.get("until"));
15✔
166
    specificEnd = true;
5✔
167
  }
168

169
  auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
292✔
170
  int recurrence_counter = 0;
146✔
171
  Datetime now;
146✔
172
  Datetime i = due;
146✔
173
  while (1) {
174
    allDue.push_back(i);
209✔
175

176
    if (specificEnd && i > until) {
209✔
177
      // If i > until, it means there are no more tasks to generate, and if the
178
      // parent mask contains all + or X, then there never will be another task
179
      // to generate, and this parent task may be safely reaped.
180
      auto mask = parent.get("mask");
3✔
181
      if (mask.length() == allDue.size() && mask.find('-') == std::string::npos) return false;
3✔
182

183
      return true;
3✔
184
    }
3✔
185

186
    if (i > now) ++recurrence_counter;
206✔
187

188
    if (recurrence_counter >= recurrence_limit) return true;
206✔
189
    auto next = getNextRecurrence(i, recur);
63✔
190
    if (next) {
63✔
191
      i = *next;
63✔
192
    } else {
193
      return true;
×
194
    }
195
  }
63✔
196

197
  return true;
198
}
146✔
199

200
////////////////////////////////////////////////////////////////////////////////
201
/// Determine the next recurrence of the given period.
202
///
203
/// If no such date can be calculated, such as with a very large period, returns
204
/// nullopt.
205
std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period) {
63✔
206
  auto m = current.month();
63✔
207
  auto d = current.day();
63✔
208
  auto y = current.year();
63✔
209
  auto ho = current.hour();
63✔
210
  auto mi = current.minute();
63✔
211
  auto se = current.second();
63✔
212

213
  // Some periods are difficult, because they can be vague.
214
  if (period == "monthly" || period == "P1M") {
63✔
215
    if (++m > 12) {
×
216
      m -= 12;
×
217
      ++y;
×
218
    }
219

220
    while (!Datetime::valid(y, m, d)) --d;
×
221

222
    return Datetime(y, m, d, ho, mi, se);
×
223
  }
224

225
  else if (period == "weekdays") {
63✔
226
    auto dow = current.dayOfWeek();
1✔
227
    int days;
228

229
    if (dow == 5)
1✔
230
      days = 3;
1✔
231
    else if (dow == 6)
×
232
      days = 2;
×
233
    else
234
      days = 1;
×
235

236
    return checked_add_datetime(current, days * 86400);
1✔
237
  }
238

239
  else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'm') {
62✔
240
    int increment = strtol(period.substr(0, period.length() - 1).c_str(), nullptr, 10);
1✔
241

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

246
    m += increment;
1✔
247
    while (m > 12) {
2✔
248
      m -= 12;
1✔
249
      ++y;
1✔
250
    }
251

252
    while (!Datetime::valid(y, m, d)) --d;
1✔
253

254
    return Datetime(y, m, d, ho, mi, se);
1✔
255
  }
256

257
  else if (period[0] == 'P' && Lexer::isAllDigits(period.substr(1, period.length() - 2)) &&
61✔
258
           period[period.length() - 1] == 'M') {
×
259
    int increment = strtol(period.substr(1, period.length() - 2).c_str(), nullptr, 10);
×
260

261
    if (increment <= 0)
×
262
      throw format("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period,
263
                   increment);
×
264

265
    m += increment;
×
266
    while (m > 12) {
×
267
      m -= 12;
×
268
      ++y;
×
269
    }
270

271
    while (!Datetime::valid(y, m, d)) --d;
×
272

273
    return Datetime(y, m, d);
×
274
  }
275

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

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

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

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

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

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

301
    while (!Datetime::valid(y, m, d)) --d;
×
302

303
    return Datetime(y, m, d, ho, mi, se);
×
304
  }
305

306
  else if (period == "semiannual" || period == "P6M") {
61✔
307
    m += 6;
×
308
    if (m > 12) {
×
309
      m -= 12;
×
310
      ++y;
×
311
    }
312

313
    while (!Datetime::valid(y, m, d)) --d;
×
314

315
    return Datetime(y, m, d, ho, mi, se);
×
316
  }
317

318
  else if (period == "bimonthly" || period == "P2M") {
61✔
319
    m += 2;
×
320
    if (m > 12) {
×
321
      m -= 12;
×
322
      ++y;
×
323
    }
324

325
    while (!Datetime::valid(y, m, d)) --d;
×
326

327
    return Datetime(y, m, d, ho, mi, se);
×
328
  }
329

330
  else if (period == "biannual" || period == "biyearly" || period == "P2Y") {
61✔
331
    y += 2;
×
332

333
    return Datetime(y, m, d, ho, mi, se);
×
334
  }
335

336
  else if (period == "annual" || period == "yearly" || period == "P1Y") {
61✔
337
    y += 1;
16✔
338

339
    // If the due data just happens to be 2/29 in a leap year, then simply
340
    // incrementing y is going to create an invalid date.
341
    if (m == 2 && d == 29) d = 28;
16✔
342

343
    return Datetime(y, m, d, ho, mi, se);
16✔
344
  }
345

346
  // Add the period to current, and we're done.
347
  std::string::size_type idx = 0;
45✔
348
  Duration p;
45✔
349
  if (!p.parse(period, idx))
45✔
350
    throw std::string(format("The recurrence value '{1}' is not valid.", period));
×
351

352
  return checked_add_datetime(current, p.toTime_t());
45✔
353
}
354

355
////////////////////////////////////////////////////////////////////////////////
356
// When the status of a recurring child task changes, the parent task must
357
// update it's mask.
358
void updateRecurrenceMask(Task& task) {
362✔
359
  auto uuid = task.get("parent");
362✔
360
  Task parent;
362✔
361

362
  if (uuid != "" && Context::getContext().tdb2.get(uuid, parent)) {
362✔
363
    unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10);
58✔
364
    auto mask = parent.get("mask");
29✔
365
    if (mask.length() > index) {
29✔
366
      mask[index] = (task.getStatus() == Task::pending)     ? '-'
47✔
367
                    : (task.getStatus() == Task::completed) ? '+'
36✔
368
                    : (task.getStatus() == Task::deleted)   ? 'X'
18✔
369
                    : (task.getStatus() == Task::waiting)   ? 'W'
×
370
                                                            : '?';
371
    } else {
372
      std::string mask;
×
373
      for (unsigned int i = 0; i < index; ++i) mask += "?";
×
374

375
      mask += (task.getStatus() == Task::pending)     ? '-'
×
376
              : (task.getStatus() == Task::completed) ? '+'
×
377
              : (task.getStatus() == Task::deleted)   ? 'X'
×
378
              : (task.getStatus() == Task::waiting)   ? 'W'
×
379
                                                      : '?';
×
380
    }
×
381

382
    parent.set("mask", mask);
29✔
383
    Context::getContext().tdb2.modify(parent);
29✔
384
  }
29✔
385
}
362✔
386

387
////////////////////////////////////////////////////////////////////////////////
388
// Delete expired tasks.
389
void handleUntil() {
909✔
390
  Datetime now;
909✔
391
  auto tasks = Context::getContext().tdb2.pending_tasks();
909✔
392
  for (auto& t : tasks) {
7,363✔
393
    // TODO What about expiring template tasks?
394
    if (t.getStatus() == Task::pending && t.has("until")) {
19,020✔
395
      auto until = Datetime(t.get_date("until"));
13✔
396
      if (until < now) {
13✔
397
        Context::getContext().debug(format("handleUntil: recurrence expired until {1} < now {2}",
8✔
398
                                           until.toISOLocalExtended(), now.toISOLocalExtended()));
8✔
399
        t.setStatus(Task::deleted);
4✔
400
        Context::getContext().tdb2.modify(t);
4✔
401
        Context::getContext().footnote(onExpiration(t));
4✔
402
      }
403
    }
404
  }
405
}
909✔
406

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

© 2025 Coveralls, Inc