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