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

GothenburgBitFactory / taskwarrior / 11335495770

14 Oct 2024 09:47PM UTC coverage: 84.223% (-0.6%) from 84.776%
11335495770

push

github

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

updates:
- [github.com/psf/black: 24.8.0 → 24.10.0](https://github.com/psf/black/compare/24.8.0...24.10.0)

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

19005 of 22565 relevant lines covered (84.22%)

23473.55 hits per line

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

66.32
/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 <sstream>
51

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

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

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

77
      // Get the mask from the parent task.
78
      auto mask = t.get("mask");
146✔
79

80
      // Iterate over the due dates, and check each against the mask.
81
      auto changed = false;
146✔
82
      unsigned int i = 0;
146✔
83
      for (auto& d : due) {
355✔
84
        if (mask.length() <= i) {
209✔
85
          changed = true;
114✔
86

87
          Task rec(t);                       // Clone the parent.
114✔
88
          rec.setStatus(Task::pending);      // Change the status.
114✔
89
          rec.set("uuid", uuid());           // New UUID.
342✔
90
          rec.set("parent", t.get("uuid"));  // Remember mom.
456✔
91
          rec.setAsNow("entry");             // New entry date.
114✔
92
          rec.set("due", format(d.toEpoch()));
342✔
93

94
          if (t.has("wait")) {
228✔
95
            Datetime old_wait(t.get_date("wait"));
2✔
96
            Datetime old_due(t.get_date("due"));
1✔
97
            Datetime due(d);
1✔
98
            rec.set("wait", format((due + (old_wait - old_due)).toEpoch()));
3✔
99
            rec.setStatus(Task::waiting);
1✔
100
            mask += 'W';
1✔
101
          } else {
102
            mask += '-';
113✔
103
            rec.setStatus(Task::pending);
113✔
104
          }
105

106
          rec.set("imask", i);
342✔
107
          rec.remove("mask");  // Remove the mask of the parent.
114✔
108

109
          // Add the new task to the DB.
110
          Context::getContext().tdb2.add(rec);
114✔
111
        }
114✔
112

113
        ++i;
209✔
114
      }
115

116
      // Only modify the parent if necessary.
117
      if (changed) {
146✔
118
        t.set("mask", mask);
72✔
119
        Context::getContext().tdb2.modify(t);
72✔
120

121
        if (Context::getContext().verbose("recur"))
216✔
122
          Context::getContext().footnote(
116✔
123
              format("Creating recurring task instance '{1}'", t.get("description")));
290✔
124
      }
125
    }
146✔
126
  }
127
}
907✔
128

129
////////////////////////////////////////////////////////////////////////////////
130
// Determine a start date (due), an optional end date (until), and an increment
131
// period (recur).  Then generate a set of corresponding dates.
132
//
133
// Returns false if the parent recurring task is depleted.
134
bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
146✔
135
  // Determine due date, recur period and until date.
136
  Datetime due(parent.get_date("due"));
146✔
137
  if (due._date == 0) return false;
146✔
138

139
  std::string recur = parent.get("recur");
146✔
140

141
  bool specificEnd = false;
146✔
142
  Datetime until;
146✔
143
  if (parent.get("until") != "") {
292✔
144
    until = Datetime(parent.get("until"));
15✔
145
    specificEnd = true;
5✔
146
  }
147

148
  auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
292✔
149
  int recurrence_counter = 0;
146✔
150
  Datetime now;
146✔
151
  for (Datetime i = due;; i = getNextRecurrence(i, recur)) {
209✔
152
    allDue.push_back(i);
209✔
153

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

161
      return true;
3✔
162
    }
3✔
163

164
    if (i > now) ++recurrence_counter;
206✔
165

166
    if (recurrence_counter >= recurrence_limit) return true;
206✔
167
  }
63✔
168

169
  return true;
170
}
146✔
171

172
////////////////////////////////////////////////////////////////////////////////
173
Datetime getNextRecurrence(Datetime& current, std::string& period) {
63✔
174
  auto m = current.month();
63✔
175
  auto d = current.day();
63✔
176
  auto y = current.year();
63✔
177
  auto ho = current.hour();
63✔
178
  auto mi = current.minute();
63✔
179
  auto se = current.second();
63✔
180

181
  // Some periods are difficult, because they can be vague.
182
  if (period == "monthly" || period == "P1M") {
63✔
183
    if (++m > 12) {
×
184
      m -= 12;
×
185
      ++y;
×
186
    }
187

188
    while (!Datetime::valid(y, m, d)) --d;
×
189

190
    return Datetime(y, m, d, ho, mi, se);
×
191
  }
192

193
  else if (period == "weekdays") {
63✔
194
    auto dow = current.dayOfWeek();
1✔
195
    int days;
196

197
    if (dow == 5)
1✔
198
      days = 3;
1✔
199
    else if (dow == 6)
×
200
      days = 2;
×
201
    else
202
      days = 1;
×
203

204
    return current + (days * 86400);
1✔
205
  }
206

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

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

214
    m += increment;
1✔
215
    while (m > 12) {
1✔
216
      m -= 12;
×
217
      ++y;
×
218
    }
219

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

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

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

229
    if (increment <= 0)
×
230
      throw format("Recurrence period '{1}' is equivalent to {2} and hence invalid.", period,
231
                   increment);
×
232

233
    m += increment;
×
234
    while (m > 12) {
×
235
      m -= 12;
×
236
      ++y;
×
237
    }
238

239
    while (!Datetime::valid(y, m, d)) --d;
×
240

241
    return Datetime(y, m, d);
×
242
  }
