timewarrior/src/helper.cpp
Thomas Lauf 79a02f73ca Make timew summary display open intervals with future start time
Closes #491

Signed-off-by: Thomas Lauf <thomas.lauf@tngtech.com>
2022-08-17 21:12:13 +02:00

464 lines
11 KiB
C++

////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 - 2021, Thomas Lauf, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
#include <timew.h>
#include <shared.h>
#include <format.h>
#include <Datetime.h>
#include <Duration.h>
#include <IntervalFactory.h>
#include <sstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <vector>
////////////////////////////////////////////////////////////////////////////////
// Select a color to represent the interval in a summary report.
Color summaryIntervalColor (
const Rules& rules,
const std::set <std::string>& tags)
{
Color c;
for (auto& tag : tags)
{
c.blend (tagColor (rules, tag));
}
return c;
}
////////////////////////////////////////////////////////////////////////////////
// Select a color to represent the interval on a chart.
Color chartIntervalColor (
const std::set <std::string>& tags,
const std::map <std::string, Color>& tag_colors)
{
if (tags.empty ())
{
return tag_colors.at ("");
}
Color c;
for (auto& tag : tags)
{
c.blend (tag_colors.at (tag));
}
return c;
}
////////////////////////////////////////////////////////////////////////////////
// Consult rules to find any defined color for the given tag, and colorize it.
Color tagColor (const Rules& rules, const std::string& tag)
{
Color c;
std::string name = std::string ("tags.") + tag + ".color";
if (rules.has (name))
c = Color (rules.get (name));
return c;
}
////////////////////////////////////////////////////////////////////////////////
// Summarize either an active or closed interval, for user feedback.
std::string intervalSummarize (const Rules& rules, const Interval& interval)
{
std::stringstream out;
if (interval.is_started ())
{
Duration total (interval.total ());
// Combine and colorize tags.
std::string tags;
for (auto& tag : interval.tags ())
{
if (! tags.empty ())
{
tags += " ";
}
tags += tagColor (rules, tag).colorize (quoteIfNeeded (tag));
}
// Interval open.
if (interval.is_open ())
{
auto now = Datetime ();
if (interval.start <= now )
{
out << "Tracking " << tags << '\n'
<< " Started " << interval.start.toISOLocalExtended () << '\n'
<< " Current " << minimalDelta (interval.start, now) << '\n'
<< " Total " << std::setw (19) << std::setfill (' ') << total.formatHours () << '\n';
}
else
{
out << "Tracking " << tags << '\n'
<< " Starting " << interval.start.toISOLocalExtended () << '\n';
}
}
// Interval closed.
else
{
out << "Recorded " << tags << '\n'
<< " Started " << interval.start.toISOLocalExtended () << '\n'
<< " Ended " << minimalDelta (interval.start, interval.end) << '\n'
<< " Total " << std::setw (19) << std::setfill (' ') << total.formatHours () << '\n';
}
}
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////
// Convert a set of hints to equivalent date ranges.
bool expandIntervalHint (
const std::string& hint,
Range& range)
{
static std::map <std::string, std::vector <std::string>> hints
{
{":yesterday", {"sopd", "sod" }},
{":day", {"sod", "sond"}},
{":week", {"sow", "sonw"}},
{":fortnight", {"sopw", "sonw"}},
{":month", {"som", "sonm"}},
{":quarter", {"soq", "sonq"}},
{":year", {"soy", "sony"}},
};
static std::vector <std::string> dayNames
{
":sunday",
":monday",
":tuesday",
":wednesday",
":thursday",
":friday",
":saturday"
};
// Some hints are just synonyms.
if (hints.find (hint) != hints.end ())
{
range.start = Datetime (hints[hint][0]);
range.end = Datetime (hints[hint][1]);
debug (format ("Hint {1} expanded to {2} - {3}",
hint,
range.start.toISOLocalExtended (),
range.end.toISOLocalExtended ()));
return true;
}
if (hint == ":all")
{
range.start = 0;
range.end = 0;
return true;
}
// Some require math.
if (hint == ":lastweek")
{
// Note: Simply subtracting (7 * 86400) from sow, eow fails to consider
// daylight savings.
Datetime sow ("sow");
int sy = sow.year ();
int sm = sow.month ();
int sd = sow.day ();
Datetime eow ("eow");
int ey = eow.year ();
int em = eow.month ();
int ed = eow.day ();
sd -= 7;
if (sd < 1)
{
--sm;
if (sm < 1)
{
--sy;
sm = 12;
}
sd += Datetime::daysInMonth (sy, sm);
}
ed -= 7;
if (ed < 1)
{
--em;
if (em < 1)
{
--ey;
em = 12;
}
ed += Datetime::daysInMonth (ey, em);
}
range.start = Datetime (sy, sm, sd);
range.end = Datetime (ey, em, ed);
debug (format ("Hint {1} expanded to {2} - {3}",
hint,
range.start.toISOLocalExtended (),
range.end.toISOLocalExtended ()));
return true;
}
else if (hint == ":lastmonth")
{
Datetime now;
int y = now.year ();
int y_prev = y;
int m = now.month ();
int m_prev = m - 1;
if (m_prev == 0)
{
m_prev = 12;
--y_prev;
}
range.start = Datetime (y_prev, m_prev, 1);
range.end = Datetime (y, m, 1);
debug (format ("Hint {1} expanded to {2} - {3}",
hint,
range.start.toISOLocalExtended (),
range.end.toISOLocalExtended ()));
return true;
}
else if (hint == ":lastquarter")
{
Datetime now;
int y = now.year ();
int m = now.month ();
int q = ((m - 1) / 3) + 1;
if (--q == 0)
{
q = 4;
--y;
}
m = ((q - 1) * 3) + 1;
range.start = Datetime (y, m, 1);
m += 3;
y += m/12;
m %= 12;
range.end = Datetime (y, m, 1);
debug (format ("Hint {1} expanded to {2} - {3}",
hint,
range.start.toISOLocalExtended (),
range.end.toISOLocalExtended ()));
return true;
}
else if (hint == ":lastyear")
{
Datetime now;
range.start = Datetime (now.year () - 1, 1, 1);
range.end = Datetime (now.year (), 1, 1);
debug (format ("Hint {1} expanded to {2} - {3}",
hint,
range.start.toISOLocalExtended (),
range.end.toISOLocalExtended ()));
return true;
}
else if (std::find (dayNames.begin (), dayNames.end (), hint) != dayNames.end ())
{
int wd = std::find (dayNames.begin (), dayNames.end (), hint) - dayNames.begin ();
Datetime now;
int dow = now.dayOfWeek ();
Datetime sd = now - (86400 * dow) + (86400 * (wd - 7 * (wd <= dow ? 0 : 1)));
Datetime ed = sd + 86400;
range.start = Datetime (sd.year(), sd.month(), sd.day());
range.end = Datetime (ed.year(), ed.month(), ed.day());
debug (format ("Hint {1} expanded to {2} - {3}",
hint,
range.start.toISOLocalExtended (),
range.end.toISOLocalExtended ()));
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Compose a JSON document of intervals. In the trivial case:
// [
// ]
//
// In the non-trivial case:
// [
// {...},
// {...}
// ]
//
std::string jsonFromIntervals (const std::vector <Interval>& intervals)
{
std::stringstream out;
out << "[\n";
int counter = 0;
for (auto& interval : intervals)
{
if (counter)
out << ",\n";
out << interval.json ();
++counter;
}
if (counter)
out << '\n';
out << "]\n";
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////
Palette createPalette (const Rules& rules)
{
Palette p;
auto colors = rules.all ("theme.palette.color");
if (! colors.empty ())
{
p.clear ();
for (auto& c : colors)
p.add (Color (rules.get (c)));
}
p.enabled = rules.getBoolean ("color");
return p;
}
////////////////////////////////////////////////////////////////////////////////
// Extract the tags from a set of intervals, and using a rotating color palette,
// map unique tags to color.
//
// If there is a tags.<tag>.color setting, use it.
std::map <std::string, Color> createTagColorMap (
const Rules& rules,
Palette& palette,
const std::vector <Interval>& intervals)
{
std::map <std::string, Color> mapping;
// Add a color for intervals without tags
mapping[""] = palette.next ();
for (auto& interval : intervals)
{
for (auto& tag : interval.tags ())
{
std::string custom = "tags." + tag + ".color";
if (rules.has (custom))
{
mapping[tag] = Color (rules.get (custom));
}
else if (mapping.find (tag) == mapping.end ())
{
mapping[tag] = palette.next ();
}
}
}
return mapping;
}
////////////////////////////////////////////////////////////////////////////////
int quantizeToNMinutes (const int minutes, const int N)
{
if (minutes % N == 0)
return minutes;
auto deviation = minutes % N;
if (deviation < N/2)
return minutes - deviation;
return minutes + N - deviation;
}
////////////////////////////////////////////////////////////////////////////////
bool findHint (const CLI& cli, const std::string& hint)
{
for (auto& arg : cli._args)
{
if (arg.hasTag ("HINT") &&
arg.getToken () == hint)
{
return true;
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
std::string minimalDelta (const Datetime& left, const Datetime& right)
{
std::string result = right.toISOLocalExtended ();
if (left.sameYear (right))
{
result.replace (0, 5, " ");
if (left.sameMonth (right))
{
result.replace (5, 3, " ");
if (left.sameDay (right))
{
result.replace (8, 3, " ");
if (left.sameHour (right))
{
result.replace (11, 3, " ");
if (left.minute () == right.minute ())
result.replace (14, 3, " ");
}
}
}
}
return result;
}
////////////////////////////////////////////////////////////////////////////////