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

GothenburgBitFactory / taskwarrior / 9924819625

14 Jul 2024 02:54AM UTC coverage: 84.35% (+0.1%) from 84.231%
9924819625

push

github

web-flow
Note in taskrc(5) that "undo" configurations are not currently supported (#3518)

19284 of 22862 relevant lines covered (84.35%)

20261.66 hits per line

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

92.26
/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
#include <CmdBurndown.h>
29
#include <sstream>
30
#include <map>
31
#include <algorithm>
32
#include <limits>
33
#include <string.h>
34
#include <math.h>
35
#include <Context.h>
36
#include <Filter.h>
37
#include <Datetime.h>
38
#include <Duration.h>
39
#include <main.h>
40
#include <shared.h>
41
#include <format.h>
42

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

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

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

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

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

87
  return *this;
315✔
88
}
89

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

129
  void scan (std::vector <Task>&);
130
  void scanForPeak (std::vector <Task>&);
131
  std::string render ();
132

133
private:
134
  void generateBars ();
135
  void optimizeGrid ();
136
  Datetime quantize (const Datetime&, char);
137

138
  Datetime increment (const Datetime&, char);
139
  Datetime decrement (const Datetime&, char);
140
  void maxima ();
141
  void yLabels (std::vector <int>&);
142
  void calculateRates ();
143
  unsigned round_up_to (unsigned, unsigned);
144
  unsigned burndown_size (unsigned);
145

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

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

180
  // Estimate how many 'bars' can be dsplayed.  This will help subset a
181
  // potentially enormous data set.
182
  _estimated_bars = (_width - 1 - 14) / 3;
5✔
183

184
  _period = type;
5✔
185
}
5✔
186

187
////////////////////////////////////////////////////////////////////////////////
188
// Scan all tasks, quantize the dates by day, and find the peak pending count
189
// and corresponding epoch.
190
void Chart::scanForPeak (std::vector <Task>& tasks)
5✔
191
{
192
  std::map <time_t, int> pending;
5✔
193
  _current_count = 0;
5✔
194

195
  for (auto& task : tasks)
35✔
196
  {
197
    // The entry date is when the counting starts.
198
    Datetime entry (task.get_date ("entry"));
30✔
199

200
    Datetime end;
30✔
201
    if (task.has ("end"))
30✔
202
      end = Datetime (task.get_date ("end"));
20✔
203
    else
204
      ++_current_count;
10✔
205

206
    while (entry < end)
6,495✔
207
    {
208
      time_t epoch = quantize (entry.toEpoch (), 'D').toEpoch ();
6,465✔
209
      if (pending.find (epoch) != pending.end ())
6,465✔
210
        ++pending[epoch];
2,810✔
211
      else
212
        pending[epoch] = 1;
3,655✔
213

214
      entry = increment (entry, 'D');
6,465✔
215
    }
216
  }
217

218
  // Find the peak and peak date.
219
  for (auto& count : pending)
3,660✔
220
  {
221
    if (count.second > _peak_count)
3,655✔
222
    {
223
      _peak_count = count.second;
15✔
224
      _peak_epoch = count.first;
15✔
225
    }
226
  }
227
}
5✔
228

