1 ////////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright 2016 - 2021, Thomas Lauf, 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 <timew.h>
29 #include <shared.h>
30 #include <format.h>
31 #include <Datetime.h>
32 #include <Duration.h>
33 #include <IntervalFactory.h>
34 #include <sstream>
35 #include <iomanip>
36 #include <iostream>
37 #include <map>
38 #include <vector>
39
40 ////////////////////////////////////////////////////////////////////////////////
41 // Select a color to represent the interval.
intervalColor(const std::set<std::string> & tags,const std::map<std::string,Color> & tag_colors)42 Color intervalColor (
43 const std::set <std::string>& tags,
44 const std::map <std::string, Color>& tag_colors)
45 {
46 if (tags.empty ())
47 {
48 return tag_colors.at ("");
49 }
50
51 Color c;
52
53 for (auto& tag : tags)
54 {
55 c.blend (tag_colors.at (tag));
56 }
57
58 return c;
59 }
60
61 ////////////////////////////////////////////////////////////////////////////////
62 // Consult rules to find any defined color for the given tag, and colorize it.
tagColor(const Rules & rules,const std::string & tag)63 Color tagColor (const Rules& rules, const std::string& tag)
64 {
65 Color c;
66 std::string name = std::string ("tags.") + tag + ".color";
67 if (rules.has (name))
68 c = Color (rules.get (name));
69
70 return c;
71 }
72
73 ////////////////////////////////////////////////////////////////////////////////
74 // Summarize either an active or closed interval, for user feedback.
intervalSummarize(const Rules & rules,const Interval & interval)75 std::string intervalSummarize (const Rules& rules, const Interval& interval)
76 {
77 std::stringstream out;
78
79 if (interval.is_started ())
80 {
81 Duration total (interval.total ());
82
83 // Combine and colorize tags.
84 std::string tags;
85 for (auto& tag : interval.tags ())
86 {
87 if (! tags.empty ())
88 {
89 tags += " ";
90 }
91
92 tags += tagColor (rules, tag).colorize (quoteIfNeeded (tag));
93 }
94
95 // Interval open.
96 if (interval.is_open ())
97 {
98 out << "Tracking " << tags << '\n'
99 << " Started " << interval.start.toISOLocalExtended () << '\n'
100 << " Current " << minimalDelta (interval.start, Datetime ()) << '\n'
101 << " Total " << std::setw (19) << std::setfill (' ') << total.formatHours () << '\n';
102 }
103
104 // Interval closed.
105 else
106 {
107 out << "Recorded " << tags << '\n'
108 << " Started " << interval.start.toISOLocalExtended () << '\n'
109 << " Ended " << minimalDelta (interval.start, interval.end) << '\n'
110 << " Total " << std::setw (19) << std::setfill (' ') << total.formatHours () << '\n';
111 }
112 }
113
114 return out.str ();
115 }
116
117 ////////////////////////////////////////////////////////////////////////////////
118 // Convert a set of hints to equivalent date ranges.
expandIntervalHint(const std::string & hint,Range & range)119 bool expandIntervalHint (
120 const std::string& hint,
121 Range& range)
122 {
123 static std::map <std::string, std::vector <std::string>> hints
124 {
125 {":yesterday", {"yesterday", "today"}},
126 {":day", {"today", "eod"}},
127 {":week", {"sow", "eow"}},
128 {":fortnight", {"sopw", "eow"}},
129 {":month", {"som", "eom"}},
130 {":quarter", {"soq", "eoq"}},
131 {":year", {"soy", "eoy"}},
132 };
133
134 static std::vector <std::string> dayNames
135 {
136 ":sunday",
137 ":monday",
138 ":tuesday",
139 ":wednesday",
140 ":thursday",
141 ":friday",
142 ":saturday"
143 };
144
145 // Some hints are just synonyms.
146 if (hints.find (hint) != hints.end ())
147 {
148 range.start = Datetime (hints[hint][0]);
149 range.end = Datetime (hints[hint][1]);
150 debug (format ("Hint {1} expanded to {2} - {3}",
151 hint,
152 range.start.toISOLocalExtended (),
153 range.end.toISOLocalExtended ()));
154 return true;
155 }
156
157 if (hint == ":all")
158 {
159 range.start = 0;
160 range.end = 0;
161
162 return true;
163 }
164
165 // Some require math.
166 if (hint == ":lastweek")
167 {
168 // Note: Simply subtracting (7 * 86400) from sow, eow fails to consider
169 // daylight savings.
170 Datetime sow ("sow");
171 int sy = sow.year ();
172 int sm = sow.month ();
173 int sd = sow.day ();
174
175 Datetime eow ("eow");
176 int ey = eow.year ();
177 int em = eow.month ();
178 int ed = eow.day ();
179
180 sd -= 7;
181 if (sd < 1)
182 {
183 --sm;
184 if (sm < 1)
185 {
186 --sy;
187 sm = 12;
188 }
189
190 sd += Datetime::daysInMonth (sy, sm);
191 }
192
193 ed -= 7;
194 if (ed < 1)
195 {
196 --em;
197 if (em < 1)
198 {
199 --ey;
200 em = 12;
201 }
202
203 ed += Datetime::daysInMonth (ey, em);
204 }
205
206 range.start = Datetime (sy, sm, sd);
207 range.end = Datetime (ey, em, ed);
208 debug (format ("Hint {1} expanded to {2} - {3}",
209 hint,
210 range.start.toISOLocalExtended (),
211 range.end.toISOLocalExtended ()));
212 return true;
213 }
214 else if (hint == ":lastmonth")
215 {
216 Datetime now;
217 int y = now.year ();
218 int y_prev = y;
219
220 int m = now.month ();
221 int m_prev = m - 1;
222
223 if (m_prev == 0)
224 {
225 m_prev = 12;
226 --y_prev;
227 }
228
229 range.start = Datetime (y_prev, m_prev, 1);
230 range.end = Datetime (y, m, 1);
231 debug (format ("Hint {1} expanded to {2} - {3}",
232 hint,
233 range.start.toISOLocalExtended (),
234 range.end.toISOLocalExtended ()));
235 return true;
236 }
237 else if (hint == ":lastquarter")
238 {
239 Datetime now;
240 int y = now.year ();
241 int m = now.month ();
242 int q = ((m - 1) / 3) + 1;
243
244 if (--q == 0)
245 {
246 q = 4;
247 --y;
248 }
249
250 m = ((q - 1) * 3) + 1;
251
252 range.start = Datetime (y, m, 1);
253
254 m += 3;
255 y += m/12;
256 m %= 12;
257
258 range.end = Datetime (y, m, 1);
259
260 debug (format ("Hint {1} expanded to {2} - {3}",
261 hint,
262 range.start.toISOLocalExtended (),
263 range.end.toISOLocalExtended ()));
264 return true;
265 }
266 else if (hint == ":lastyear")
267 {
268 Datetime now;
269 range.start = Datetime (now.year () - 1, 1, 1);
270 range.end = Datetime (now.year (), 1, 1);
271 debug (format ("Hint {1} expanded to {2} - {3}",
272 hint,
273 range.start.toISOLocalExtended (),
274 range.end.toISOLocalExtended ()));
275 return true;
276 }
277 else if (std::find (dayNames.begin (), dayNames.end (), hint) != dayNames.end ())
278 {
279 int wd = std::find (dayNames.begin (), dayNames.end (), hint) - dayNames.begin ();
280
281 Datetime now;
282 int dow = now.dayOfWeek ();
283 Datetime sd = now - (86400 * dow) + (86400 * (wd - 7 * (wd <= dow ? 0 : 1)));
284 Datetime ed = sd + 86400;
285
286 range.start = Datetime (sd.year(), sd.month(), sd.day());
287 range.end = Datetime (ed.year(), ed.month(), ed.day());
288
289 debug (format ("Hint {1} expanded to {2} - {3}",
290 hint,
291 range.start.toISOLocalExtended (),
292 range.end.toISOLocalExtended ()));
293 return true;
294 }
295
296 return false;
297 }
298
299 ////////////////////////////////////////////////////////////////////////////////
300 // Compose a JSON document of intervals. In the trivial case:
301 // [
302 // ]
303 //
304 // In the non-trivial case:
305 // [
306 // {...},
307 // {...}
308 // ]
309 //
jsonFromIntervals(const std::vector<Interval> & intervals)310 std::string jsonFromIntervals (const std::vector <Interval>& intervals)
311 {
312 std::stringstream out;
313
314 out << "[\n";
315 int counter = 0;
316 for (auto& interval : intervals)
317 {
318 if (counter)
319 out << ",\n";
320
321 out << interval.json ();
322 ++counter;
323 }
324
325 if (counter)
326 out << '\n';
327
328 out << "]\n";
329 return out.str ();
330 }
331
332 ////////////////////////////////////////////////////////////////////////////////
createPalette(const Rules & rules)333 Palette createPalette (const Rules& rules)
334 {
335 Palette p;
336 auto colors = rules.all ("theme.palette.color");
337
338 if (! colors.empty ())
339 {
340 p.clear ();
341 for (auto& c : colors)
342 p.add (Color (rules.get (c)));
343 }
344
345 p.enabled = rules.getBoolean ("color");
346 return p;
347 }
348
349 ////////////////////////////////////////////////////////////////////////////////
350 // Extract the tags from a set of intervals, and using a rotating color palette,
351 // map unique tags to color.
352 //
353 // If there is a tags.<tag>.color setting, use it.
createTagColorMap(const Rules & rules,Palette & palette,const std::vector<Interval> & intervals)354 std::map <std::string, Color> createTagColorMap (
355 const Rules& rules,
356 Palette& palette,
357 const std::vector <Interval>& intervals)
358 {
359 std::map <std::string, Color> mapping;
360
361 // Add a color for intervals without tags
362 mapping[""] = palette.next ();
363
364 for (auto& interval : intervals)
365 {
366 for (auto& tag : interval.tags ())
367 {
368 std::string custom = "tags." + tag + ".color";
369 if (rules.has (custom))
370 {
371 mapping[tag] = Color (rules.get (custom));
372 }
373 else if (mapping.find (tag) == mapping.end ())
374 {
375 mapping[tag] = palette.next ();
376 }
377 }
378 }
379
380 return mapping;
381 }
382
383 ////////////////////////////////////////////////////////////////////////////////
quantizeToNMinutes(const int minutes,const int N)384 int quantizeToNMinutes (const int minutes, const int N)
385 {
386 if (minutes % N == 0)
387 return minutes;
388
389 auto deviation = minutes % N;
390 if (deviation < N/2)
391 return minutes - deviation;
392
393 return minutes + N - deviation;
394 }
395
396 ////////////////////////////////////////////////////////////////////////////////
findHint(const CLI & cli,const std::string & hint)397 bool findHint (const CLI& cli, const std::string& hint)
398 {
399 for (auto& arg : cli._args)
400 if (arg.hasTag ("HINT") &&
401 arg.getToken () == hint)
402 return true;
403
404 return false;
405 }
406
407 ////////////////////////////////////////////////////////////////////////////////
minimalDelta(const Datetime & left,const Datetime & right)408 std::string minimalDelta (const Datetime& left, const Datetime& right)
409 {
410 std::string result = right.toISOLocalExtended ();
411
412 if (left.sameYear (right))
413 {
414 result.replace (0, 5, " ");
415 if (left.sameMonth (right))
416 {
417 result.replace (5, 3, " ");
418 if (left.sameDay (right))
419 {
420 result.replace (8, 3, " ");
421 if (left.sameHour (right))
422 {
423 result.replace (11, 3, " ");
424 if (left.minute () == right.minute ())
425 result.replace (14, 3, " ");
426 }
427 }
428 }
429 }
430
431 return result;
432 }
433
434 ////////////////////////////////////////////////////////////////////////////////
435