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

GothenburgBitFactory / taskwarrior / 11420355627

19 Oct 2024 08:00PM UTC coverage: 84.853% (+0.6%) from 84.223%
11420355627

push

github

web-flow
Pass rc.weekstart to libshared for ISO8601 weeknum parsing if "monday" (#3654)

* libshared: bump for weekstart, epoch defines, eopww fix

mainly those visible changes, and miscellaneous others

see GothenburgBitFactory/taskwarrior#3623 (weekstart)
see GothenburgBitFactory/taskwarrior#3651 (epoch limit defines)
see GothenburgBitFactory/libshared#73 (eopww fix)

* Initialize libshared's weekstart from user's rc.weekstart config

This enables use of newer libshared code that can parse week numbers
according to ISO8601 instead of existing code which is always using
Sunday-based weeks.  To get ISO behavior, set rc.weekstart=monday.
Default is still Sunday / old algorithm, as before, since Sunday is in
the hardcoded default rcfile.

Weekstart does not yet fix week-relative shortcuts, which will still
always use Monday.

See #3623 for further details.

4 of 6 new or added lines in 2 files covered. (66.67%)

993 existing lines in 25 files now uncovered.

19019 of 22414 relevant lines covered (84.85%)

23067.98 hits per line

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

65.63
/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;
909✔
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");
292✔
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.
114✔
90
          rec.set("parent", t.get("uuid"));  // Remember mom.
114✔
91
          rec.setAsNow("entry");             // New entry date.
114✔
92
          rec.set("due", format(d.toEpoch()));
114✔
93

94
          if (t.has("wait")) {
114✔
95
            Datetime old_wait(t.get_date("wait"));
1✔
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()));
1✔
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);
114✔
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"))
72✔
122
          Context::getContext().footnote(
116✔
123
              format("Creating recurring task instance '{1}'", t.get("description")));
116✔
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");
292✔
140

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

148
  auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
146✔
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");
6✔
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✔
UNCOV
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)
×
UNCOV
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)
×
UNCOV
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");
724✔
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);
29✔
332
    auto mask = parent.get("mask");
58✔
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")) {
6,442✔
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