229
////////////////////////////////////////////////////////////////////////////////
230
void Chart::scan (std::vector <Task>& tasks)
5✔
231
{
232
  generateBars ();
5✔
233

234
  // Not quantized, so that "while (xxx < now)" is inclusive.
235
  Datetime now;
5✔
236

237
  time_t epoch;
238
  auto& config = Context::getContext ().config;
5✔
239
  bool cumulative;
240
  if (config.has ("burndown.cumulative"))
5✔
241
  {
242
    cumulative = config.getBoolean ("burndown.cumulative");
3✔
243
  }
244
  else
245
  {
246
    cumulative = true;
2✔
247
  }
248

249
  for (auto& task : tasks)
35✔
250
  {
251
    // The entry date is when the counting starts.
252
    Datetime from = quantize (Datetime (task.get_date ("entry")), _period);
30✔
253
    epoch = from.toEpoch ();
30✔
254

255
    if (_bars.find (epoch) != _bars.end ())
30✔
256
      ++_bars[epoch]._added;
17✔
257

258
    // e-->   e--s-->
259
    // ppp>   pppsss>
260
    Task::status status = task.getStatus ();
30✔
261
    if (status == Task::pending ||
30✔
262
        status == Task::waiting)
263
    {
264
      if (task.has ("start"))
10✔
265
      {
266
        Datetime start = quantize (Datetime (task.get_date ("start")), _period);
5✔
267
        while (from < start)
1,165✔
268
        {
269
          epoch = from.toEpoch ();
1,160✔
270
          if (_bars.find (epoch) != _bars.end ())
1,160✔
271
            ++_bars[epoch]._pending;
92✔
272
          from = increment (from, _period);
1,160✔
273
        }
274

275
        while (from < now)
10✔
276
        {
277
          epoch = from.toEpoch ();
5✔
278
          if (_bars.find (epoch) != _bars.end ())
5✔
279
            ++_bars[epoch]._started;
5✔
280
          from = increment (from, _period);
5✔
281
        }
282
      }
283
      else
284
      {
285
        while (from < now)
2,329✔
286
        {
287
          epoch = from.toEpoch ();
2,324✔
288
          if (_bars.find (epoch) != _bars.end ())
2,324✔
289
            ++_bars[epoch]._pending;
105✔
290
          from = increment (from, _period);
2,324✔
291
        }
292
      }
293
    }
10✔
294

295
    // e--C   e--s--C
296
    // pppd>  pppsssd>
297
    else if (status == Task::completed)
20✔
298
    {
299
      // Truncate history so it starts at 'earliest' for completed tasks.
300
      Datetime end = quantize (Datetime (task.get_date ("end")), _period);
15✔
301
      epoch = end.toEpoch ();
15✔
302

303
      if (_bars.find (epoch) != _bars.end ())
15✔
304
        ++_bars[epoch]._removed;
15✔
305

306
      while (from < end)
15✔
307
      {
308
        epoch = from.toEpoch ();
×
309
        if (_bars.find (epoch) != _bars.end ())
×
310
          ++_bars[epoch]._pending;
×
311
        from = increment (from, _period);
×
312
      }
313

314
      if (cumulative)
15✔
315
      {
316
        while (from < now)
12✔
317
        {
318
          epoch = from.toEpoch ();
6✔
319
          if (_bars.find (epoch) != _bars.end ())
6✔
320
            ++_bars[epoch]._done;
6✔
321
          from = increment (from, _period);
6✔
322
        }
323

324
        // Maintain a running total of 'done' tasks that are off the left of the
325
        // chart.
326
        if (end < _earliest)
6✔
327
        {
328
          ++_carryover_done;
×
329
          continue;
×
330
        }
331
      }
332

333
      else
334
      {
335
                  epoch = from.toEpoch ();
9✔
336
        if (_bars.find (epoch) != _bars.end ())
9✔
337
          ++_bars[epoch]._done;
9✔
338
      }
339
    }
340
  }
341

342
  // Size the data.
343
  maxima ();
5✔
344
}
5✔
345

