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