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 <CmdStats.h>
29 #include <sstream>
30 #include <iomanip>
31 #include <stdlib.h>
32 #include <Table.h>
33 #include <Datetime.h>
34 #include <Duration.h>
35 #include <Context.h>
36 #include <Filter.h>
37 #include <main.h>
38 #include <format.h>
39 #include <util.h>
40 
41 ////////////////////////////////////////////////////////////////////////////////
CmdStats()42 CmdStats::CmdStats ()
43 {
44   _keyword               = "stats";
45   _usage                 = "task <filter> stats";
46   _description           = "Shows task database statistics";
47   _read_only             = true;
48   _displays_id           = false;
49   _needs_gc              = true;
50   _uses_context          = true;
51   _accepts_filter        = true;
52   _accepts_modifications = false;
53   _accepts_miscellaneous = false;
54   _category              = Command::Category::metadata;
55 }
56 
57 ////////////////////////////////////////////////////////////////////////////////
execute(std::string & output)58 int CmdStats::execute (std::string& output)
59 {
60   int rc = 0;
61   std::stringstream out;
62 
63   std::string dateformat = Context::getContext ().config.get ("dateformat");
64 
65   // Go get the file sizes.
66   size_t dataSize = Context::getContext ().tdb2.pending._file.size ()
67                   + Context::getContext ().tdb2.completed._file.size ()
68                   + Context::getContext ().tdb2.undo._file.size ()
69                   + Context::getContext ().tdb2.backlog._file.size ();
70 
71   // Count the undo transactions.
72   std::vector <std::string> undoTxns = Context::getContext ().tdb2.undo.get_lines ();
73   int undoCount = std::count(undoTxns.begin(), undoTxns.end(), "---");
74 
75   // Count the backlog transactions.
76   std::vector <std::string> backlogTxns = Context::getContext ().tdb2.backlog.get_lines ();
77   int backlogCount = std::count_if(backlogTxns.begin(), backlogTxns.end(), [](const auto& tx){ return tx.front() == '{'; });
78 
79   // Get all the tasks.
80   Filter filter;
81   std::vector <Task> all = Context::getContext ().tdb2.all_tasks ();
82   std::vector <Task> filtered;
83   filter.subset (all, filtered);
84 
85   Datetime now;
86   time_t earliest   = time (nullptr);
87   time_t latest     = 1;
88   int totalT        = 0;
89   int deletedT      = 0;
90   int pendingT      = 0;
91   int completedT    = 0;
92   int waitingT      = 0;
93   int taggedT       = 0;
94   int annotationsT  = 0;
95   int recurringT    = 0;
96   int blockingT     = 0;
97   int blockedT      = 0;
98   float daysPending = 0.0;
99   int descLength    = 0;
100   std::map <std::string, int> allTags;
101   std::map <std::string, int> allProjects;
102 
103   for (auto& task : filtered)
104   {
105     ++totalT;
106 
107     Task::status status = task.getStatus ();
108     switch (status)
109     {
110     case Task::deleted:   ++deletedT;   break;
111     case Task::pending:   ++pendingT;   break;
112     case Task::completed: ++completedT; break;
113     case Task::recurring: ++recurringT; break;
114     case Task::waiting:   ++waitingT;   break;
115     }
116 
117     if (task.is_blocked)  ++blockedT;
118     if (task.is_blocking) ++blockingT;
119 
120     time_t entry = strtol (task.get ("entry").c_str (), nullptr, 10);
121     if (entry < earliest) earliest = entry;
122     if (entry > latest)   latest   = entry;
123 
124     if (status == Task::completed)
125     {
126       time_t end = strtol (task.get ("end").c_str (), nullptr, 10);
127       daysPending += (end - entry) / 86400.0;
128     }
129 
130     if (status == Task::pending)
131       daysPending += (now.toEpoch () - entry) / 86400.0;
132 
133     descLength += task.get ("description").length ();
134     annotationsT += task.getAnnotations ().size ();
135 
136     auto tags = task.getTags ();
137     if (tags.size ())
138       ++taggedT;
139 
140     for (auto& tag : tags)
141       allTags[tag] = 0;
142 
143     std::string project = task.get ("project");
144     if (project != "")
145       allProjects[project] = 0;
146   }
147 
148   // Create a table for output.
149   Table view;
150   view.width (Context::getContext ().getWidth ());
151   view.intraPadding (2);
152   view.add ("Category");
153   view.add ("Data");
154   setHeaderUnderline (view);
155 
156   int row = view.addRow ();
157   view.set (row, 0, "Pending");
158   view.set (row, 1, pendingT);
159 
160   row = view.addRow ();
161   view.set (row, 0, "Waiting");
162   view.set (row, 1, waitingT);
163 
164   row = view.addRow ();
165   view.set (row, 0, "Recurring");
166   view.set (row, 1, recurringT);
167 
168   row = view.addRow ();
169   view.set (row, 0, "Completed");
170   view.set (row, 1, completedT);
171 
172   row = view.addRow ();
173   view.set (row, 0, "Deleted");
174   view.set (row, 1, deletedT);
175 
176   row = view.addRow ();
177   view.set (row, 0, "Total");
178   view.set (row, 1, totalT);
179 
180   row = view.addRow ();
181   view.set (row, 0, "Annotations");
182   view.set (row, 1, annotationsT);
183 
184   row = view.addRow ();
185   view.set (row, 0, "Unique tags");
186   view.set (row, 1, (int)allTags.size ());
187 
188   row = view.addRow ();
189   view.set (row, 0, "Projects");
190   view.set (row, 1, (int)allProjects.size ());
191 
192   row = view.addRow ();
193   view.set (row, 0, "Blocked tasks");
194   view.set (row, 1, blockedT);
195 
196   row = view.addRow ();
197   view.set (row, 0, "Blocking tasks");
198   view.set (row, 1, blockingT);
199 
200   row = view.addRow ();
201   view.set (row, 0, "Data size");
202   view.set (row, 1, formatBytes (dataSize));
203 
204   row = view.addRow ();
205   view.set (row, 0, "Undo transactions");
206   view.set (row, 1, undoCount);
207 
208   row = view.addRow ();
209   view.set (row, 0, "Sync backlog transactions");
210   view.set (row, 1, backlogCount);
211 
212   if (totalT)
213   {
214     row = view.addRow ();
215     view.set (row, 0, "Tasks tagged");
216 
217     std::stringstream value;
218     value << std::setprecision (3) << (100.0 * taggedT / totalT) << '%';
219     view.set (row, 1, value.str ());
220   }
221 
222   if (filtered.size ())
223   {
224     Datetime e (earliest);
225     row = view.addRow ();
226     view.set (row, 0, "Oldest task");
227     view.set (row, 1, e.toString (dateformat));
228 
229     Datetime l (latest);
230     row = view.addRow ();
231     view.set (row, 0, "Newest task");
232     view.set (row, 1, l.toString (dateformat));
233 
234     row = view.addRow ();
235     view.set (row, 0, "Task used for");
236     view.set (row, 1, Duration (latest - earliest).formatVague ());
237   }
238 
239   if (totalT)
240   {
241     row = view.addRow ();
242     view.set (row, 0, "Task added every");
243     view.set (row, 1, Duration (((latest - earliest) / totalT)).formatVague ());
244   }
245 
246   if (completedT)
247   {
248     row = view.addRow ();
249     view.set (row, 0, "Task completed every");
250     view.set (row, 1, Duration ((latest - earliest) / completedT).formatVague ());
251   }
252 
253   if (deletedT)
254   {
255     row = view.addRow ();
256     view.set (row, 0, "Task deleted every");
257     view.set (row, 1, Duration ((latest - earliest) / deletedT).formatVague ());
258   }
259 
260   if (pendingT || completedT)
261   {
262     row = view.addRow ();
263     view.set (row, 0, "Average time pending");
264     view.set (row, 1, Duration ((int) ((daysPending / (pendingT + completedT)) * 86400)).formatVague ());
265   }
266 
267   if (totalT)
268   {
269     row = view.addRow ();
270     view.set (row, 0, "Average desc length");
271     view.set (row, 1, format ("{1} characters", (int) (descLength / totalT)));
272   }
273 
274   // If an alternating row color is specified, notify the table.
275   if (Context::getContext ().color ())
276   {
277     Color alternate (Context::getContext ().config.get ("color.alternate"));
278     if (alternate.nontrivial ())
279     {
280       view.colorOdd (alternate);
281       view.intraColorOdd (alternate);
282       view.extraColorOdd (alternate);
283     }
284   }
285 
286   out << optionalBlankLine ()
287       << view.render ()
288       << optionalBlankLine ();
289 
290   output = out.str ();
291   return rc;
292 }
293 
294 ////////////////////////////////////////////////////////////////////////////////
295