346
////////////////////////////////////////////////////////////////////////////////
347
// Graph should render like this:
348
//   +---------------------------------------------------------------------+
349
//   |                                                                     |
350
//   | 20 |                                                                |
351
//   |    |                            DD DD DD DD DD DD DD DD             |
352
//   |    |          DD DD DD DD DD DD DD DD DD DD DD DD DD DD             |
353
//   |    | PP PP SS SS SS SS SS SS SS SS SS DD DD DD DD DD DD   DD Done   |
354
//   | 10 | PP PP PP PP PP PP SS SS SS SS SS SS DD DD DD DD DD   SS Started|
355
//   |    | PP PP PP PP PP PP PP PP PP PP PP SS SS SS SS DD DD   PP Pending|
356
//   |    | PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP SS DD             |
357
//   |    | PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP             |
358
//   |  0 +----------------------------------------------------            |
359
//   |      21 22 23 24 25 26 27 28 29 30 31 01 02 03 04 05 06             |
360
//   |      July                             August                        |
361
//   |                                                                     |
362
//   |      ADD rate 1.7/d           Estimated completion 8/12/2010        |
363
//   |      Don/Delete rate  1.3/d                                         |
364
//   +---------------------------------------------------------------------+
365
std::string Chart::render ()
5✔
366
{
367
  if (_graph_height < 5 ||     // a 4-line graph is essentially unreadable.
5✔
368
      _graph_width < 2)        // A single-bar graph is useless.
5✔
369
  {
370
    return std::string ("Terminal window too small to draw a graph.\n");
×
371
  }
372

373
  else if (_graph_height > 1000 || // each line is a string allloc
5✔
374
           _graph_width  > 1000)
5✔
375
  {
376
    return std::string ("Terminal window too large to draw a graph.\n");
×
377
  }
378

379
  if (_max_value == 0)
5✔
380
    Context::getContext ().footnote ("No matches.");
×
381

382
  // Create a grid, folded into a string.
383
  _grid = "";
5✔
384
  for (int i = 0; i < _height; ++i)
115✔
385
    _grid += std::string (_width, ' ') + '\n';
110✔
386

387
  // Title.
388
  std::string title = _period == 'D' ? "Daily"
5✔
389
                    : _period == 'W' ? "Weekly"
2✔
390
                    :                  "Monthly";
7✔
391
  title += std::string (" Burndown");
5✔
392
  _grid.replace (LOC (0, (_width - title.length ()) / 2), title.length (), title);
5✔
393

394
  // Legend.
395
  _grid.replace (LOC (_graph_height / 2 - 1, _width - 10), 10, "DD " + leftJustify ("Done",    7));
5✔
396
  _grid.replace (LOC (_graph_height / 2,     _width - 10), 10, "SS " + leftJustify ("Started", 7));
5✔
397
  _grid.replace (LOC (_graph_height / 2 + 1, _width - 10), 10, "PP " + leftJustify ("Pending", 7));
5✔
398

399
  // Determine y-axis labelling.
400
  std::vector <int> _labels;
5✔
401
  yLabels (_labels);
5✔
402
  _max_label = (int) log10 ((double) _labels[2]) + 1;
5✔
403

404
  // Draw y-axis.
405
  for (int i = 0; i < _graph_height; ++i)
80✔
406
     _grid.replace (LOC (i + 1, _max_label + 1), 1, "|");
75✔
407

408
  // Draw y-axis labels.
409
  char label [12];
410
  snprintf (label, 12, "%*d", _max_label, _labels[2]);
5✔
411
  _grid.replace (LOC (1,                       _max_label - strlen (label)), strlen (label), label);
5✔
412
  snprintf (label, 12, "%*d", _max_label, _labels[1]);
5✔
413
  _grid.replace (LOC (1 + (_graph_height / 2), _max_label - strlen (label)), strlen (label), label);
5✔
414
  _grid.replace (LOC (_graph_height + 1,       _max_label - 1),              1,              "0");
5✔
415

416
  // Draw x-axis.
417
  _grid.replace (LOC (_height - 6, _max_label + 1), 1, "+");
5✔
418
  _grid.replace (LOC (_height - 6, _max_label + 2), _graph_width, std::string (_graph_width, '-'));
5✔
419

420
  // Draw x-axis labels.
421
  std::vector <time_t> bars_in_sequence;
5✔
422
  for (auto& bar : _bars)
110✔
423
    bars_in_sequence.push_back (bar.first);
105✔
424

425
  std::sort (bars_in_sequence.begin (), bars_in_sequence.end ());
5✔
426
  std::string _major_label;
5✔
427
  for (auto& seq : bars_in_sequence)
110✔
428
  {
429
    Bar bar = _bars[seq];
105✔
430

431
    // If it fits within the allowed space.
432
    if (bar._offset < _actual_bars)
105✔
433
    {
434
      _grid.replace (LOC (_height - 5, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), bar._minor_label.length (), bar._minor_label);
105✔
435

436
      if (_major_label != bar._major_label)
105✔
437
        _grid.replace (LOC (_height - 4, _max_label + 2 + ((_actual_bars - bar._offset - 1) * 3)), bar._major_label.length (), ' ' + bar._major_label);
10✔
438

439
      _major_label = bar._major_label;
105✔
440
    }
441
  }
105✔
442

443
  // Draw bars.
444
  for (auto& seq : bars_in_sequence)
110✔
445
  {
446
    Bar bar = _bars[seq];
105✔
447

448
    // If it fits within the allowed space.
449
    if (bar._offset < _actual_bars)
105✔
450
    {
451
      int pending = ( bar._pending                                               * _graph_height) / _labels[2];
105✔
452
      int started = ((bar._pending + bar._started)                               * _graph_height) / _labels[2];
105✔
453
      int done    = ((bar._pending + bar._started + bar._done + _carryover_done) * _graph_height) / _labels[2];
105✔
454

455
      for (int b = 0; b < pending; ++b)
591✔
456
        _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "PP");
486✔
457

458
      for (int b = pending; b < started; ++b)
120✔
459
        _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "SS");
15✔
460

461
      for (int b = started; b < done; ++b)
140✔
462
        _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "DD");
