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

GothenburgBitFactory / taskwarrior / 16272683545

14 Jul 2025 04:43PM UTC coverage: 85.114% (-0.08%) from 85.197%
16272683545

Pull #3913

github

web-flow
[pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/pre-commit/mirrors-clang-format: v20.1.7 → v20.1.8](https://github.com/pre-commit/mirrors-clang-format/compare/v20.1.7...v20.1.8)
Pull Request #3913: [pre-commit.ci] pre-commit autoupdate

19561 of 22982 relevant lines covered (85.11%)

23488.34 hits per line

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

89.66
/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,498✔
199
      time_t epoch = quantize(entry.toEpoch(), 'D').toEpoch();
6,466✔
200
      if (pending.find(epoch) != pending.end())
6,466✔
201
        ++pending[epoch];
2,810✔
202
      else
203
        pending[epoch] = 1;
3,656✔
204

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

209
  // Find the peak and peak date.
210
  for (auto& count : pending) {
3,663✔
211
    if (count.second > _peak_count) {
3,656✔
212
      _peak_count = count.second;
21✔
213
      _peak_epoch = count.first;
21✔
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,333✔
260
          epoch = from.toEpoch();
2,326✔
261
          if (_bars.find(epoch) != _bars.end()) ++_bars[epoch]._pending;
2,326✔
262
          from = increment(from, _period);
2,326✔
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)),
12✔
391
                      bar._major_label.length(), ' ' + bar._major_label);
24✔
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✔
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,973✔
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,518✔
486
  if (period == 'D') return input.startOfDay();
6,518✔
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,962✔
495
  // Move to the next period.
496
  int d = input.day();
9,962✔
497
  int m = input.month();
9,962✔
498
  int y = input.year();
9,962✔
499

500
  int days;
501

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

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

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

520
        if (++m == 13) {
36✔
521
          m = 1;
3✔
522
          ++y;
3✔
523
        }
524
      }
525
      break;
161✔
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,962✔
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;
×
563
          y--;
×
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.
697
    auto fix_rate = 1.0 * (_peak_count - _current_count) / (now.toEpoch() - _peak_epoch);
×
698
    _net_fix_rate = fix_rate * 86400;
×
699

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

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

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

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

718
    std::stringstream completion_message;
×
719
    completion_message << "Chart::calculateRates (" << _current_count << " tasks / "
×
720
                       << _net_fix_rate << ") = " << delta.format() << " --> " << end.toISO();
×
721
    Context::getContext().debug(completion_message.str());
×
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,593✔
763
  _keyword = "burndown.monthly";
4,593✔
764
  _usage = "task <filter> burndown.monthly";
4,593✔
765
  _description = "Shows a graphical burndown chart, by month";
4,593✔
766
  _read_only = true;
4,593✔
767
  _displays_id = false;
4,593✔
768
  _needs_gc = true;
4,593✔
769
  _needs_recur_update = true;
4,593✔
770
  _uses_context = true;
4,593✔
771
  _accepts_filter = true;
4,593✔
772
  _accepts_modifications = false;
4,593✔
773
  _accepts_miscellaneous = false;
4,593✔
774
  _category = Command::Category::graphs;
4,593✔
775
}
4,593✔
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,593✔
796
  _keyword = "burndown.weekly";
4,593✔
797
  _usage = "task <filter> burndown.weekly";
4,593✔
798
  _description = "Shows a graphical burndown chart, by week";
4,593✔
799
  _read_only = true;
4,593✔
800
  _displays_id = false;
4,593✔
801
  _needs_gc = true;
4,593✔
802
  _needs_recur_update = true;
4,593✔
803
  _uses_context = true;
4,593✔
804
  _accepts_filter = true;
4,593✔
805
  _accepts_modifications = false;
4,593✔
806
  _accepts_miscellaneous = false;
4,593✔
807
  _category = Command::Category::graphs;
4,593✔
808
}
4,593✔
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,593✔
829
  _keyword = "burndown.daily";
4,593✔
830
  _usage = "task <filter> burndown.daily";
4,593✔
831
  _description = "Shows a graphical burndown chart, by day";
4,593✔
832
  _read_only = true;
4,593✔
833
  _displays_id = false;
4,593✔
834
  _needs_gc = true;
4,593✔
835
  _needs_recur_update = true;
4,593✔
836
  _uses_context = true;
4,593✔
837
  _accepts_filter = true;
4,593✔
838
  _accepts_modifications = false;
4,593✔
839
  _accepts_miscellaneous = false;
4,593✔
840
  _category = Command::Category::graphs;
4,593✔
841
}
4,593✔
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

© 2025 Coveralls, Inc