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

GothenburgBitFactory / taskwarrior / 11703734187

06 Nov 2024 12:40PM UTC coverage: 85.073% (+0.08%) from 84.996%
11703734187

push

github

web-flow
bump libshared for bold 256color support (#3670)

In particular, commit https://github.com/GothenburgBitFactory/libshared/commit/47a750c38.

19138 of 22496 relevant lines covered (85.07%)

22922.84 hits per line

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

94.03
/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 <main.h>
37
#include <math.h>
38
#include <shared.h>
39
#include <string.h>
40

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

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

49
////////////////////////////////////////////////////////////////////////////////
50
class Bar {
51
 public:
52
  Bar() = default;
110✔
53
  Bar(const Bar&);
54
  Bar& operator=(const Bar&);
55
  ~Bar() = default;
320✔
56

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

68
////////////////////////////////////////////////////////////////////////////////
69
Bar::Bar(const Bar& other) { *this = other; }
210✔
70

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

84
  return *this;
315✔
85
}
86

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

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

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

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

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

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

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

179
  _period = type;
5✔
180
}
5✔
181

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

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

193
    Datetime end;
30✔
194
    if (task.has("end"))
30✔
195
      end = Datetime(task.get_date("end"));
20✔
196
    else
197
      ++_current_count;
10✔
198

199
    while (entry < end) {
7,070✔
200
      time_t epoch = quantize(entry.toEpoch(), 'D').toEpoch();
7,040✔
201
      if (pending.find(epoch) != pending.end())
7,040✔
202
        ++pending[epoch];
3,385✔
203
      else
204
        pending[epoch] = 1;
3,655✔
205

206
      entry = increment(entry, 'D');
7,040✔
207
    }
208
  }
209

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

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

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

226
  time_t epoch;
227
  auto& config = Context::getContext().config;
5✔
228
  bool cumulative;
229
  if (config.has("burndown.cumulative")) {
5✔
230
    cumulative = config.getBoolean("burndown.cumulative");
3✔
231
  } else {
232
    cumulative = true;
2✔
233
  }
234

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

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

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

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

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

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

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

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

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

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

305
  // Size the data.
306
  maxima();
5✔
307
}
5✔
308

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

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

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

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

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

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

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

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

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

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

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

380
  std::sort(bars_in_sequence.begin(), bars_in_sequence.end());
5✔
381
  std::string _major_label;
5✔
382
  for (auto& seq : bars_in_sequence) {
110✔
383
    Bar bar = _bars[seq];
105✔
384

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

390
      if (_major_label != bar._major_label)
105✔
391
        _grid.replace(LOC(_height - 4, _max_label + 2 + ((_actual_bars - bar._offset - 1) * 3)),
9✔
392
                      bar._major_label.length(), ' ' + bar._major_label);
18✔
393

394
      _major_label = bar._major_label;
105✔
395
    }
396
  }
105✔
397

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

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

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

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

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

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

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

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

442
  optimizeGrid();
5✔
443

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

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

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

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

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

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

470
  return _grid;
5✔
471
}
5✔
472

473
////////////////////////////////////////////////////////////////////////////////
474
// _grid =~ /\s+$//g
475
void Chart::optimizeGrid() {
5✔
476
  std::string::size_type ws;
477
  while ((ws = _grid.find(" \n")) != std::string::npos) {
105✔
478
    auto non_ws = ws;
100✔
479
    while (_grid[non_ws] == ' ') --non_ws;
3,249✔
480

481
    _grid.replace(non_ws + 1, ws - non_ws + 1, "\n");
100✔
482
  }
483
}
5✔
484

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

491
  return input;
×
492
}
493

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

501
  int days;
502

503
  switch (period) {
10,533✔
504
    case 'D':
10,337✔
505
      if (++d > Datetime::daysInMonth(y, m)) {
10,337✔
506
        d = 1;
338✔
507

508
        if (++m == 13) {
338✔
509
          m = 1;
24✔
510
          ++y;
24✔
511
        }
512
      }
513
      break;
10,337✔
514

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

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

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

537
  return Datetime(y, m, d, 0, 0, 0);
10,533✔
538
}
539

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

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

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

559
    case 'W':
21✔
560
      d -= 7;
21✔
561
      if (d < 1) {
21✔
562
        if (--m == 0) {
5✔
563
          m = 12;
×
564
          y--;
×
565
        }
566

567
        d += Datetime::daysInMonth(y, m);
5✔
568
      }
569
      break;
21✔
570

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

580
  return Datetime(y, m, d, 0, 0, 0);
105✔
581
}
582

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

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

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

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

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

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

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

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

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

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

639
    // Move to the previous period.
640
    cursor = decrement(cursor, _period);
105✔
641
  }
642
}
5✔
643

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

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

653
    // Determine _max_value.
654
    if (total > _max_value) _max_value = total;
105✔
655

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

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

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

673
  labels.push_back(0);
5✔
674
  labels.push_back(half);
5✔
675
  labels.push_back(high);
5✔
676
}
5✔
677

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

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

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

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

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

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

717
    _completion = end.toString(format) + " (" + delta.formatVague() + ')';
5✔
718

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

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

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

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

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

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

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

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

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

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

762
////////////////////////////////////////////////////////////////////////////////
763
CmdBurndownMonthly::CmdBurndownMonthly() {
4,503✔
764
  _keyword = "burndown.monthly";
4,503✔
765
  _usage = "task <filter> burndown.monthly";
4,503✔
766
  _description = "Shows a graphical burndown chart, by month";
4,503✔
767
  _read_only = true;
4,503✔
768
  _displays_id = false;
4,503✔
769
  _needs_gc = true;
4,503✔
770
  _uses_context = true;
4,503✔
771
  _accepts_filter = true;
4,503✔
772
  _accepts_modifications = false;
4,503✔
773
  _accepts_miscellaneous = false;
4,503✔
774
  _category = Command::Category::graphs;
4,503✔
775
}
4,503✔
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
  handleUntil();
1✔
783
  handleRecurrence();
1✔
784
  Filter filter;
1✔
785
  std::vector<Task> filtered;
1✔
786
  filter.subset(filtered);
1✔
787

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

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

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

815
  // Scan the pending tasks, applying any filter.
816
  handleUntil();
1✔
817
  handleRecurrence();
1✔
818
  Filter filter;
1✔
819
  std::vector<Task> filtered;
1✔
820
  filter.subset(filtered);
1✔
821

822
  // Create a chart, scan the tasks, then render.
823
  Chart chart('W');
1✔
824
  chart.scanForPeak(filtered);
1✔
825
  chart.scan(filtered);
1✔
826
  output = chart.render();
1✔
827
  return rc;
1✔
828
}
1✔
829

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

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

849
  // Scan the pending tasks, applying any filter.
850
  handleUntil();
3✔
851
  handleRecurrence();
3✔
852
  Filter filter;
3✔
853
  std::vector<Task> filtered;
3✔
854
  filter.subset(filtered);
3✔
855

856
  // Create a chart, scan the tasks, then render.
857
  Chart chart('D');
3✔
858
  chart.scanForPeak(filtered);
3✔
859
  chart.scan(filtered);
3✔
860
  output = chart.render();
3✔
861
  return rc;
3✔
862
}
3✔
863

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