35✔
463
    }
464
  }
105✔
465

466
  // Draw rates.
467
  calculateRates ();
5✔
468
  char rate[12];
469
  if (_net_fix_rate != 0.0)
5✔
470
    snprintf (rate, 12, "%.1f/d", _net_fix_rate);
5✔
471
  else
472
    strcpy (rate, "-");
×
473

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

476
  // Draw completion date.
477
  if (_completion.length ())
5✔
478
    _grid.replace (LOC (_height - 1, _max_label + 3), 22 + _completion.length (), "Estimated completion: " + _completion);
5✔
479

480
  optimizeGrid ();
5✔
481

482
  if (Context::getContext ().color ())
5✔
483
  {
484
    // Colorize the grid.
485
    Color color_pending (Context::getContext ().config.get ("color.burndown.pending"));
1✔
486
    Color color_done    (Context::getContext ().config.get ("color.burndown.done"));
1✔
487
    Color color_started (Context::getContext ().config.get ("color.burndown.started"));
1✔
488

489
    // Replace DD, SS, PP with colored strings.
490
    std::string::size_type i;
491
    while ((i = _grid.find ("PP")) != std::string::npos)
104✔
492
      _grid.replace (i, 2, color_pending.colorize ("  "));
103✔
493

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

497
    while ((i = _grid.find ("DD")) != std::string::npos)
9✔
498
      _grid.replace (i, 2, color_done.colorize ("  "));
8✔
499
  }
500
  else
501
  {
502
    // Replace DD, SS, PP with ./+/X strings.
503
    std::string::size_type i;
504
    while ((i = _grid.find ("PP")) != std::string::npos)
392✔
505
      _grid.replace (i, 2, " X");
388✔
506

507
    while ((i = _grid.find ("SS")) != std::string::npos)
20✔
508
      _grid.replace (i, 2, " +");
16✔
509

510
    while ((i = _grid.find ("DD")) != std::string::npos)
36✔
511
      _grid.replace (i, 2, " .");
32✔
512
  }
513

514
  return _grid;
5✔
515
}
5✔
516

517
////////////////////////////////////////////////////////////////////////////////
518
// _grid =~ /\s+$//g
519
void Chart::optimizeGrid ()
5✔
520
{
521
  std::string::size_type ws;
522
  while ((ws = _grid.find (" \n")) != std::string::npos)
105✔
523
  {
524
    auto non_ws = ws;
100✔
525
    while (_grid[non_ws] == ' ')
3,312✔
526
      --non_ws;
3,212✔
527

528
    _grid.replace (non_ws + 1, ws - non_ws + 1, "\n");
100✔
529
  }
530
}
5✔
531

