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 <CmdHistory.h>
29 #include <sstream>
30 #include <Context.h>
31 #include <Filter.h>
32 #include <Table.h>
33 #include <main.h>
34 #include <format.h>
35 #include <util.h>
36 #include <Datetime.h>
37 
38 #define STRING_CMD_HISTORY_YEAR      "Year"
39 #define STRING_CMD_HISTORY_MONTH     "Month"
40 #define STRING_CMD_HISTORY_DAY       "Day"
41 #define STRING_CMD_HISTORY_ADDED     "Added"
42 #define STRING_CMD_HISTORY_COMP      "Completed"
43 #define STRING_CMD_HISTORY_DEL       "Deleted"
44 
45 ////////////////////////////////////////////////////////////////////////////////
46 template<class HistoryStrategy>
CmdHistoryBase()47 CmdHistoryBase<HistoryStrategy>::CmdHistoryBase ()
48 {
49   _keyword               = HistoryStrategy::keyword;
50   _usage                 = HistoryStrategy::usage;
51   _description           = HistoryStrategy::description;
52 
53   _read_only             = true;
54   _displays_id           = false;
55   _needs_gc              = false;
56   _uses_context          = true;
57   _accepts_filter        = true;
58   _accepts_modifications = false;
59   _accepts_miscellaneous = false;
60   _category              = Command::Category::graphs;
61 }
62 
63 ////////////////////////////////////////////////////////////////////////////////
64 template<class HistoryStrategy>
outputGraphical(std::string & output)65 void CmdHistoryBase<HistoryStrategy>::outputGraphical (std::string& output)
66 {
67   auto widthOfBar = Context::getContext ().getWidth () - HistoryStrategy::labelWidth;
68 
69   // Now build the view.
70   Table view;
71   setHeaderUnderline (view);
72   view.width (Context::getContext ().getWidth ());
73 
74   HistoryStrategy::setupTableDates (view);
75 
76   view.add ("Number Added/Completed/Deleted", true, false); // Fixed.
77 
78   Color color_add    (Context::getContext ().config.get ("color.history.add"));
79   Color color_done   (Context::getContext ().config.get ("color.history.done"));
80   Color color_delete (Context::getContext ().config.get ("color.history.delete"));
81   Color label        (Context::getContext ().config.get ("color.label"));
82 
83   // Determine the longest line, and the longest "added" line.
84   auto maxAddedLine = 0;
85   auto maxRemovedLine = 0;
86   for (auto& i : groups)
87   {
88     if (completedGroup[i.first] + deletedGroup[i.first] > maxRemovedLine)
89       maxRemovedLine = completedGroup[i.first] + deletedGroup[i.first];
90 
91     if (addedGroup[i.first] > maxAddedLine)
92       maxAddedLine = addedGroup[i.first];
93   }
94 
95   auto maxLine = maxAddedLine + maxRemovedLine;
96   if (maxLine > 0)
97   {
98     unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine;
99 
100     auto totalAdded     = 0;
101     auto totalCompleted = 0;
102     auto totalDeleted   = 0;
103 
104     time_t priorTime = 0;
105     auto row = 0;
106     for (auto& i : groups)
107     {
108       row = view.addRow ();
109 
110       totalAdded     += addedGroup[i.first];
111       totalCompleted += completedGroup[i.first];
112       totalDeleted   += deletedGroup[i.first];
113 
114       HistoryStrategy::insertRowDate (view, row, i.first, priorTime);
115       priorTime = i.first;
116 
117       unsigned int addedBar     = (widthOfBar *     addedGroup[i.first]) / maxLine;
118       unsigned int completedBar = (widthOfBar * completedGroup[i.first]) / maxLine;
119       unsigned int deletedBar   = (widthOfBar *   deletedGroup[i.first]) / maxLine;
120 
121       std::string bar;
122       if (Context::getContext ().color ())
123       {
124         std::string aBar;
125         if (addedGroup[i.first])
126         {
127           aBar = format (addedGroup[i.first]);
128           while (aBar.length () < addedBar)
129             aBar = ' ' + aBar;
130         }
131 
132         std::string cBar;
133         if (completedGroup[i.first])
134         {
135           cBar = format (completedGroup[i.first]);
136           while (cBar.length () < completedBar)
137             cBar = ' ' + cBar;
138         }
139 
140         std::string dBar;
141         if (deletedGroup[i.first])
142         {
143           dBar = format (deletedGroup[i.first]);
144           while (dBar.length () < deletedBar)
145             dBar = ' ' + dBar;
146         }
147 
148         bar += std::string (leftOffset - aBar.length (), ' ');
149         bar += color_add.colorize    (aBar);
150         bar += color_done.colorize   (cBar);
151         bar += color_delete.colorize (dBar);
152       }
153       else
154       {
155         std::string aBar; while (aBar.length () < addedBar)     aBar += '+';
156         std::string cBar; while (cBar.length () < completedBar) cBar += 'X';
157         std::string dBar; while (dBar.length () < deletedBar)   dBar += '-';
158 
159         bar += std::string (leftOffset - aBar.length (), ' ');
160         bar += aBar + cBar + dBar;
161       }
162 
163       view.set (row, HistoryStrategy::dateFieldCount + 0, bar);
164     }
165   }
166 
167   std::stringstream out;
168   if (view.rows ())
169   {
170     out << optionalBlankLine ()
171         << view.render ()
172         << '\n';
173 
174     if (Context::getContext ().color ())
175       out << format ("Legend: {1}, {2}, {3}",
176                      color_add.colorize (STRING_CMD_HISTORY_ADDED),
177                      color_done.colorize (STRING_CMD_HISTORY_COMP),
178                      color_delete.colorize (STRING_CMD_HISTORY_DEL))
179           << optionalBlankLine ()
180           << '\n';
181     else
182       out << "Legend: + Added, X Completed, - Deleted\n";
183   }
184   else
185   {
186     Context::getContext ().footnote ("No tasks.");
187     rc = 1;
188   }
189 
190   output = out.str ();
191 }
192 
193 ////////////////////////////////////////////////////////////////////////////////
194 template<class HistoryStrategy>
outputTabular(std::string & output)195 void CmdHistoryBase<HistoryStrategy>::outputTabular (std::string& output)
196 {
197   Table view;
198   setHeaderUnderline (view);
199   view.width (Context::getContext ().getWidth ());
200 
201   HistoryStrategy::setupTableDates (view);
202 
203   view.add (STRING_CMD_HISTORY_ADDED, false);
204   view.add (STRING_CMD_HISTORY_COMP,  false);
205   view.add (STRING_CMD_HISTORY_DEL,   false);
206   view.add ("Net",                    false);
207 
208   auto totalAdded     = 0;
209   auto totalCompleted = 0;
210   auto totalDeleted   = 0;
211 
212   auto row = 0;
213   time_t lastTime = 0;
214   for (auto& i : groups)
215   {
216     row = view.addRow ();
217 
218     totalAdded     += addedGroup     [i.first];
219     totalCompleted += completedGroup [i.first];
220     totalDeleted   += deletedGroup   [i.first];
221 
222     HistoryStrategy::insertRowDate (view, row, i.first, lastTime);
223     lastTime = i.first;
224 
225     auto net = 0;
226 
227     if (addedGroup.find (i.first) != addedGroup.end ())
228     {
229       view.set (row, HistoryStrategy::dateFieldCount + 0, addedGroup[i.first]);
230       net +=addedGroup[i.first];
231     }
232 
233     if (completedGroup.find (i.first) != completedGroup.end ())
234     {
235       view.set (row, HistoryStrategy::dateFieldCount + 1, completedGroup[i.first]);
236       net -= completedGroup[i.first];
237     }
238 
239     if (deletedGroup.find (i.first) != deletedGroup.end ())
240     {
241       view.set (row, HistoryStrategy::dateFieldCount + 2, deletedGroup[i.first]);
242       net -= deletedGroup[i.first];
243     }
244 
245     Color net_color;
246     if (Context::getContext ().color () && net)
247       net_color = net > 0
248                     ? Color (Color::red)
249                     : Color (Color::green);
250 
251     view.set (row, HistoryStrategy::dateFieldCount + 3, net, net_color);
252   }
253 
254   if (view.rows ())
255   {
256     row = view.addRow();
257     view.set (row, 1, " ");
258     row = view.addRow ();
259 
260     Color row_color;
261     if (Context::getContext ().color ())
262       row_color = Color (Color::nocolor, Color::nocolor, false, true, false);
263 
264     view.set (row, HistoryStrategy::dateFieldCount - 1, "Average", row_color);
265     view.set (row, HistoryStrategy::dateFieldCount + 0, totalAdded     / (view.rows () - 2), row_color);
266     view.set (row, HistoryStrategy::dateFieldCount + 1, totalCompleted / (view.rows () - 2), row_color);
267     view.set (row, HistoryStrategy::dateFieldCount + 2, totalDeleted   / (view.rows () - 2), row_color);
268     view.set (row, HistoryStrategy::dateFieldCount + 3, (totalAdded - totalCompleted - totalDeleted) / (view.rows () - 2), row_color);
269   }
270 
271   std::stringstream out;
272   if (view.rows ())
273     out << optionalBlankLine ()
274         << view.render ()
275         << '\n';
276   else
277   {
278     Context::getContext ().footnote ("No tasks.");
279     rc = 1;
280   }
281 
282   output = out.str ();
283 }
284 
285 ////////////////////////////////////////////////////////////////////////////i
286 class MonthlyHistoryStrategy
287 {
288 public:
getRelevantDate(const Datetime & dt)289   static Datetime getRelevantDate (const Datetime& dt)
290   {
291     return dt.startOfMonth ();
292   }
293 
setupTableDates(Table & view)294   static void setupTableDates (Table& view)
295   {
296     view.add (STRING_CMD_HISTORY_YEAR,  true);
297     view.add (STRING_CMD_HISTORY_MONTH, true);
298   }
299 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)300   static void insertRowDate (
301     Table& view,
302     int row,
303     time_t rowTime,
304     time_t lastTime)
305   {
306     Datetime dt (rowTime);
307     int m, d, y;
308     dt.toYMD (y, m, d);
309 
310     Datetime last_dt (lastTime);
311     int last_m, last_d, last_y;
312     last_dt.toYMD (last_y, last_m, last_d);
313 
314     if (y != last_y)
315       view.set (row, 0, y);
316 
317     view.set (row, 1, Datetime::monthName (m));
318   }
319 
320   static constexpr const char* keyword         = "history.monthly";
321   static constexpr const char* usage           = "task <filter> history.monthly";
322   static constexpr const char* description     = "Shows a report of task history, by month";
323   static constexpr unsigned int dateFieldCount = 2;
324   static constexpr bool graphical              = false;
325   static constexpr unsigned int labelWidth     = 0;  // unused.
326 };
327 
328 ////////////////////////////////////////////////////////////////////////////////
329 template<class HistoryStrategy>
execute(std::string & output)330 int CmdHistoryBase<HistoryStrategy>::execute (std::string& output)
331 {
332   rc = 0;
333 
334   // TODO is this necessary?
335   groups.clear ();
336   addedGroup.clear ();
337   deletedGroup.clear ();
338   completedGroup.clear ();
339 
340   // Apply filter.
341   handleUntil ();
342   handleRecurrence ();
343   Filter filter;
344   std::vector <Task> filtered;
345   filter.subset (filtered);
346 
347   for (auto& task : filtered)
348   {
349     Datetime entry (task.get_date ("entry"));
350 
351     Datetime end;
352     if (task.has ("end"))
353       end = Datetime (task.get_date ("end"));
354 
355     auto epoch = HistoryStrategy::getRelevantDate (entry).toEpoch ();
356     groups[epoch] = 0;
357 
358     // Every task has an entry date, but exclude templates.
359     if (task.getStatus () != Task::recurring)
360       ++addedGroup[epoch];
361 
362     // All deleted tasks have an end date.
363     if (task.getStatus () == Task::deleted)
364     {
365       epoch = HistoryStrategy::getRelevantDate (end).toEpoch ();
366       groups[epoch] = 0;
367       ++deletedGroup[epoch];
368     }
369 
370     // All completed tasks have an end date.
371     else if (task.getStatus () == Task::completed)
372     {
373       epoch = HistoryStrategy::getRelevantDate (end).toEpoch ();
374       groups[epoch] = 0;
375       ++completedGroup[epoch];
376     }
377   }
378 
379   // Now build the view.
380   if (HistoryStrategy::graphical)
381     this->outputGraphical (output);
382   else
383     this->outputTabular (output);
384 
385   return rc;
386 }
387 
388 ////////////////////////////////////////////////////////////////////////////i
389 class MonthlyGHistoryStrategy
390 {
391 public:
getRelevantDate(const Datetime & dt)392   static Datetime getRelevantDate (const Datetime& dt)
393   {
394     return dt.startOfMonth ();
395   }
396 
setupTableDates(Table & view)397   static void setupTableDates (Table& view)
398   {
399     view.add (STRING_CMD_HISTORY_YEAR,  true);
400     view.add (STRING_CMD_HISTORY_MONTH, true);
401   }
402 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)403   static void insertRowDate (
404     Table& view,
405      int row,
406      time_t rowTime,
407      time_t lastTime)
408   {
409     Datetime dt (rowTime);
410     int m, d, y;
411     dt.toYMD (y, m, d);
412 
413     Datetime last_dt (lastTime);
414     int last_m, last_d, last_y;
415     last_dt.toYMD (last_y, last_m, last_d);
416 
417     if (y != last_y)
418       view.set (row, 0, y);
419 
420     view.set (row, 1, Datetime::monthName (m));
421   }
422 
423   static constexpr const char* keyword         = "ghistory.monthly";
424   static constexpr const char* usage           = "task <filter> ghistory.monthly";
425   static constexpr const char* description     = "Shows a graphical report of task history, by month";
426   static constexpr unsigned int dateFieldCount = 2;
427   static constexpr bool graphical              = true;
428   static constexpr unsigned int labelWidth     = 15;  // length '2017 September ' = 15
429 };
430 
431 ////////////////////////////////////////////////////////////////////////////i
432 class AnnualGHistoryStrategy
433 {
434 public:
getRelevantDate(const Datetime & dt)435   static Datetime getRelevantDate (const Datetime& dt)
436   {
437     return dt.startOfYear ();
438   }
439 
setupTableDates(Table & view)440   static void setupTableDates (Table& view)
441   {
442     view.add (STRING_CMD_HISTORY_YEAR, true);
443   }
444 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)445   static void insertRowDate (
446     Table& view,
447     int row,
448     time_t rowTime,
449     time_t lastTime)
450   {
451     Datetime dt (rowTime);
452     int m, d, y;
453     dt.toYMD (y, m, d);
454 
455     Datetime last_dt (lastTime);
456     int last_m, last_d, last_y;
457     last_dt.toYMD (last_y, last_m, last_d);
458 
459     if (y != last_y)
460       view.set (row, 0, y);
461   }
462 
463   static constexpr const char* keyword         = "ghistory.annual";
464   static constexpr const char* usage           = "task <filter> ghistory.annual";
465   static constexpr const char* description     = "Shows a graphical report of task history, by year";
466   static constexpr unsigned int dateFieldCount = 1;
467   static constexpr bool graphical              = true;
468   static constexpr unsigned int labelWidth     = 5;  // length '2017 ' = 5
469 };
470 
471 ////////////////////////////////////////////////////////////////////////////i
472 class AnnualHistoryStrategy
473 {
474 public:
getRelevantDate(const Datetime & dt)475   static Datetime getRelevantDate (const Datetime& dt)
476   {
477     return dt.startOfYear ();
478   }
479 
setupTableDates(Table & view)480   static void setupTableDates (Table& view)
481   {
482     view.add (STRING_CMD_HISTORY_YEAR, true);
483   }
484 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)485   static void insertRowDate (
486     Table& view,
487      int row,
488      time_t rowTime,
489      time_t lastTime)
490   {
491     Datetime dt (rowTime);
492     int m, d, y;
493     dt.toYMD (y, m, d);
494 
495     Datetime last_dt (lastTime);
496     int last_m, last_d, last_y;
497     last_dt.toYMD (last_y, last_m, last_d);
498 
499     if (y != last_y)
500       view.set (row, 0, y);
501   }
502 
503   static constexpr const char* keyword         = "history.annual";
504   static constexpr const char* usage           = "task <filter> history.annual";
505   static constexpr const char* description     = "Shows a report of task history, by year";
506   static constexpr unsigned int dateFieldCount = 1;
507   static constexpr bool graphical              = false;
508   static constexpr unsigned int labelWidth     = 0;  // unused.
509 };
510 
511 
512 ////////////////////////////////////////////////////////////////////////////i
513 class DailyHistoryStrategy
514 {
515 public:
getRelevantDate(const Datetime & dt)516   static Datetime getRelevantDate (const Datetime& dt)
517   {
518     return dt.startOfDay ();
519   }
520 
setupTableDates(Table & view)521   static void setupTableDates (Table& view)
522   {
523     view.add (STRING_CMD_HISTORY_YEAR,  true);
524     view.add (STRING_CMD_HISTORY_MONTH, true);
525     view.add (STRING_CMD_HISTORY_DAY,   false);
526   }
527 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)528   static void insertRowDate (
529     Table& view,
530     int row,
531     time_t rowTime,
532     time_t lastTime)
533   {
534     Datetime dt (rowTime);
535     int m, d, y;
536     dt.toYMD (y, m, d);
537 
538     Datetime last_dt (lastTime);
539     int last_m, last_d, last_y;
540     last_dt.toYMD (last_y, last_m, last_d);
541 
542     bool y_changed = (y != last_y) || (lastTime == 0);
543     bool m_changed = (m != last_m) || (lastTime == 0);
544 
545     if (y_changed)
546       view.set (row, 0, y);
547 
548     if (y_changed || m_changed)
549       view.set (row, 1, Datetime::monthName (m));
550 
551     view.set (row, 2, d);
552   }
553 
554   static constexpr const char* keyword         = "history.daily";
555   static constexpr const char* usage           = "task <filter> history.daily";
556   static constexpr const char* description     = "Shows a report of task history, by day";
557   static constexpr unsigned int dateFieldCount = 3;
558   static constexpr bool graphical              = false;
559   static constexpr unsigned int labelWidth     = 0;  // unused.
560 };
561 
562 ////////////////////////////////////////////////////////////////////////////i
563 class DailyGHistoryStrategy
564 {
565 public:
getRelevantDate(const Datetime & dt)566   static Datetime getRelevantDate (const Datetime& dt)
567   {
568     return dt.startOfDay ();
569   }
570 
setupTableDates(Table & view)571   static void setupTableDates (Table& view)
572   {
573     view.add (STRING_CMD_HISTORY_YEAR,  true);
574     view.add (STRING_CMD_HISTORY_MONTH, true);
575     view.add (STRING_CMD_HISTORY_DAY,   false);
576   }
577 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)578   static void insertRowDate (
579     Table& view,
580      int row,
581      time_t rowTime,
582      time_t lastTime)
583   {
584     Datetime dt (rowTime);
585     int m, d, y;
586     dt.toYMD (y, m, d);
587 
588     Datetime last_dt (lastTime);
589     int last_m, last_d, last_y;
590     last_dt.toYMD (last_y, last_m, last_d);
591 
592     bool y_changed = (y != last_y) || (lastTime == 0);
593     bool m_changed = (m != last_m) || (lastTime == 0);
594 
595     if (y_changed)
596       view.set (row, 0, y);
597 
598     if (y_changed || m_changed)
599       view.set (row, 1, Datetime::monthName (m));
600 
601     view.set (row, 2, d);
602   }
603 
604   static constexpr const char* keyword         = "ghistory.daily";
605   static constexpr const char* usage           = "task <filter> ghistory.daily";
606   static constexpr const char* description     = "Shows a graphical report of task history, by day";
607   static constexpr unsigned int dateFieldCount = 3;
608   static constexpr bool graphical              = true;
609   static constexpr unsigned int labelWidth     = 19;  // length '2017 September Day ' = 19
610 };
611 
612 ////////////////////////////////////////////////////////////////////////////i
613 class WeeklyHistoryStrategy
614 {
615 public:
getRelevantDate(const Datetime & dt)616   static Datetime getRelevantDate (const Datetime& dt)
617   {
618     return dt.startOfWeek ();
619   }
620 
setupTableDates(Table & view)621   static void setupTableDates (Table& view)
622   {
623     view.add (STRING_CMD_HISTORY_YEAR,  true);
624     view.add (STRING_CMD_HISTORY_MONTH, true);
625     view.add (STRING_CMD_HISTORY_DAY,   false);
626   }
627 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)628   static void insertRowDate (
629     Table& view,
630     int row,
631     time_t rowTime,
632     time_t lastTime)
633   {
634     Datetime dt (rowTime);
635     int m, d, y;
636     dt.toYMD (y, m, d);
637 
638     Datetime last_dt (lastTime);
639     int last_m, last_d, last_y;
640     last_dt.toYMD (last_y, last_m, last_d);
641 
642     bool y_changed = (y != last_y) || (lastTime == 0);
643     bool m_changed = (m != last_m) || (lastTime == 0);
644 
645     if (y_changed)
646       view.set (row, 0, y);
647 
648     if (y_changed || m_changed)
649       view.set (row, 1, Datetime::monthName (m));
650 
651     view.set (row, 2, d);
652   }
653 
654   static constexpr const char* keyword         = "history.weekly";
655   static constexpr const char* usage           = "task <filter> history.weekly";
656   static constexpr const char* description     = "Shows a report of task history, by week";
657   static constexpr unsigned int dateFieldCount = 3;
658   static constexpr bool graphical              = false;
659   static constexpr unsigned int labelWidth     = 0;  // unused.
660 };
661 
662 ////////////////////////////////////////////////////////////////////////////i
663 class WeeklyGHistoryStrategy
664 {
665 public:
getRelevantDate(const Datetime & dt)666   static Datetime getRelevantDate (const Datetime& dt)
667   {
668     return dt.startOfWeek ();
669   }
670 
setupTableDates(Table & view)671   static void setupTableDates (Table& view)
672   {
673     view.add (STRING_CMD_HISTORY_YEAR,  true);
674     view.add (STRING_CMD_HISTORY_MONTH, true);
675     view.add (STRING_CMD_HISTORY_DAY,   false);
676   }
677 
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)678   static void insertRowDate (
679     Table& view,
680      int row,
681      time_t rowTime,
682      time_t lastTime)
683   {
684     Datetime dt (rowTime);
685     int m, d, y;
686     dt.toYMD (y, m, d);
687 
688     Datetime last_dt (lastTime);
689     int last_m, last_d, last_y;
690     last_dt.toYMD (last_y, last_m, last_d);
691 
692     bool y_changed = (y != last_y) || (lastTime == 0);
693     bool m_changed = (m != last_m) || (lastTime == 0);
694 
695     if (y_changed)
696       view.set (row, 0, y);
697 
698     if (y_changed || m_changed)
699       view.set (row, 1, Datetime::monthName (m));
700 
701     view.set (row, 2, d);
702   }
703 
704   static constexpr const char* keyword         = "ghistory.weekly";
705   static constexpr const char* usage           = "task <filter> ghistory.weekly";
706   static constexpr const char* description     = "Shows a graphical report of task history, by week";
707   static constexpr unsigned int dateFieldCount = 3;
708   static constexpr bool graphical              = true;
709   static constexpr unsigned int labelWidth     = 19;  // length '2017 September Day ' = 19
710 };
711 
712 
713 // Explicit instantiations, avoiding cpp-inclusion or implementation in header
714 template class CmdHistoryBase<DailyHistoryStrategy>;
715 template class CmdHistoryBase<WeeklyHistoryStrategy>;
716 template class CmdHistoryBase<MonthlyHistoryStrategy>;
717 template class CmdHistoryBase<AnnualHistoryStrategy>;
718 template class CmdHistoryBase<DailyGHistoryStrategy>;
719 template class CmdHistoryBase<WeeklyGHistoryStrategy>;
720 template class CmdHistoryBase<MonthlyGHistoryStrategy>;
721 template class CmdHistoryBase<AnnualGHistoryStrategy>;
722 
723 ////////////////////////////////////////////////////////////////////////////////
724