243

244
  else if (period == "quarterly" || period == "P3M") {
61✔
245
    m += 3;
×
246
    if (m > 12) {
×
247
      m -= 12;
×
248
      ++y;
×
249
    }
250

251
    while (!Datetime::valid(y, m, d)) --d;
×
252

253
    return Datetime(y, m, d, ho, mi, se);
×
254
  }
255

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

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

263
    m += 3 * increment;
×
264
    while (m > 12) {
×
265
      m -= 12;
×
266
      ++y;
×
267
    }
268

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

271
    return Datetime(y, m, d, ho, mi, se);
×
272
  }
273

274
  else if (period == "semiannual" || period == "P6M") {
61✔
275
    m += 6;
×
276
    if (m > 12) {
×
277
      m -= 12;
×
278
      ++y;
×
279
    }
280

281
    while (!Datetime::valid(y, m, d)) --d;
×
282

283
    return Datetime(y, m, d, ho, mi, se);
×
284
  }
285

286
  else if (period == "bimonthly" || period == "P2M") {
61✔
287
    m += 2;
×
288
    if (m > 12) {
×
289
      m -= 12;
×
290
      ++y;
×
291
    }
292

293
    while (!Datetime::valid(y, m, d)) --d;
×
294

295
    return Datetime(y, m, d, ho, mi, se);
×
296
  }
297

298
  else if (period == "biannual" || period == "biyearly" || period == "P2Y") {
61✔
299
    y += 2;
×
300

301
    return Datetime(y, m, d, ho, mi, se);
×
302
  }
303

304
  else if (period == "annual" || period == "yearly" || period == "P1Y") {
61✔
305
    y += 1;
16✔
306

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

311
    return Datetime(y, m, d, ho, mi, se);
16✔
312
  }
313

314
  // Add the period to current, and we're done.
315
  std::string::size_type idx = 0;
45✔
316
  Duration p;
45✔
317
  if (!p.parse(period, idx))
45✔
318
    throw std::string(format("The recurrence value '{1}' is not valid.", period));
×
319

320
  return current + p.toTime_t();
45✔
321
}
322

323
////////////////////////////////////////////////////////////////////////////////
324
// When the status of a recurring child task changes, the parent task must
325
// update it's mask.
326
void updateRecurrenceMask(Task& task) {
362✔
327
  auto uuid = task.get("parent");
362✔
328
  Task parent;
362✔
329

330
  if (uuid != "" && Context::getContext().tdb2.get(uuid, parent)) {
362✔
331
    unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10);
58✔
332
    auto mask = parent.get("mask");
29✔
333
    if (mask.length() > index) {
29✔
334
      mask[index] = (task.getStatus() == Task::pending)     ? '-'
47✔
335
                    : (task.getStatus() == Task::completed) ? '+'
36✔
336
                    : (task.getStatus() == Task::deleted)   ? 'X'
18✔
337
                    : (task.getStatus() == Task::waiting)   ? 'W'
×
338
                                                            : '?';
339
    } else {
340
      std::string mask;
×
341
      for (unsigned int i = 0; i < index; ++i) mask += "?";
×
342

343
      mask += (task.getStatus() == Task::pending)     ? '-'
×
344
              : (task.getStatus() == Task::completed) ? '+'
×
345
              : (task.getStatus() == Task::deleted)   ? 'X'
×
346
              : (task.getStatus() == Task::waiting)   ? 'W'
×
347
                                                      : '?';
×
348
    }
×
349

350
    parent.set("mask", mask);
29✔
351
    Context::getContext().tdb2.modify(parent);
29✔
352
  }
29✔
353
}
362✔
354

355
////////////////////////////////////////////////////////////////////////////////
356
// Delete expired tasks.
357
void handleUntil() {
909✔
358
  Datetime now;
909✔
359
  auto tasks = Context::getContext().tdb2.pending_tasks();
909✔
360
  for (auto& t : tasks) {
7,351✔
361
    // TODO What about expiring template tasks?
362
    if (t.getStatus() == Task::pending && t.has("until")) {
18,984✔
363
      auto until = Datetime(t.get_date("until"));
13✔
364
      if (until < now) {
13✔
365
        Context::getContext().debug(format("handleUntil: recurrence expired until {1} < now {2}",
8✔
366
                                           until.toISOLocalExtended(), now.toISOLocalExtended()));
8✔
367
        t.setStatus(Task::deleted);
4✔
368
        Context::getContext().tdb2.modify(t);
4✔
369
        Context::getContext().footnote(onExpiration(t));
4✔
370
      }
371
    }
372
  }
373
}
909✔
374

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