532
////////////////////////////////////////////////////////////////////////////////
533
Datetime Chart::quantize (const Datetime& input, char period)
6,515✔
534
{
535
  if (period == 'D') return input.startOfDay ();
6,515✔
536
  if (period == 'W') return input.startOfWeek ();
20✔
537
  if (period == 'M') return input.startOfMonth ();
10✔
538

539
  return input;
×
540
}
541

542
////////////////////////////////////////////////////////////////////////////////
543
Datetime Chart::increment (const Datetime& input, char period)
9,960✔
544
{
545
  // Move to the next period.
546
  int d = input.day ();
9,960✔
547
  int m = input.month ();
9,960✔
548
  int y = input.year ();
9,960✔
549

550
  int days;
551

552
  switch (period)
9,960✔
553
  {
554
  case 'D':
9,762✔
555
    if (++d > Datetime::daysInMonth (y, m))
9,762✔
556
    {
557
      d = 1;
318✔
558

559
      if (++m == 13)
318✔
560
      {
561
        m = 1;
24✔
562
        ++y;
24✔
563
      }
564
    }
565
    break;
9,762✔
566

567
  case 'W':
160✔
568
    d += 7;
160✔
569
    days = Datetime::daysInMonth (y, m);
160✔
570
    if (d > days)
160✔
571
    {
572
      d -= days;
36✔
573

574
      if (++m == 13)
36✔
575
      {
576
        m = 1;
3✔
577
        ++y;
3✔
578
      }
579
    }
580
    break;
160✔
581

582
  case 'M':
38✔
583
    d = 1;
38✔
584
    if (++m == 13)
38✔
585
    {
586
      m = 1;
3✔
587
      ++y;
3✔
588
    }
589
    break;
38✔
590
  }
591

592
  return Datetime (y, m, d, 0, 0, 0);
9,960✔
593
}
594

595
////////////////////////////////////////////////////////////////////////////////
596
Datetime Chart::decrement (const Datetime& input, char period)
105✔
597
{
598
  // Move to the previous period.
599
  int d = input.day ();
105✔
600
  int m = input.month ();
105✔
601
  int y = input.year ();
105✔
602

603
  switch (period)
105✔
604
  {
605
  case 'D':
63✔
606
    if (--d == 0)
63✔
607
    {
608
      if (--m == 0)
3✔
609
      {
610
        m = 12;
×
611
        --y;
×
612
      }
613

614
      d = Datetime::daysInMonth (y, m);
3✔
615
    }
616
    break;
63✔
617

618
  case 'W':
21✔
619
    d -= 7;
21✔
620
    if (d < 1)
21✔
621
    {
622
      if (--m == 0)
5✔
623
      {
624
        m = 12;
×
625
        y--;
×
626
      }
627

628
      d += Datetime::daysInMonth (y, m);
5✔
629
    }
630
    break;
21✔
631

632
  case 'M':
21✔
633
    d = 1;
21✔
634
    if (--m == 0)
21✔
635
    {
636
      m = 12;
2✔
637
      --y;
2✔
638
    }
639
    break;
21✔
640
  }
641

642
  return Datetime (y, m, d, 0, 0, 0);
105✔
643
}
644

