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

GothenburgBitFactory / taskwarrior / 14912062652

08 May 2025 05:08PM UTC coverage: 85.189% (-0.06%) from 85.246%
14912062652

push

github

web-flow
Fix compiler warning about unused variable (#3873)

This was added to indicate that the return value of chdir was unused,
but newer compilers "see through" this and determine it to be unused.
The return value is not marked must-use, so just doing nothing with it
is sufficient.

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

21 existing lines in 2 files now uncovered.

19567 of 22969 relevant lines covered (85.19%)

23443.04 hits per line

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

90.14
/src/commands/CmdBurndown.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 <CmdBurndown.h>
31
#include <Context.h>
32
#include <Datetime.h>
33
#include <Duration.h>
34
#include <Filter.h>
35
#include <format.h>
36
#include <math.h>
37
#include <shared.h>
38
#include <string.h>
39

40
#include <algorithm>
41
#include <limits>
42
#include <map>
43
#include <sstream>
44

45
// Helper macro.
46
#define LOC(y, x) (((y) * (_width + 1)) + (x))
47

48
////////////////////////////////////////////////////////////////////////////////
49
class Bar {
50
 public:
51
  Bar() = default;
770✔
52
  Bar(const Bar&);
53
  Bar& operator=(const Bar&);
54
  ~Bar() = default;
448✔
55

56
 public:
57
  int _offset{0};                // from left of chart
58
  std::string _major_label{""};  // x-axis label, major (year/-/month)
59
  std::string _minor_label{""};  // x-axis label, minor (month/week/day)
60
  int _pending{0};               // Number of pending tasks in period
61
  int _started{0};               // Number of started tasks in period
62
  int _done{0};                  // Number of done tasks in period
63
  int _added{0};                 // Number added in period
64
  int _removed{0};               // Number removed in period
65
};
66

67
////////////////////////////////////////////////////////////////////////////////
68
Bar::Bar(const Bar& other) { *this = other; }
1,470✔
69

70
////////////////////////////////////////////////////////////////////////////////
71
Bar& Bar::operator=(const Bar& other) {
441✔
72
  if (this != &other) {
441✔
73
    _offset = other._offset;
441✔
74
    _major_label = other._major_label;
441✔
75
    _minor_label = other._minor_label;
441✔
76
    _pending = other._pending;
441✔
77
    _started = other._started;
441✔
78
    _done = other._done;
441✔
79
    _added = other._added;
441✔
80
    _removed = other._removed;
441✔
81
  }
82

83
  return *this;
441✔
84
}
85

86
////////////////////////////////////////////////////////////////////////////////
87
// Data gathering algorithm:
88
//
89
//   e = entry
90
//   s = start
91
//   C = end/Completed
92
//   D = end/Deleted
93
//   > = Pending/Waiting
94
//
95
//   ID  30 31 01 02 03 04 05 06 07 08 09 10
96
//   --  ------------------------------------
97
//   1          e-----s--C
98
//   2             e--s-----D
99
//   3                e-----s-------------->
100
//   4                   e----------------->
101
//   5                               e----->
102
//   --  ------------------------------------
103
//   PP         1  2  3  3  2  2  2  3  3  3
104
//   SS               2  1  1  1  1  1  1  1
105
//   DD                  1  1  1  1  1  1  1
106
//   --  ------------------------------------
107
//
108
//   5 |             SS DD          DD DD DD
109
//   4 |             SS SS DD DD DD SS SS SS
110
//   3 |             PP PP SS SS SS PP PP PP
111
//   2 |          PP PP PP PP PP PP PP PP PP
112
//   1 |       PP PP PP PP PP PP PP PP PP PP
113
//   0 +-------------------------------------
114
//       30 31 01 02 03 04 05 06 07 08 09 10
115
//       Oct   Nov
116
//
117
class Chart {
118
 public:
119
  Chart(char);
120
  Chart(const Chart&);             // Unimplemented
121
  Chart& operator=(const Chart&);  // Unimplemented
122
  ~Chart() = default;
7✔
123

124
  void scan(std::vector<Task>&);
125
  void scanForPeak(std::vector<Task>&);
126
  std::string render();
127

128
 private:
129
  void generateBars();
130
  void optimizeGrid();
131
  Datetime quantize(const Datetime&, char);
132

133
  Datetime increment(const Datetime&, char);
134
  Datetime decrement(const Datetime&, char);
135
  void maxima();
136
  void yLabels(std::vector<int>&);
137
  void calculateRates();
138
  unsigned round_up_to(unsigned, unsigned);
139
  unsigned burndown_size(unsigned);
140

141
 public:
142
  int _width{};                   // Terminal width
143
  int _height{};                  // Terminal height
144
  int _graph_width{};             // Width of plot area
145
  int _graph_height{};            // Height of plot area
146
  int _max_value{0};              // Largest combined bar value
147
  int _max_label{1};              // Longest y-axis label
148
  std::vector<int> _labels{};     // Y-axis labels
149
  int _estimated_bars{};          // Estimated bar count
150
  int _actual_bars{0};            // Calculated bar count
151
  std::map<time_t, Bar> _bars{};  // Epoch-indexed set of bars
152
  Datetime _earliest{};           // Date of earliest estimated bar
153
  int _carryover_done{0};         // Number of 'done' tasks prior to chart range
154
  char _period{};                 // D, W, M
155
  std::string _grid{};            // String representing grid of characters
156
  time_t _peak_epoch{};           // Quantized (D) date of highest pending peak
157
  int _peak_count{0};             // Corresponding peak pending count
158
  int _current_count{0};          // Current pending count
159
  float _net_fix_rate{0.0};       // Calculated fix rate
160
  std::string _completion{};      // Estimated completion date
161
};
162

163
////////////////////////////////////////////////////////////////////////////////
164
Chart::Chart(char type) {
7✔
165
  // How much space is there to render in?  This chart will occupy the
166
  // maximum space, and the width drives various other parameters.
167
  _width = Context::getContext().getWidth();
7✔
168
  _height = Context::getContext().getHeight() -
7✔
169
            Context::getContext().config.getInteger("reserved.lines") -
14✔
170
            1;  // Allow for new line with prompt.
171
  _graph_height = _height - 7;
7✔
172
  _graph_width = _width - _max_label - 14;
7✔
173

174
  // Estimate how many 'bars' can be dsplayed.  This will help subset a
175
  // potentially enormous data set.
176
  _estimated_bars = (_width - 1 - 14) / 3;
7✔
177

178
  _period = type;
7✔
179
}
7✔
180

181
////////////////////////////////////////////////////////////////////////////////
182
// Scan all tasks, quantize the dates by day, and find the peak pending count
183
// and corresponding epoch.
184
void Chart::scanForPeak(std::vector<Task>& tasks) {
7✔
185
  std::map<time_t, int> pending;
7✔
186
  _current_count = 0;
7✔
187

188
  for (auto& task : tasks) {
39✔
189
    // The entry date is when the counting starts.
190
    Datetime entry(task.get_date("entry"));
32✔
191

192
    Datetime end;
32✔
193
    if (task.has("end"))
64✔
194
      end = Datetime(task.get_date("end"));
40✔
195
    else
196
      ++_current_count;
12✔
197

198
    while (entry < end) {
6,162✔
199
      time_t epoch = quantize(entry.toEpoch(), 'D').toEpoch();
6,130✔
200
      if (pending.find(epoch) != pending.end())
6,130✔
201
        ++pending[epoch];
2,475✔
202
      else
203
        pending[epoch] = 1;
3,655✔
204

205
      entry = increment(entry, 'D');
6,130✔
206
    }
207
  }
208

209
  // Find the peak and peak date.
210
  for (auto& count : pending) {
3,662✔
211
    if (count.second > _peak_count) {
3,655✔
212
      _peak_count = count.second;
20✔
213
      _peak_epoch = count.first;
20✔
214
    }
215
  }
216
}
7✔
217

218
////////////////////////////////////////////////////////////////////////////////
219
void Chart::scan(std::vector<Task>& tasks) {
7✔
220
  generateBars();
7✔
221

222
  // Not quantized, so that "while (xxx < now)" is inclusive.
223
  Datetime now;
7✔
224

225
  time_t epoch;
226
  auto& config = Context::getContext().config;
7✔
227
  bool cumulative;
228
  if (config.has("burndown.cumulative")) {
14✔
229
    cumulative = config.getBoolean("burndown.cumulative");
6✔
230
  } else {
231
    cumulative = true;
4✔
232
  }
233

234
  for (auto& task : tasks) {
39✔
235
    // The entry date is when the counting starts.
236
    Datetime from = quantize(Datetime(task.get_date("entry")), _period);
64✔
237
    epoch = from.toEpoch();
32✔
238

239
    if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._added;
32✔
240

241
    // e-->   e--s-->
242
    // ppp>   pppsss>
243
    Task::status status = task.getStatus();
32✔
244
    if (status == Task::pending || status == Task::waiting) {
32✔
245
      if (task.has("start")) {
24✔
246
        Datetime start = quantize(Datetime(task.get_date("start")), _period);
10✔
247
        while (from < start) {
1,164✔
248
          epoch = from.toEpoch();
1,159✔
249
          if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._pending;
1,159✔
250
          from = increment(from, _period);
1,159✔
251
        }
252

253
        while (from < now) {
10✔
254
          epoch = from.toEpoch();
5✔
255
          if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._started;
5✔
256
          from = increment(from, _period);
5✔
257
        }
258
      } else {
259
        while (from < now) {
2,332✔
260
          epoch = from.toEpoch();
2,325✔
261
          if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._pending;
2,325✔
262
          from = increment(from, _period);
2,325✔
263
        }
264
      }
265
    }
12✔
266

267
    // e--C   e--s--C
268
    // pppd>  pppsssd>
269
    else if (status == Task::completed) {
20✔
270
      // Truncate history so it starts at 'earliest' for completed tasks.
271
      Datetime end = quantize(Datetime(task.get_date("end")), _period);
30✔
272
      epoch = end.toEpoch();
15✔
273

274
      if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._removed;
15✔
275

276
      while (from < end) {
15✔
277
        epoch = from.toEpoch();
×
278
        if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._pending;
×
279
        from = increment(from, _period);
×
280
      }
281

282
      if (cumulative) {
15✔
283
        while (from < now) {
12✔
284
          epoch = from.toEpoch();
6✔
285
          if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._done;
6✔
286
          from = increment(from, _period);
6✔
287
        }
288

289
        // Maintain a running total of 'done' tasks that are off the left of the
290
        // chart.
291
        if (end < _earliest) {
6✔
292
          ++_carryover_done;
×
293
          continue;
×
294
        }
295
      }
296

297
      else {
298
        epoch = from.toEpoch();
9✔
299
        if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._done;
9✔
300
      }
301
    }
302
  }
303

304
  // Size the data.
305
  maxima();
7✔
306
}
7✔
307

308
////////////////////////////////////////////////////////////////////////////////
309
// Graph should render like this:
310
//   +---------------------------------------------------------------------+
311
//   |                                                                     |
312
//   | 20 |                                                                |
313
//   |    |                            DD DD DD DD DD DD DD DD             |
314
//   |    |          DD DD DD DD DD DD DD DD DD DD DD DD DD DD             |
315
//   |    | PP PP SS SS SS SS SS SS SS SS SS DD DD DD DD DD DD   DD Done   |
316
//   | 10 | PP PP PP PP PP PP SS SS SS SS SS SS DD DD DD DD DD   SS Started|
317
//   |    | PP PP PP PP PP PP PP PP PP PP PP SS SS SS SS DD DD   PP Pending|
318
//   |    | PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP SS DD             |
319
//   |    | PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP             |
320
//   |  0 +----------------------------------------------------            |
321
//   |      21 22 23 24 25 26 27 28 29 30 31 01 02 03 04 05 06             |
322
//   |      July                             August                        |
323
//   |                                                                     |
324
//   |      ADD rate 1.7/d           Estimated completion 8/12/2010        |
325
//   |      Don/Delete rate  1.3/d                                         |
326
//   +---------------------------------------------------------------------+
327
std::string Chart::render() {
7✔
328
  if (_graph_height < 5 ||  // a 4-line graph is essentially unreadable.
7✔
329
      _graph_width < 2)     // A single-bar graph is useless.
7✔
330
  {
331
    return std::string("Terminal window too small to draw a graph.\n");
×
332
  }
333

334
  else if (_graph_height > 1000 ||  // each line is a string allloc
7✔
335
           _graph_width > 1000) {
7✔
336
    return std::string("Terminal window too large to draw a graph.\n");
×
337
  }
338

339
  if (_max_value == 0) Context::getContext().footnote("No matches.");
7✔
340

341
  // Create a grid, folded into a string.
342
  _grid = "";
7✔
343
  for (int i = 0; i < _height; ++i) _grid += std::string(_width, ' ') + '\n';
315✔
344

345
  // Title.
346
  std::string title = _period == 'D' ? "Daily" : _period == 'W' ? "Weekly" : "Monthly";
14✔
347
  title += std::string(" Burndown");
7✔
348
  _grid.replace(LOC(0, (_width - title.length()) / 2), title.length(), title);
7✔
349

350
  // Legend.
351
  _grid.replace(LOC(_graph_height / 2 - 1, _width - 10), 10, "DD " + leftJustify("Done", 7));
14✔
352
  _grid.replace(LOC(_graph_height / 2, _width - 10), 10, "SS " + leftJustify("Started", 7));
14✔
353
  _grid.replace(LOC(_graph_height / 2 + 1, _width - 10), 10, "PP " + leftJustify("Pending", 7));
14✔
354

355
  // Determine y-axis labelling.
356
  std::vector<int> _labels;
7✔
357
  yLabels(_labels);
7✔
358
  _max_label = (int)log10((double)_labels[2]) + 1;
7✔
359

360
  // Draw y-axis.
361
  for (int i = 0; i < _graph_height; ++i) _grid.replace(LOC(i + 1, _max_label + 1), 1, "|");
112✔
362

363
  // Draw y-axis labels.
364
  char label[12];
365
  snprintf(label, 12, "%*d", _max_label, _labels[2]);
7✔
366
  _grid.replace(LOC(1, _max_label - strlen(label)), strlen(label), label);
7✔
367
  snprintf(label, 12, "%*d", _max_label, _labels[1]);
7✔
368
  _grid.replace(LOC(1 + (_graph_height / 2), _max_label - strlen(label)), strlen(label), label);
7✔
369
  _grid.replace(LOC(_graph_height + 1, _max_label - 1), 1, "0");
7✔
370

371
  // Draw x-axis.
372
  _grid.replace(LOC(_height - 6, _max_label + 1), 1, "+");
7✔
373
  _grid.replace(LOC(_height - 6, _max_label + 2), _graph_width, std::string(_graph_width, '-'));
14✔
374

375
  // Draw x-axis labels.
376
  std::vector<time_t> bars_in_sequence;
7✔
377
  for (auto& bar : _bars) bars_in_sequence.push_back(bar.first);
154✔
378

379
  std::sort(bars_in_sequence.begin(), bars_in_sequence.end());
7✔
380
  std::string _major_label;
7✔
381
  for (auto& seq : bars_in_sequence) {
154✔
382
    Bar bar = _bars[seq];
147✔
383

384
    // If it fits within the allowed space.
385
    if (bar._offset < _actual_bars) {
147✔
386
      _grid.replace(LOC(_height - 5, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)),
147✔
387
                    bar._minor_label.length(), bar._minor_label);
388

389
      if (_major_label != bar._major_label)
147✔
390
        _grid.replace(LOC(_height - 4, _max_label + 2 + ((_actual_bars - bar._offset - 1) * 3)),
15✔
391
                      bar._major_label.length(), ' ' + bar._major_label);
30✔
392

393
      _major_label = bar._major_label;
147✔
394
    }
395
  }
147✔
396

397
  // Draw bars.
398
  for (auto& seq : bars_in_sequence) {
154✔
399
    Bar bar = _bars[seq];
147✔
400

401
    // If it fits within the allowed space.
402
    if (bar._offset < _actual_bars) {
147✔
403
      int pending = (bar._pending * _graph_height) / _labels[2];
147✔
404
      int started = ((bar._pending + bar._started) * _graph_height) / _labels[2];
147✔
405
      int done = ((bar._pending + bar._started + bar._done + _carryover_done) * _graph_height) /
294✔
406
                 _labels[2];
147✔
407

408
      for (int b = 0; b < pending; ++b)
647✔
409
        _grid.replace(
500✔
410
            LOC(_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2,
500✔
411
            "PP");
412

413
      for (int b = pending; b < started; ++b)
162✔
414
        _grid.replace(
15✔
415
            LOC(_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2,
15✔
416
            "SS");
417

418
      for (int b = started; b < done; ++b)
182✔
419
        _grid.replace(
35✔
420
            LOC(_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2,
35✔
421
            "DD");
422
    }
423
  }
147✔
424

425
  // Draw rates.
426
  calculateRates();
7✔
427
  char rate[12];
428
  if (_net_fix_rate != 0.0)
7✔
UNCOV
429
    snprintf(rate, 12, "%.1f/d", _net_fix_rate);
×
430
  else
431
    strcpy(rate, "-");
7✔
432

433
  _grid.replace(LOC(_height - 2, _max_label + 3), 22 + strlen(rate),
14✔
434
                std::string("Net Fix Rate:         ") + rate);
14✔
435

436
  // Draw completion date.
437
  if (_completion.length())
7✔
438
    _grid.replace(LOC(_height - 1, _max_label + 3), 22 + _completion.length(),
7✔
439
                  "Estimated completion: " + _completion);
14✔
440

441
  optimizeGrid();
7✔
442

443
  if (Context::getContext().color()) {
7✔
444
    // Colorize the grid.
445
    Color color_pending(Context::getContext().config.get("color.burndown.pending"));
2✔
446
    Color color_done(Context::getContext().config.get("color.burndown.done"));
2✔
447
    Color color_started(Context::getContext().config.get("color.burndown.started"));
2✔
448

449
    // Replace DD, SS, PP with colored strings.
450
    std::string::size_type i;
451
    while ((i = _grid.find("PP")) != std::string::npos)
104✔
452
      _grid.replace(i, 2, color_pending.colorize("  "));
309✔
453

454
    while ((i = _grid.find("SS")) != std::string::npos)
5✔
455
      _grid.replace(i, 2, color_started.colorize("  "));
12✔
456

457
    while ((i = _grid.find("DD")) != std::string::npos)
9✔
458
      _grid.replace(i, 2, color_done.colorize("  "));
24✔
459
  } else {
460
    // Replace DD, SS, PP with ./+/X strings.
461
    std::string::size_type i;
462
    while ((i = _grid.find("PP")) != std::string::npos) _grid.replace(i, 2, " X");
410✔
463

464
    while ((i = _grid.find("SS")) != std::string::npos) _grid.replace(i, 2, " +");
24✔
465

466
    while ((i = _grid.find("DD")) != std::string::npos) _grid.replace(i, 2, " .");
40✔
467
  }
468

469
  return _grid;
7✔
470
}
7✔
471

472
////////////////////////////////////////////////////////////////////////////////
473
// _grid =~ /\s+$//g
474
void Chart::optimizeGrid() {
7✔
475
  std::string::size_type ws;
476
  while ((ws = _grid.find(" \n")) != std::string::npos) {
147✔
477
    auto non_ws = ws;
140✔
478
    while (_grid[non_ws] == ' ') --non_ws;
4,895✔
479

480
    _grid.replace(non_ws + 1, ws - non_ws + 1, "\n");
140✔
481
  }
482
}
7✔
483

484
////////////////////////////////////////////////////////////////////////////////
485
Datetime Chart::quantize(const Datetime& input, char period) {
6,182✔
486
  if (period == 'D') return input.startOfDay();
6,182✔
487
  if (period == 'W') return input.startOfWeek();
22✔
488
  if (period == 'M') return input.startOfMonth();
10✔
489

490
  return input;
×
491
}
492

493
////////////////////////////////////////////////////////////////////////////////
494
Datetime Chart::increment(const Datetime& input, char period) {
9,625✔
495
  // Move to the next period.
496
  int d = input.day();
9,625✔
497
  int m = input.month();
9,625✔
498
  int y = input.year();
9,625✔
499

500
  int days;
501

502
  switch (period) {
9,625✔
503
    case 'D':
9,427✔
504
      if (++d > Datetime::daysInMonth(y, m)) {
9,427✔
505
        d = 1;
308✔
506

507
        if (++m == 13) {
308✔
508
          m = 1;
24✔
509
          ++y;
24✔
510
        }
511
      }
512
      break;
9,427✔
513

514
    case 'W':
160✔
515
      d += 7;
160✔
516
      days = Datetime::daysInMonth(y, m);
160✔
517
      if (d > days) {
160✔
518
        d -= days;
36✔
519

520
        if (++m == 13) {
36✔
521
          m = 1;
3✔
522
          ++y;
3✔
523
        }
524
      }
525
      break;
160✔
526

527
    case 'M':
38✔
528
      d = 1;
38✔
529
      if (++m == 13) {
38✔
530
        m = 1;
3✔
531
        ++y;
3✔
532
      }
533
      break;
38✔
534
  }
535

536
  return Datetime(y, m, d, 0, 0, 0);
9,625✔
537
}
538

539
////////////////////////////////////////////////////////////////////////////////
540
Datetime Chart::decrement(const Datetime& input, char period) {
147✔
541
  // Move to the previous period.
542
  int d = input.day();
147✔
543
  int m = input.month();
147✔
544
  int y = input.year();
147✔
545

546
  switch (period) {
147✔
547
    case 'D':
63✔
548
      if (--d == 0) {
63✔
549
        if (--m == 0) {
3✔
550
          m = 12;
×
551
          --y;
×
552
        }
553

554
        d = Datetime::daysInMonth(y, m);
3✔
555
      }
556
      break;
63✔
557

558
    case 'W':
63✔
559
      d -= 7;
63✔
560
      if (d < 1) {
63✔
561
        if (--m == 0) {
15✔
562
          m = 12;
3✔
563
          y--;
3✔
564
        }
565

566
        d += Datetime::daysInMonth(y, m);
15✔
567
      }
568
      break;
63✔
569

570
    case 'M':
21✔
571
      d = 1;
21✔
572
      if (--m == 0) {
21✔
573
        m = 12;
2✔
574
        --y;
2✔
575
      }
576
      break;
21✔
577
  }
578

579
  return Datetime(y, m, d, 0, 0, 0);
147✔
580
}
581

582
////////////////////////////////////////////////////////////////////////////////
583
// Do '_bars[epoch] = Bar' for every bar that may appear on a chart.
584
void Chart::generateBars() {
7✔
585
  Bar bar;
7✔
586

587
  // Determine the last bar date.
588
  Datetime cursor;
7✔
589
  switch (_period) {
7✔
590
    case 'D':
3✔
591
      cursor = Datetime().startOfDay();
3✔
592
      break;
3✔
593
    case 'W':
3✔
594
      cursor = Datetime().startOfWeek();
3✔
595
      break;
3✔
596
    case 'M':
1✔
597
      cursor = Datetime().startOfMonth();
1✔
598
      break;
1✔
599
  }
600

601
  // Iterate and determine all the other bar dates.
602
  char str[12];
603
  for (int i = 0; i < _estimated_bars; ++i) {
154✔
604
    // Create the major and minor labels.
605
    switch (_period) {
147✔
606
      case 'D':  // month/day
63✔
607
      {
608
        std::string month = Datetime::monthName(cursor.month());
63✔
609
        bar._major_label = month.substr(0, 3);
63✔
610

611
        snprintf(str, 12, "%02d", cursor.day());
63✔
612
        bar._minor_label = str;
63✔
613
      } break;
63✔
614

615
      case 'W':  // year/week
63✔
616
        snprintf(str, 12, "%d", cursor.year());
63✔
617
        bar._major_label = str;
63✔
618

619
        snprintf(str, 12, "%02d", cursor.week());
63✔
620
        bar._minor_label = str;
63✔
621
        break;
63✔
622

623
      case 'M':  // year/month
21✔
624
        snprintf(str, 12, "%d", cursor.year());
21✔
625
        bar._major_label = str;
21✔
626

627
        snprintf(str, 12, "%02d", cursor.month());
21✔
628
        bar._minor_label = str;
21✔
629
        break;
21✔
630
    }
631

632
    bar._offset = i;
147✔
633
    _bars[cursor.toEpoch()] = bar;
147✔
634

635
    // Record the earliest date, for use as a cutoff when scanning data.
636
    _earliest = cursor;
147✔
637

638
    // Move to the previous period.
639
    cursor = decrement(cursor, _period);
147✔
640
  }
641
}
7✔
642

643
////////////////////////////////////////////////////////////////////////////////
644
void Chart::maxima() {
7✔
645
  _max_value = 0;
7✔
646
  _max_label = 1;
7✔
647

648
  for (auto& bar : _bars) {
154✔
649
    // Determine _max_label.
650
    int total = bar.second._pending + bar.second._started + bar.second._done + _carryover_done;
147✔
651

652
    // Determine _max_value.
653
    if (total > _max_value) _max_value = total;
147✔
654

655
    int length = (int)log10((double)total) + 1;
147✔
656
    if (length > _max_label) _max_label = length;
147✔
657
  }
658

659
  // How many bars can be shown?
660
  _actual_bars = (_width - _max_label - 14) / 3;
7✔
661
  _graph_width = _width - _max_label - 14;
7✔
662
}
7✔
663

664
////////////////////////////////////////////////////////////////////////////////
665
// Given the vertical chart area size (graph_height), the largest value
666
// (_max_value), populate a vector of labels for the y axis.
667
void Chart::yLabels(std::vector<int>& labels) {
7✔
668
  // Calculate may Y using a nice algorithm that rounds the data.
669
  int high = burndown_size(_max_value);
7✔
670
  int half = high / 2;
7✔
671

672
  labels.push_back(0);
7✔
673
  labels.push_back(half);
7✔
674
  labels.push_back(high);
7✔
675
}
7✔
676

677
////////////////////////////////////////////////////////////////////////////////
678
void Chart::calculateRates() {
7✔
679
  // Q: Why is this equation written out as a debug message?
680
  // A: People are going to want to know how the rates and the completion date
681
  //    are calculated.  This may also help debugging.
682
  std::stringstream peak_message;
7✔
683
  peak_message << "Chart::calculateRates Maximum of " << _peak_count << " pending tasks on "
7✔
684
               << (Datetime(_peak_epoch).toISO()) << ", with currently " << _current_count
14✔
685
               << " pending tasks";
7✔
686
  Context::getContext().debug(peak_message.str());
7✔
687

688
  // If there are no current pending tasks, then it is meaningless to find
689
  // rates or estimated completion date.
690
  if (_current_count == 0) return;
7✔
691

692
  // If there is a net fix rate, and the peak was at least three days ago.
693
  Datetime now;
7✔
694
  Datetime peak(_peak_epoch);
7✔
695
  if (_peak_count > _current_count && (now - peak) > 3 * 86400) {
7✔
696
    // Fixes per second.  Not a large number.
UNCOV
697
    auto fix_rate = 1.0 * (_peak_count - _current_count) / (now.toEpoch() - _peak_epoch);
×
UNCOV
698
    _net_fix_rate = fix_rate * 86400;
×
699

UNCOV
700
    std::stringstream rate_message;
×
UNCOV
701
    rate_message << "Chart::calculateRates Net reduction is " << (_peak_count - _current_count)
×
UNCOV
702
                 << " tasks in " << Duration(now.toEpoch() - _peak_epoch).formatISO() << " = "
×
UNCOV
703
                 << _net_fix_rate << " tasks/d";
×
UNCOV
704
    Context::getContext().debug(rate_message.str());
×
705

UNCOV
706
    Duration delta(static_cast<time_t>(_current_count / fix_rate));
×
UNCOV
707
    Datetime end = now + delta.toTime_t();
×
708

709
    // Prefer dateformat.report over dateformat.
UNCOV
710
    std::string format = Context::getContext().config.get("dateformat.report");
×
UNCOV
711
    if (format == "") {
×
UNCOV
712
      format = Context::getContext().config.get("dateformat");
×
UNCOV
713
      if (format == "") format = "Y-M-D";
×
714
    }
715

UNCOV
716
    _completion = end.toString(format) + " (" + delta.formatVague() + ')';
×
717

UNCOV
718
    std::stringstream completion_message;
×
UNCOV
719
    completion_message << "Chart::calculateRates (" << _current_count << " tasks / "
×
UNCOV
720
                       << _net_fix_rate << ") = " << delta.format() << " --> " << end.toISO();
×
UNCOV
721
    Context::getContext().debug(completion_message.str());
×
UNCOV
722
  } else {
×
723
    _completion = "No convergence";
7✔
724
  }
725
}
7✔
726

727
////////////////////////////////////////////////////////////////////////////////
728
unsigned Chart::round_up_to(unsigned n, unsigned target) { return n + target - (n % target); }
7✔
729

730
////////////////////////////////////////////////////////////////////////////////
731
unsigned Chart::burndown_size(unsigned ntasks) {
7✔
732
  // Nearest 2
733
  if (ntasks < 20) return round_up_to(ntasks, 2);
7✔
734

735
  // Nearest 10
736
  if (ntasks < 50) return round_up_to(ntasks, 10);
×
737

738
  // Nearest 20
739
  if (ntasks < 100) return round_up_to(ntasks, 20);
×
740

741
  // Choose the number from here rounded up to the nearest 10% of the next
742
  // highest power of 10 or half of power of 10.
743
  const auto count = (unsigned)log10(static_cast<double>(std::numeric_limits<unsigned>::max()));
×
744
  unsigned half = 500;
×
745
  unsigned full = 1000;
×
746

747
  // We start at two because we handle 5, 10, 50, and 100 above.
748
  for (unsigned i = 2; i < count; ++i) {
×
749
    if (ntasks < half) return round_up_to(ntasks, half / 10);
×
750

751
    if (ntasks < full) return round_up_to(ntasks, full / 10);
×
752

753
    half *= 10;
×
754
    full *= 10;
×
755
  }
756

757
  // Round up to max of unsigned.
758
  return std::numeric_limits<unsigned>::max();
×
759
}
760

761
////////////////////////////////////////////////////////////////////////////////
762
CmdBurndownMonthly::CmdBurndownMonthly() {
4,579✔
763
  _keyword = "burndown.monthly";
4,579✔
764
  _usage = "task <filter> burndown.monthly";
4,579✔
765
  _description = "Shows a graphical burndown chart, by month";
4,579✔
766
  _read_only = true;
4,579✔
767
  _displays_id = false;
4,579✔
768
  _needs_gc = true;
4,579✔
769
  _needs_recur_update = true;
4,579✔
770
  _uses_context = true;
4,579✔
771
  _accepts_filter = true;
4,579✔
772
  _accepts_modifications = false;
4,579✔
773
  _accepts_miscellaneous = false;
4,579✔
774
  _category = Command::Category::graphs;
4,579✔
775
}
4,579✔
776

777
////////////////////////////////////////////////////////////////////////////////
778
int CmdBurndownMonthly::execute(std::string& output) {
1✔
779
  int rc = 0;
1✔
780

781
  // Scan the pending tasks, applying any filter.
782
  Filter filter;
1✔
783
  std::vector<Task> filtered;
1✔
784
  filter.subset(filtered);
1✔
785

786
  // Create a chart, scan the tasks, then render.
787
  Chart chart('M');
1✔
788
  chart.scanForPeak(filtered);
1✔
789
  chart.scan(filtered);
1✔
790
  output = chart.render();
1✔
791
  return rc;
1✔
792
}
1✔
793

794
////////////////////////////////////////////////////////////////////////////////
795
CmdBurndownWeekly::CmdBurndownWeekly() {
4,579✔
796
  _keyword = "burndown.weekly";
4,579✔
797
  _usage = "task <filter> burndown.weekly";
4,579✔
798
  _description = "Shows a graphical burndown chart, by week";
4,579✔
799
  _read_only = true;
4,579✔
800
  _displays_id = false;
4,579✔
801
  _needs_gc = true;
4,579✔
802
  _needs_recur_update = true;
4,579✔
803
  _uses_context = true;
4,579✔
804
  _accepts_filter = true;
4,579✔
805
  _accepts_modifications = false;
4,579✔
806
  _accepts_miscellaneous = false;
4,579✔
807
  _category = Command::Category::graphs;
4,579✔
808
}
4,579✔
809

810
////////////////////////////////////////////////////////////////////////////////
811
int CmdBurndownWeekly::execute(std::string& output) {
3✔
812
  int rc = 0;
3✔
813

814
  // Scan the pending tasks, applying any filter.
815
  Filter filter;
3✔
816
  std::vector<Task> filtered;
3✔
817
  filter.subset(filtered);
3✔
818

819
  // Create a chart, scan the tasks, then render.
820
  Chart chart('W');
3✔
821
  chart.scanForPeak(filtered);
3✔
822
  chart.scan(filtered);
3✔
823
  output = chart.render();
3✔
824
  return rc;
3✔
825
}
3✔
826

827
////////////////////////////////////////////////////////////////////////////////
828
CmdBurndownDaily::CmdBurndownDaily() {
4,579✔
829
  _keyword = "burndown.daily";
4,579✔
830
  _usage = "task <filter> burndown.daily";
4,579✔
831
  _description = "Shows a graphical burndown chart, by day";
4,579✔
832
  _read_only = true;
4,579✔
833
  _displays_id = false;
4,579✔
834
  _needs_gc = true;
4,579✔
835
  _needs_recur_update = true;
4,579✔
836
  _uses_context = true;
4,579✔
837
  _accepts_filter = true;
4,579✔
838
  _accepts_modifications = false;
4,579✔
839
  _accepts_miscellaneous = false;
4,579✔
840
  _category = Command::Category::graphs;
4,579✔
841
}
4,579✔
842

843
////////////////////////////////////////////////////////////////////////////////
844
int CmdBurndownDaily::execute(std::string& output) {
3✔
845
  int rc = 0;
3✔
846

847
  // Scan the pending tasks, applying any filter.
848
  Filter filter;
3✔
849
  std::vector<Task> filtered;
3✔
850
  filter.subset(filtered);
3✔
851

852
  // Create a chart, scan the tasks, then render.
853
  Chart chart('D');
3✔
854
  chart.scanForPeak(filtered);
3✔
855
  chart.scan(filtered);
3✔
856
  output = chart.render();
3✔
857
  return rc;
3✔
858
}
3✔
859

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