645
////////////////////////////////////////////////////////////////////////////////
646
// Do '_bars[epoch] = Bar' for every bar that may appear on a chart.
647
void Chart::generateBars ()
5✔
648
{
649
  Bar bar;
5✔
650

651
  // Determine the last bar date.
652
  Datetime cursor;
5✔
653
  switch (_period)
5✔
654
  {
655
  case 'D': cursor = Datetime ().startOfDay ();   break;
3✔
656
  case 'W': cursor = Datetime ().startOfWeek ();  break;
1✔
657
  case 'M': cursor = Datetime ().startOfMonth (); break;
1✔
658
  }
659

660
  // Iterate and determine all the other bar dates.
661
  char str[12];
662
  for (int i = 0; i < _estimated_bars; ++i)
110✔
663
  {
664
    // Create the major and minor labels.
665
    switch (_period)
105✔
666
    {
667
    case 'D': // month/day
63✔
668
      {
669
        std::string month = Datetime::monthName (cursor.month ());
63✔
670
        bar._major_label = month.substr (0, 3);
63✔
671

672
        snprintf (str, 12, "%02d", cursor.day ());
63✔
673
        bar._minor_label = str;
63✔
674
      }
63✔
675
      break;
63✔
676

677
    case 'W': // year/week
21✔
678
      snprintf (str, 12, "%d", cursor.year ());
21✔
679
      bar._major_label = str;
21✔
680

681
      snprintf (str, 12, "%02d", cursor.week ());
21✔
682
      bar._minor_label = str;
21✔
683
      break;
21✔
684

685
    case 'M': // year/month
21✔
686
      snprintf (str, 12, "%d", cursor.year ());
21✔
687
      bar._major_label = str;
21✔
688

689
      snprintf (str, 12, "%02d", cursor.month ());
21✔
690
      bar._minor_label = str;
21✔
691
      break;
21✔
692
    }
693

694
    bar._offset = i;
105✔
695
    _bars[cursor.toEpoch ()] = bar;
105✔
696

697
    // Record the earliest date, for use as a cutoff when scanning data.
698
    _earliest = cursor;
105✔
699

700
    // Move to the previous period.
701
    cursor = decrement (cursor, _period);
105✔
702
  }
703
}
5✔
704

705
////////////////////////////////////////////////////////////////////////////////
706
void Chart::maxima ()
5✔
707
{
708
  _max_value = 0;
5✔
709
  _max_label = 1;
5✔
710

711
  for (auto& bar : _bars)
110✔
712
  {
713
    // Determine _max_label.
714
    int total = bar.second._pending +
105✔
715
                bar.second._started +
105✔
716
                bar.second._done    +
105✔
717
                _carryover_done;
105✔
718

719
    // Determine _max_value.
720
    if (total > _max_value)
105✔
721
      _max_value = total;
11✔
722

723
    int length = (int) log10 ((double) total) + 1;
105✔
724
    if (length > _max_label)
105✔
725
      _max_label = length;
×
726
  }
727

728
  // How many bars can be shown?
729
  _actual_bars = (_width - _max_label - 14) / 3;
5✔
730
  _graph_width = _width - _max_label - 14;
5✔
731
}
5✔
732

733
////////////////////////////////////////////////////////////////////////////////
734
// Given the vertical chart area size (graph_height), the largest value
735
// (_max_value), populate a vector of labels for the y axis.
736
void Chart::yLabels (std::vector <int>& labels)
5✔
737
{
738
  // Calculate may Y using a nice algorithm that rounds the data.
739
  int high = burndown_size (_max_value);
5✔
740
  int half = high / 2;
5✔
741

742
  labels.push_back (0);
5✔
743
  labels.push_back (half);
5✔
744
  labels.push_back (high);
5✔
745
}
5✔
746

747
////////////////////////////////////////////////////////////////////////////////
748
void Chart::calculateRates ()
5✔
749
{
750
  // Q: Why is this equation written out as a debug message?
751
  // A: People are going to want to know how the rates and the completion date
752
  //    are calculated.  This may also help debugging.
753
  std::stringstream peak_message;
5✔
754
  peak_message << "Chart::calculateRates Maximum of "
5✔
755
               << _peak_count
756
               << " pending tasks on "
757
               << (Datetime (_peak_epoch).toISO ())
10✔
758
               << ", with currently "
10✔
759
               << _current_count
760
               << " pending tasks";
5✔
761
  Context::getContext ().debug (peak_message.str ());
5✔
762

763
  // If there are no current pending tasks, then it is meaningless to find
764
  // rates or estimated completion date.
765
  if (_current_count == 0)
5✔
766
    return;
×
767

768
  // If there is a net fix rate, and the peak was at least three days ago.
769
  Datetime now;
5✔
770
  Datetime peak (_peak_epoch);
5✔
771
  if (_peak_count > _current_count &&
10✔
772
      (now - peak) > 3 * 86400)
5✔
773
  {
774
    // Fixes per second.  Not a large number.
775
    auto fix_rate = 1.0 * (_peak_count - _current_count) / (now.toEpoch () - _peak_epoch);
5✔
776
    _net_fix_rate = fix_rate * 86400;
5✔
777

778
    std::stringstream rate_message;
5✔
779
    rate_message << "Chart::calculateRates Net reduction is "
5✔
780
                 << (_peak_count - _current_count)
5✔
781
                 << " tasks in "
782
                 << Duration (now.toEpoch () - _peak_epoch).formatISO ()
10✔
783
                 << " = "
10✔
784
                 << _net_fix_rate
5✔
785
                 << " tasks/d";
5✔
786
    Context::getContext ().debug (rate_message.str ());
5✔
787

788
    Duration delta (static_cast <time_t> (_current_count / fix_rate));
5✔
789
    Datetime end = now + delta.toTime_t ();
5✔
790

791
    // Prefer dateformat.report over dateformat.
792
    std::string format = Context::getContext ().config.get ("dateformat.report");
10✔
793
    if (format == "")
5✔
794
    {
795
      format = Context::getContext ().config.get ("dateformat");
5✔
796
      if (format == "")
5✔
797
        format = "Y-M-D";
×
798
    }
799

800
    _completion = end.toString (format)
5✔
801
               + " ("
10✔
802
               + delta.formatVague ()
20✔
803
               + ')';
5✔
804

805
    std::stringstream completion_message;
5✔
806
    completion_message << "Chart::calculateRates ("
5✔
807
                       << _current_count
808
                       << " tasks / "
5✔
809
                       << _net_fix_rate
5✔
810
                       << ") = "
811
                       << delta.format ()
×
812
                       << " --> "
813
                       << end.toISO ();
5✔
814
    Context::getContext ().debug (completion_message.str ());
5✔
815
  }
5✔
816
  else
817
  {
818
    _completion = "No convergence";
×
819
  }
820
}
5✔
821

822
////////////////////////////////////////////////////////////////////////////////
823
unsigned Chart::round_up_to (unsigned n, unsigned target)
5✔
824
{
825
  return n + target - (n % target);
5✔
826
}
827

828
////////////////////////////////////////////////////////////////////////////////
829
unsigned Chart::burndown_size (unsigned ntasks)
5✔
830
{
831
  // Nearest 2
832
  if (ntasks < 20)
5✔
833
    return round_up_to (ntasks, 2);
5✔
834

835
  // Nearest 10
836
  if (ntasks < 50)
×
837
    return round_up_to (ntasks, 10);
×
838

839
  // Nearest 20
840
  if (ntasks < 100)
×
841
    return round_up_to (ntasks, 20);
×
842

843
  // Choose the number from here rounded up to the nearest 10% of the next
844
  // highest power of 10 or half of power of 10.
845
  const auto count = (unsigned) log10 (static_cast<double>(std::numeric_limits<unsigned>::max ()));
×
846
  unsigned half = 500;
×
847
  unsigned full = 1000;
×
848

849
  // We start at two because we handle 5, 10, 50, and 100 above.
850
  for (unsigned i = 2; i < count; ++i)
×
851
  {
852
    if (ntasks < half)
×
853
      return round_up_to (ntasks, half / 10);
×
854

855
    if (ntasks < full)
×
856
      return round_up_to (ntasks, full / 10);
×
857

858
    half *= 10;
×
859
    full *= 10;
×
860
  }
861

862
  // Round up to max of unsigned.
863
  return std::numeric_limits<unsigned>::max ();
×
864
}
865

866
////////////////////////////////////////////////////////////////////////////////
867
CmdBurndownMonthly::CmdBurndownMonthly ()
4,348✔
868
{
869
  _keyword               = "burndown.monthly";
4,348✔
870
  _usage                 = "task <filter> burndown.monthly";
4,348✔
871
  _description           = "Shows a graphical burndown chart, by month";
4,348✔
872
  _read_only             = true;
4,348✔
873
  _displays_id           = false;
4,348✔
874
  _needs_gc              = true;
4,348✔
875
  _uses_context          = true;
4,348✔
876
  _accepts_filter        = true;
4,348✔
877
  _accepts_modifications = false;
4,348✔
878
  _accepts_miscellaneous = false;
4,348✔
879
  _category              = Command::Category::graphs;
4,348✔
880
}
4,348✔
881

882
////////////////////////////////////////////////////////////////////////////////
883
int CmdBurndownMonthly::execute (std::string& output)
1✔
884
{
885
  int rc = 0;
1✔
886

887
  // Scan the pending tasks, applying any filter.
888
  handleUntil ();
1✔
889
  handleRecurrence ();
1✔
890
  Filter filter;
1✔
891
  std::vector <Task> filtered;
1✔
892
  filter.subset (filtered);
1✔
893

894
  // Create a chart, scan the tasks, then render.
895
  Chart chart ('M');
1✔
896
  chart.scanForPeak (filtered);
1✔
897
  chart.scan (filtered);
1✔
898
  output = chart.render ();
1✔
899
  return rc;
1✔
900
}
1✔
901

902
////////////////////////////////////////////////////////////////////////////////
903
CmdBurndownWeekly::CmdBurndownWeekly ()
4,348✔
904
{
905
  _keyword               = "burndown.weekly";
4,348✔
906
  _usage                 = "task <filter> burndown.weekly";
4,348✔
907
  _description           = "Shows a graphical burndown chart, by week";
4,348✔
908
  _read_only             = true;
4,348✔
909
  _displays_id           = false;
4,348✔
910
  _needs_gc              = true;
4,348✔
911
  _uses_context          = true;
4,348✔
912
  _accepts_filter        = true;
4,348✔
913
  _accepts_modifications = false;
4,348✔
914
  _accepts_miscellaneous = false;
4,348✔
915
  _category              = Command::Category::graphs;
4,348✔
916
}
4,348✔
917

918
////////////////////////////////////////////////////////////////////////////////
919
int CmdBurndownWeekly::execute (std::string& output)
1✔
920
{
921
  int rc = 0;
1✔
922

923
  // Scan the pending tasks, applying any filter.
924
  handleUntil ();
1✔
925
  handleRecurrence ();
1✔
926
  Filter filter;
1✔
927
  std::vector <Task> filtered;
1✔
928
  filter.subset (filtered);
1✔
929

930
  // Create a chart, scan the tasks, then render.
931
  Chart chart ('W');
1✔
932
  chart.scanForPeak (filtered);
1✔
933
  chart.scan (filtered);
1✔
934
  output = chart.render ();
1✔
935
  return rc;
1✔
936
}
1✔
937

938
////////////////////////////////////////////////////////////////////////////////
939
CmdBurndownDaily::CmdBurndownDaily ()
4,348✔
940
{
941
  _keyword               = "burndown.daily";
4,348✔
942
  _usage                 = "task <filter> burndown.daily";
4,348✔
943
  _description           = "Shows a graphical burndown chart, by day";
4,348✔
944
  _read_only             = true;
4,348✔
945
  _displays_id           = false;
4,348✔
946
  _needs_gc              = true;
4,348✔
947
  _uses_context          = true;
4,348✔
948
  _accepts_filter        = true;
4,348✔
949
  _accepts_modifications = false;
4,348✔
950
  _accepts_miscellaneous = false;
4,348✔
951
  _category              = Command::Category::graphs;
4,348✔
952
}
4,348✔
953

954
////////////////////////////////////////////////////////////////////////////////
955
int CmdBurndownDaily::execute (std::string& output)
3✔
956
{
957
  int rc = 0;
3✔
958

959
  // Scan the pending tasks, applying any filter.
960
  handleUntil ();
3✔
961
  handleRecurrence ();
3✔
962
  Filter filter;
3✔
963
  std::vector <Task> filtered;
3✔
964
  filter.subset (filtered);
3✔
965

966
  // Create a chart, scan the tasks, then render.
967
  Chart chart ('D');
3✔
968
  chart.scanForPeak (filtered);
3✔
969
  chart.scan (filtered);
3✔
970
  output = chart.render ();
3✔
971
  return rc;
3✔
972
}
3✔
973

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