mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Burndown Chart
- Implemented burndown.daily, which is functional, but has outstanding problems that need to be addressed: - Slow - Does not optimize output (i.e. contains /\S\s+$/) - Needs generalized helper functions to reduce the size of the handler
This commit is contained in:
parent
94480c23d2
commit
652b7d9c8d
16 changed files with 1594 additions and 782 deletions
|
@ -2,6 +2,7 @@
|
|||
------ current release ---------------------------
|
||||
|
||||
1.9.4 ()
|
||||
+ Added burndown charts - burndown.daily, burndown.weekly, burndown.monthly.
|
||||
+ Fixed bug #529, where the 'depends' attribute was not mentioned in the
|
||||
task man page (thanks to Dirk Deimeke).
|
||||
+ Fixed bug #535 which omitted the holidays-NO.rc file from the packages
|
||||
|
|
5
NEWS
5
NEWS
|
@ -1,14 +1,15 @@
|
|||
|
||||
New Features in taskwarrior 1.9.4
|
||||
|
||||
-
|
||||
- New burndown charts.
|
||||
|
||||
Please refer to the ChangeLog file for full details. There are too many to
|
||||
list here.
|
||||
|
||||
New commands in taskwarrior 1.9.4
|
||||
|
||||
-
|
||||
- 'burndown.daily', 'burndown.weekly', 'burndown.monthly', also with
|
||||
'burndown' that is an alias to burndown.weekly.
|
||||
|
||||
New configuration options in taskwarrior 1.9.4
|
||||
|
||||
|
|
|
@ -110,6 +110,18 @@ Shows a graphical report of task status by month. Alias to ghistory.monthly.
|
|||
.B ghistory.annual
|
||||
Shows a graphical report of task status by year.
|
||||
|
||||
.TP
|
||||
.B burndown.daily
|
||||
Shows a graphical burndown chart, by day.
|
||||
|
||||
.TP
|
||||
.B burndown.weekly
|
||||
Shows a graphical burndown chart, by week.
|
||||
|
||||
.TP
|
||||
.B burndown.monthly
|
||||
Shows a graphical burndown chart, by month.
|
||||
|
||||
.TP
|
||||
.B calendar [ y | due [y] | month year [y] | year ]
|
||||
Shows a monthly calendar with due tasks marked.
|
||||
|
|
|
@ -142,6 +142,9 @@ void Cmd::load ()
|
|||
commands.push_back ("history.annual");
|
||||
commands.push_back ("ghistory.monthly");
|
||||
commands.push_back ("ghistory.annual");
|
||||
commands.push_back ("burndown.daily");
|
||||
commands.push_back ("burndown.weekly");
|
||||
commands.push_back ("burndown.monthly");
|
||||
|
||||
// Commands whose names are localized.
|
||||
commands.push_back (context.stringtable.get (CMD_ADD, "add"));
|
||||
|
|
|
@ -136,6 +136,10 @@ std::string Config::defaults =
|
|||
"color.history.done=color0 on rgb050 # Color of completed tasks in ghistory report\n"
|
||||
"color.history.delete=color0 on rgb550 # Color of deleted tasks in ghistory report\n"
|
||||
"\n"
|
||||
"color.burndown.pending=color0 on rgb500 # Color of pending tasks in burndown report\n"
|
||||
"color.burndown.done=color0 on rgb050 # Color of completed tasks in burndown report\n"
|
||||
"color.burndown.started=color0 on rgb550 # Color of started tasks in burndown report\n"
|
||||
"\n"
|
||||
"color.sync.added=rgb005 # Color of added tasks in sync output\n"
|
||||
"color.sync.changed=rgb550 # Color of changed tasks in sync output\n"
|
||||
"color.sync.rejected=rgb500 # Color of rejected tasks in sync output\n"
|
||||
|
@ -181,6 +185,10 @@ std::string Config::defaults =
|
|||
"color.history.done=black on green # Color of completed tasks in ghistory report\n"
|
||||
"color.history.delete=black on yellow # Color of deleted tasks in ghistory report\n"
|
||||
"\n"
|
||||
"color.burndown.pending=black on red # Color of pending tasks in burndown report\n"
|
||||
"color.burndown.done=black on green # Color of completed tasks in burndown report\n"
|
||||
"color.burndown.started=black on yellow # Color of started tasks in burndown report\n"
|
||||
"\n"
|
||||
"color.sync.added=green # Color of added tasks in sync output\n"
|
||||
"color.sync.changed=yellow # Color of changed tasks in sync output\n"
|
||||
"color.sync.rejected=red # Color of rejected tasks in sync output\n"
|
||||
|
@ -273,6 +281,7 @@ std::string Config::defaults =
|
|||
"alias.ghistory=ghistory.monthly # Prefer monthly graphical over annual history reports\n"
|
||||
"alias.export=export.yaml # Prefer YAML over CSV or iCal export\n"
|
||||
"alias.export.vcalendar=export.ical # They are the same\n"
|
||||
"alias.burndown=burndown.weekly # Prefer the weekly burndown chart\n"
|
||||
"\n"
|
||||
"# Fields: id, uuid, project, priority, priority_long, entry, start, end,\n"
|
||||
"# due, countdown, countdown_compact, age, age_compact, active, tags,\n"
|
||||
|
|
|
@ -218,6 +218,9 @@ int Context::dispatch (std::string &out)
|
|||
else if (cmd.command == "history.annual") { rc = handleReportHistoryAnnual (out); }
|
||||
else if (cmd.command == "ghistory.monthly") { rc = handleReportGHistoryMonthly (out); }
|
||||
else if (cmd.command == "ghistory.annual") { rc = handleReportGHistoryAnnual (out); }
|
||||
else if (cmd.command == "burndown.daily") { rc = handleReportBurndownDaily (out); }
|
||||
else if (cmd.command == "burndown.weekly") { rc = handleReportBurndownWeekly (out); }
|
||||
else if (cmd.command == "burndown.monthly") { rc = handleReportBurndownMonthly (out); }
|
||||
else if (cmd.command == "summary") { rc = handleReportSummary (out); }
|
||||
else if (cmd.command == "calendar") { rc = handleReportCalendar (out); }
|
||||
else if (cmd.command == "timesheet") { rc = handleReportTimesheet (out); }
|
||||
|
|
64
src/Date.cpp
64
src/Date.cpp
|
@ -463,7 +463,7 @@ const std::string Date::toString (const std::string& format /*= "m/d/Y" */) cons
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
Date Date::startOfDay () const
|
||||
{
|
||||
return Date (month (), day (), year ());
|
||||
return Date (month (), day (), year (), 0, 0, 0);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -471,19 +471,19 @@ Date Date::startOfWeek () const
|
|||
{
|
||||
Date sow (mT);
|
||||
sow -= (dayOfWeek () * 86400);
|
||||
return Date (sow.month (), sow.day (), sow.year ());
|
||||
return Date (sow.month (), sow.day (), sow.year (), 0, 0, 0);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Date Date::startOfMonth () const
|
||||
{
|
||||
return Date (month (), 1, year ());
|
||||
return Date (month (), 1, year (), 0, 0, 0);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Date Date::startOfYear () const
|
||||
{
|
||||
return Date (1, 1, year ());
|
||||
return Date (1, 1, year (), 0, 0, 0);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -865,6 +865,62 @@ time_t Date::operator- (const Date& rhs)
|
|||
return mT - rhs.mT;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Prefix decrement by one day.
|
||||
void Date::operator-- ()
|
||||
{
|
||||
Date yesterday = startOfDay () - 1;
|
||||
yesterday = Date (yesterday.month (),
|
||||
yesterday.day (),
|
||||
yesterday.year (),
|
||||
hour (),
|
||||
minute (),
|
||||
second ());
|
||||
mT = yesterday.mT;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Postfix decrement by one day.
|
||||
void Date::operator-- (int)
|
||||
{
|
||||
Date yesterday = startOfDay () - 1;
|
||||
yesterday = Date (yesterday.month (),
|
||||
yesterday.day (),
|
||||
yesterday.year (),
|
||||
hour (),
|
||||
minute (),
|
||||
second ());
|
||||
mT = yesterday.mT;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Prefix increment by one day.
|
||||
void Date::operator++ ()
|
||||
{
|
||||
Date tomorrow = (startOfDay () + 90001).startOfDay ();
|
||||
tomorrow = Date (tomorrow.month (),
|
||||
tomorrow.day (),
|
||||
tomorrow.year (),
|
||||
hour (),
|
||||
minute (),
|
||||
second ());
|
||||
mT = tomorrow.mT;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Postfix increment by one day.
|
||||
void Date::operator++ (int)
|
||||
{
|
||||
Date tomorrow = (startOfDay () + 90001).startOfDay ();
|
||||
tomorrow = Date (tomorrow.month (),
|
||||
tomorrow.day (),
|
||||
tomorrow.year (),
|
||||
hour (),
|
||||
minute (),
|
||||
second ());
|
||||
mT = tomorrow.mT;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Date::isEpoch (const std::string& input)
|
||||
{
|
||||
|
|
|
@ -97,6 +97,11 @@ public:
|
|||
|
||||
time_t operator- (const Date&);
|
||||
|
||||
void operator-- (); // Prefix
|
||||
void operator-- (int); // Postfix
|
||||
void operator++ (); // Prefix
|
||||
void operator++ (int); // Postfix
|
||||
|
||||
private:
|
||||
bool isEpoch (const std::string&);
|
||||
bool isRelativeDate (const std::string&);
|
||||
|
|
|
@ -155,6 +155,8 @@ Hooks::Hooks ()
|
|||
validProgramEvents.push_back ("post-ghistory-command");
|
||||
validProgramEvents.push_back ("pre-history-command");
|
||||
validProgramEvents.push_back ("post-history-command");
|
||||
validProgramEvents.push_back ("pre-burndown-command");
|
||||
validProgramEvents.push_back ("post-burndown-command");
|
||||
validProgramEvents.push_back ("pre-import-command");
|
||||
validProgramEvents.push_back ("post-import-command");
|
||||
validProgramEvents.push_back ("pre-info-command");
|
||||
|
|
|
@ -12,11 +12,11 @@ task_SOURCES = API.cpp API.h Att.cpp Att.h Cmd.cpp Cmd.h Color.cpp Color.h \
|
|||
Task.cpp Task.h Taskmod.cpp Taskmod.h Thread.cpp Thread.h \
|
||||
Timer.cpp Timer.h Transport.cpp Transport.h TransportSSH.cpp \
|
||||
TransportSSH.h TransportRSYNC.cpp TransportRSYNC.h \
|
||||
TransportCurl.cpp TransportCurl.h Tree.cpp Tree.h command.cpp \
|
||||
custom.cpp dependency.cpp diag.cpp edit.cpp export.cpp i18n.h \
|
||||
import.cpp interactive.cpp main.cpp main.h recur.cpp report.cpp \
|
||||
rules.cpp rx.cpp rx.h text.cpp text.h util.cpp util.h Uri.cpp \
|
||||
Uri.h
|
||||
TransportCurl.cpp TransportCurl.h Tree.cpp Tree.h burndown.cpp \
|
||||
command.cpp custom.cpp dependency.cpp diag.cpp edit.cpp \
|
||||
export.cpp history.cpp i18n.h import.cpp interactive.cpp \
|
||||
main.cpp main.h recur.cpp report.cpp rules.cpp rx.cpp rx.h \
|
||||
text.cpp text.h util.cpp util.h Uri.cpp Uri.h
|
||||
task_CPPFLAGS=$(LUA_CFLAGS)
|
||||
task_LDFLAGS=$(LUA_LFLAGS)
|
||||
|
||||
|
|
627
src/burndown.cpp
Normal file
627
src/burndown.cpp
Normal file
|
@ -0,0 +1,627 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// taskwarrior - a command line task list manager.
|
||||
//
|
||||
// Copyright 2006 - 2010, Paul Beckingham, Federico Hernandez.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation; either version 2 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
// details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program; if not, write to the
|
||||
//
|
||||
// Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor,
|
||||
// Boston, MA
|
||||
// 02110-1301
|
||||
// USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <iostream> // TODO Remove
|
||||
//#include <iomanip>
|
||||
#include <sstream>
|
||||
//#include <fstream>
|
||||
#include <algorithm>
|
||||
//#include <sys/types.h>
|
||||
//#include <stdio.h>
|
||||
//#include <unistd.h>
|
||||
//#include <stdlib.h>
|
||||
//#include <pwd.h>
|
||||
//#include <time.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "Context.h"
|
||||
//#include "Directory.h"
|
||||
//#include "File.h"
|
||||
#include "Date.h"
|
||||
//#include "Duration.h"
|
||||
//#include "Table.h"
|
||||
#include "text.h"
|
||||
#include "util.h"
|
||||
#include "main.h"
|
||||
|
||||
//#ifdef HAVE_LIBNCURSES
|
||||
//#include <ncurses.h>
|
||||
//#endif
|
||||
|
||||
extern Context context;
|
||||
|
||||
// Helper macro.
|
||||
#define LOC(y,x) (((y) * (width + 1)) + (x))
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Given the vertical chart area size (height), the largest value (value),
|
||||
// populate a vector of labels for the y axis.
|
||||
void calculateYAxis (std::vector <int>& labels, int height, int value)
|
||||
{
|
||||
/*
|
||||
double logarithm = log10 ((double) value);
|
||||
|
||||
int exponent = (int) logarithm;
|
||||
logarithm -= exponent;
|
||||
|
||||
int divisions = 10;
|
||||
double localMaximum = pow (10.0, exponent + 1);
|
||||
bool repeat = true;
|
||||
|
||||
do
|
||||
{
|
||||
repeat = false;
|
||||
double scale = pow (10.0, exponent);
|
||||
|
||||
while (value < localMaximum - scale)
|
||||
{
|
||||
localMaximum -= scale;
|
||||
--divisions;
|
||||
}
|
||||
|
||||
if (divisions < 3 && exponent > 1)
|
||||
{
|
||||
divisions *= 10;
|
||||
--exponent;
|
||||
repeat = true;
|
||||
}
|
||||
}
|
||||
while (repeat);
|
||||
|
||||
int division_size = localMaximum / divisions;
|
||||
for (int i = 0; i <= divisions; ++i)
|
||||
labels.push_back (i * division_size);
|
||||
*/
|
||||
|
||||
// For now, simply select 0, n/2 and n, where n is value rounded up to the
|
||||
// nearest 10.
|
||||
int high = value;
|
||||
int mod = high % 10;
|
||||
if (mod)
|
||||
high += 10 - mod;
|
||||
|
||||
int half = high / 2;
|
||||
|
||||
labels.push_back (0);
|
||||
labels.push_back (half);
|
||||
labels.push_back (high);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Graph should render like this:
|
||||
// +---------------------------------------------------------------------+
|
||||
// | |
|
||||
// | 20 | |
|
||||
// | | dd dd dd dd dd dd dd dd |
|
||||
// | | dd dd dd dd dd dd dd dd dd dd dd dd dd dd |
|
||||
// | | pp pp ss ss ss ss ss ss ss ss ss dd dd dd dd dd dd dd Done |
|
||||
// | 10 | pp pp pp pp pp pp ss ss ss ss ss ss dd dd dd dd dd ss Started|
|
||||
// | | pp pp pp pp pp pp pp pp pp pp pp ss ss ss ss dd dd pp Pending|
|
||||
// | | pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp ss dd |
|
||||
// | | pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp |
|
||||
// | 0 +---------------------------------------------------- |
|
||||
// | 21 22 23 24 25 26 27 28 29 30 31 01 02 03 04 05 06 |
|
||||
// | July August |
|
||||
// | |
|
||||
// | Find rate 1.7/d Estimated completion 8/12/2010 |
|
||||
// | Fix rate 1.3/d |
|
||||
// +---------------------------------------------------------------------+
|
||||
//
|
||||
// e = entry
|
||||
// s = start
|
||||
// C = end/Completed
|
||||
// D = end/Deleted
|
||||
// > = Pending/Waiting
|
||||
//
|
||||
// ID 30 31 01 02 03 04 05 06 07 08 09 10
|
||||
// -- ------------------------------------
|
||||
// 1 e-----s--C
|
||||
// 2 e--s-----D
|
||||
// 3 e-----s-------------->
|
||||
// 4 e----------------->
|
||||
// 5 e----->
|
||||
// -- ------------------------------------
|
||||
// pp 1 2 3 3 2 2 2 3 3 3
|
||||
// ss 2 1 1 1 1 1 1 1
|
||||
// dd 1 1 1 1 1 1 1
|
||||
// -- ------------------------------------
|
||||
//
|
||||
// 5 | ss dd dd dd dd
|
||||
// 4 | ss ss dd dd dd ss ss ss
|
||||
// 3 | pp pp ss ss ss pp pp pp
|
||||
// 2 | pp pp pp pp pp pp pp pp pp
|
||||
// 1 | pp pp pp pp pp pp pp pp pp pp
|
||||
// 0 +-------------------------------------
|
||||
// 30 31 01 02 03 04 05 06 07 08 09 10
|
||||
// Oct Nov
|
||||
//
|
||||
int handleReportBurndownDaily (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-burndown-command"))
|
||||
{
|
||||
std::map <time_t, int> groups;
|
||||
std::map <time_t, int> pendingGroup;
|
||||
std::map <time_t, int> startedGroup;
|
||||
std::map <time_t, int> doneGroup;
|
||||
|
||||
std::map <time_t, int> addGroup;
|
||||
std::map <time_t, int> removeGroup;
|
||||
|
||||
// Scan the pending tasks, applying any filter.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
// How much space is there to render in? This chart will occupy the
|
||||
// maximum space, and the width drives various other parameters.
|
||||
int width = context.getWidth ();
|
||||
int height = context.getHeight () - 1; // Allow for new line with prompt.
|
||||
|
||||
// Estimate how many 'bars' can be dsplayed. This will help subset a
|
||||
// potentially enormous data set.
|
||||
unsigned int estimate = (width - 1 - 14) / 3;
|
||||
Date now;
|
||||
Date cutoff = (now - (estimate * 86400)).startOfDay ();
|
||||
// std::cout << "# cutoff " << cutoff.toString () << "\n";
|
||||
// std::cout << "# now " << now.toString () << "\n";
|
||||
|
||||
time_t epoch;
|
||||
foreach (task, tasks)
|
||||
{
|
||||
// The entry date is when the counting starts.
|
||||
Date from = Date (task->get ("entry")).startOfDay ();
|
||||
addGroup[from.toEpoch ()]++;
|
||||
|
||||
// e--> e--s-->
|
||||
// ppp> pppsss>
|
||||
Task::status status = task->getStatus ();
|
||||
if (status == Task::pending ||
|
||||
status == Task::waiting)
|
||||
{
|
||||
if (task->has ("start"))
|
||||
{
|
||||
Date start = Date (task->get ("start")).startOfDay ();
|
||||
while (from < start)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++pendingGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
|
||||
while (from < now)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++startedGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (from < now)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++pendingGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// e--C e--s--C
|
||||
// pppd> pppsssd>
|
||||
else if (status == Task::completed)
|
||||
{
|
||||
// Truncate history so it starts at 'cutoff' for completed tasks.
|
||||
Date end = Date (task->get ("end")).startOfDay ();
|
||||
removeGroup[end.toEpoch ()]++;
|
||||
|
||||
if (end < cutoff)
|
||||
{
|
||||
++doneGroup[cutoff.toEpoch ()];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (task->has ("start"))
|
||||
{
|
||||
Date start = Date (task->get ("start")).startOfDay ();
|
||||
while (from < start)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++pendingGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
|
||||
while (from < end)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++startedGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
|
||||
while (from < now)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++doneGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Date end = Date (task->get ("end")).startOfDay ();
|
||||
while (from < end)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++pendingGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
|
||||
while (from < now)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++doneGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// e--D e--s--D
|
||||
// ppp pppsss
|
||||
else if (status == Task::deleted)
|
||||
{
|
||||
// Skip old deleted tasks.
|
||||
Date end = Date (task->get ("end")).startOfDay ();
|
||||
removeGroup[end.toEpoch ()]++;
|
||||
|
||||
if (end < cutoff)
|
||||
continue;
|
||||
|
||||
if (task->has ("start"))
|
||||
{
|
||||
Date start = Date (task->get ("start")).startOfDay ();
|
||||
while (from < start)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++pendingGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
|
||||
while (from < end)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++startedGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Date end = Date (task->get ("end")).startOfDay ();
|
||||
while (from < end)
|
||||
{
|
||||
epoch = from.startOfDay ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
++pendingGroup[epoch];
|
||||
from++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO Render.
|
||||
foreach (g, groups)
|
||||
{
|
||||
std::stringstream s;
|
||||
s << Date (g->first).toISO () << " "
|
||||
<< pendingGroup[g->first] << "/"
|
||||
<< startedGroup[g->first] << "/"
|
||||
<< doneGroup[g->first] << "\n";
|
||||
outs += s.str ();
|
||||
}
|
||||
*/
|
||||
|
||||
if (groups.size ())
|
||||
{
|
||||
// Horizontal Breakdown
|
||||
//
|
||||
// >25 | xx ... xx ll pending<
|
||||
// ^ left margin
|
||||
// ^^ max_label
|
||||
// ^ gap
|
||||
// ^ axis
|
||||
// ^^^ gap + bar
|
||||
// ^^^ gap + bar
|
||||
// ^^ gap
|
||||
// ^^ legend swatch
|
||||
// ^ gap
|
||||
// ^^^^^^^ "Pending", "Started" & "Done"
|
||||
// ^ right margin
|
||||
|
||||
// Vertical Breakdown
|
||||
//
|
||||
// v top margin
|
||||
// blank line
|
||||
// | all remaining space
|
||||
// - x axis
|
||||
// 9 day/week/month
|
||||
// 9 month/-/year
|
||||
// blank line
|
||||
// f find rate
|
||||
// f fix rate
|
||||
// ^ bottom margin
|
||||
|
||||
// What is the longest y-axis label? This is tricky. Having
|
||||
// optimistically estimate the number of bars to be shown, then determine
|
||||
// the longest label of the records that lie within the observable range.
|
||||
// It is important to consider that there may be zero -> bars number of
|
||||
// records that match.
|
||||
int max_label = 1;
|
||||
int max_value = 0;
|
||||
|
||||
std::vector <time_t> x_axis;
|
||||
Date now;
|
||||
for (unsigned int i = 0; i < estimate; ++i)
|
||||
{
|
||||
Date x = (now - (i * 86400)).startOfDay ();
|
||||
x_axis.push_back (x.toEpoch ());
|
||||
|
||||
int total = pendingGroup[x.toEpoch ()] +
|
||||
startedGroup[x.toEpoch ()] +
|
||||
doneGroup[x.toEpoch ()];
|
||||
|
||||
if (total > max_value)
|
||||
max_value = total;
|
||||
|
||||
int length = (int) log10 ((double) total) + 1;
|
||||
if (length > max_label)
|
||||
max_label = length;
|
||||
}
|
||||
|
||||
std::sort (x_axis.begin (), x_axis.end ());
|
||||
|
||||
// How many bars can be shown?
|
||||
unsigned int bars = (width - max_label - 14) / 3;
|
||||
int graph_width = width - max_label - 14;
|
||||
|
||||
// Make them match
|
||||
while (bars < x_axis.size ())
|
||||
x_axis.erase (x_axis.begin ());
|
||||
|
||||
// Determine the y-axis.
|
||||
int graph_height = height - 7;
|
||||
|
||||
if (graph_height < 5 ||
|
||||
graph_width < 4)
|
||||
{
|
||||
outs = "Terminal window too small to draw a graph.\n";
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Determine y-axis labelling.
|
||||
std::vector <int> labels;
|
||||
calculateYAxis (labels, graph_height, max_value);
|
||||
// foreach (i, labels)
|
||||
// std::cout << "# label " << *i << "\n";
|
||||
|
||||
// std::cout << "# estimate " << estimate << " bars\n";
|
||||
// std::cout << "# actually " << bars << " bars\n";
|
||||
// std::cout << "# graph width " << graph_width << "\n";
|
||||
// std::cout << "# graph height " << graph_height << "\n";
|
||||
// std::cout << "# days " << x_axis.size () << "\n";
|
||||
// std::cout << "# max label " << max_label << "\n";
|
||||
// std::cout << "# max value " << max_value << "\n";
|
||||
|
||||
// Determine the start date.
|
||||
Date start = (Date () - ((bars - 1) * 86400)).startOfDay ();
|
||||
// std::cout << "# start " << start.toISO () << "\n";
|
||||
|
||||
// Compose the grid.
|
||||
std::string grid;
|
||||
for (int i = 0; i < height; ++i)
|
||||
grid += std::string (width, ' ') + "\n";
|
||||
|
||||
// Draw legend.
|
||||
grid.replace (LOC (graph_height / 2 - 1, width - 10), 10, "dd Done ");
|
||||
grid.replace (LOC (graph_height / 2, width - 10), 10, "ss Started");
|
||||
grid.replace (LOC (graph_height / 2 + 1, width - 10), 10, "pp Pending");
|
||||
|
||||
// Draw x-axis.
|
||||
grid.replace (LOC (height - 6, max_label + 1), 1, "+");
|
||||
grid.replace (LOC (height - 6, max_label + 2), graph_width, std::string (graph_width, '-'));
|
||||
|
||||
int month = 0;
|
||||
Date d (start);
|
||||
for (unsigned int i = 0; i < bars; ++i)
|
||||
{
|
||||
if (month != d.month ())
|
||||
grid.replace (LOC (height - 4, max_label + 3 + (i * 3)), 3, Date::monthName (d.month ()).substr (0, 3));
|
||||
|
||||
char day [3];
|
||||
sprintf (day, "%02d", d.day ());
|
||||
grid.replace (LOC (height - 5, max_label + 3 + (i * 3)), 2, day);
|
||||
|
||||
month = d.month ();
|
||||
d++;
|
||||
}
|
||||
|
||||
// Draw the y-axis.
|
||||
for (int i = 0; i < graph_height; ++i)
|
||||
grid.replace (LOC (i + 1, max_label + 1), 1, "|");
|
||||
|
||||
char label [12];
|
||||
sprintf (label, "%*d", max_label, labels[2]);
|
||||
grid.replace (LOC (1, max_label - strlen (label)), strlen (label), label);
|
||||
sprintf (label, "%*d", max_label, labels[1]);
|
||||
grid.replace (LOC (1 + (graph_height / 2), max_label - strlen (label)), strlen (label), label);
|
||||
grid.replace (LOC (graph_height + 1, max_label - 1), 1, "0");
|
||||
|
||||
// Draw the bars.
|
||||
int last_pending = 0;
|
||||
d = start;
|
||||
for (unsigned int i = 0; i < bars; ++i)
|
||||
{
|
||||
epoch = d.toEpoch ();
|
||||
int pending = (pendingGroup[epoch] * graph_height) / labels[2];
|
||||
int started = (startedGroup[epoch] * graph_height) / labels[2];
|
||||
int done = (doneGroup[epoch] * graph_height) / labels[2];
|
||||
|
||||
// Track the latest pending count, for convergence calculation.
|
||||
last_pending = pendingGroup[epoch] + startedGroup[epoch];
|
||||
|
||||
for (int b = 0; b < pending; ++b)
|
||||
grid.replace (LOC (graph_height - b, max_label + 3 + (i * 3)), 2, "pp");
|
||||
|
||||
for (int b = 0; b < started; ++b)
|
||||
grid.replace (LOC (graph_height - b - pending, max_label + 3 + (i * 3)), 2, "ss");
|
||||
|
||||
for (int b = 0; b < done; ++b)
|
||||
grid.replace (LOC (graph_height - b - pending - started, max_label + 3 + (i * 3)), 2, "dd");
|
||||
|
||||
d++;
|
||||
}
|
||||
|
||||
// std::cout << "# last pending " << last_pending << "\n";
|
||||
|
||||
// Calculate and render the rates.
|
||||
// Calculate 30-day average.
|
||||
int totalAdded30 = 0;
|
||||
int totalRemoved30 = 0;
|
||||
d = (Date () - 30 * 86400).startOfDay ();
|
||||
for (unsigned int i = 0; i < 30; i++)
|
||||
{
|
||||
epoch = d.toEpoch ();
|
||||
|
||||
totalAdded30 += addGroup[epoch];
|
||||
totalRemoved30 += removeGroup[epoch];
|
||||
|
||||
d++;
|
||||
}
|
||||
|
||||
float find_rate30 = 1.0 * totalAdded30 / x_axis.size ();
|
||||
float fix_rate30 = 1.0 * totalRemoved30 / x_axis.size ();
|
||||
|
||||
// Calculate 7-day average.
|
||||
int totalAdded7 = 0;
|
||||
int totalRemoved7 = 0;
|
||||
d = (Date () - 7 * 86400).startOfDay ();
|
||||
for (unsigned int i = 0; i < 7; i++)
|
||||
{
|
||||
epoch = d.toEpoch ();
|
||||
|
||||
totalAdded7 += addGroup[epoch];
|
||||
totalRemoved7 += removeGroup[epoch];
|
||||
|
||||
d++;
|
||||
}
|
||||
|
||||
float find_rate7 = 1.0 * totalAdded7 / x_axis.size ();
|
||||
float fix_rate7 = 1.0 * totalRemoved7 / x_axis.size ();
|
||||
|
||||
// Render rates.
|
||||
char rate[12];
|
||||
sprintf (rate, "%.1f", (find_rate30 + find_rate7) / 2.0);
|
||||
grid.replace (LOC (height - 2, max_label + 3), 13 + strlen (rate), std::string ("Find rate: ") + rate + "/d");
|
||||
|
||||
sprintf (rate, "%.1f", (fix_rate30 + fix_rate7) / 2.0);
|
||||
grid.replace (LOC (height - 1, max_label + 3), 13 + strlen (rate), std::string ("Fix rate: ") + rate + "/d");
|
||||
|
||||
if (last_pending == 0)
|
||||
{
|
||||
; // Do not render an estimated completion date.
|
||||
}
|
||||
else if (find_rate7 < fix_rate7)
|
||||
{
|
||||
int current_pending = pendingGroup[Date ().startOfDay ().toEpoch ()];
|
||||
float days = 2.0 * current_pending / (fix_rate30 + fix_rate7);
|
||||
Date end;
|
||||
end += (int) (days * 86400);
|
||||
std::string formatted = end.toString (context.config.get ("dateformat"));
|
||||
grid.replace (LOC (height - 2, max_label + 27), 22 + formatted.length (), "Estimated completion: " + formatted);
|
||||
}
|
||||
else
|
||||
{
|
||||
grid.replace (LOC (height - 2, max_label + 27), 36, "Estimated completion: No convergence");
|
||||
}
|
||||
|
||||
// Output the grid.
|
||||
Color color_pending (context.config.get ("color.burndown.pending"));
|
||||
Color color_done (context.config.get ("color.burndown.done"));
|
||||
Color color_started (context.config.get ("color.burndown.started"));
|
||||
|
||||
// Replace dd, ss, pp with colored strings.
|
||||
// TODO Use configurable values.
|
||||
std::string::size_type i;
|
||||
while ((i = grid.find ("pp")) != std::string::npos)
|
||||
grid.replace (i, 2, color_pending.colorize (" "));
|
||||
|
||||
while ((i = grid.find ("ss")) != std::string::npos)
|
||||
grid.replace (i, 2, color_started.colorize (" "));
|
||||
|
||||
while ((i = grid.find ("dd")) != std::string::npos)
|
||||
grid.replace (i, 2, color_done.colorize (" "));
|
||||
|
||||
outs += grid;
|
||||
|
||||
context.hooks.trigger ("post-burndown-command");
|
||||
}
|
||||
else
|
||||
outs = "No matches.\n";
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportBurndownWeekly (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportBurndownMonthly (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
820
src/history.cpp
Normal file
820
src/history.cpp
Normal file
|
@ -0,0 +1,820 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// taskwarrior - a command line task list manager.
|
||||
//
|
||||
// Copyright 2006 - 2010, Paul Beckingham, Federico Hernandez.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation; either version 2 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
// details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program; if not, write to the
|
||||
//
|
||||
// Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor,
|
||||
// Boston, MA
|
||||
// 02110-1301
|
||||
// USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//#include <iostream>
|
||||
//#include <iomanip>
|
||||
#include <sstream>
|
||||
//#include <fstream>
|
||||
//#include <sys/types.h>
|
||||
//#include <stdio.h>
|
||||
//#include <unistd.h>
|
||||
//#include <stdlib.h>
|
||||
//#include <pwd.h>
|
||||
//#include <time.h>
|
||||
|
||||
#include "Context.h"
|
||||
//#include "Directory.h"
|
||||
//#include "File.h"
|
||||
//#include "Date.h"
|
||||
//#include "Duration.h"
|
||||
#include "Table.h"
|
||||
#include "text.h"
|
||||
#include "util.h"
|
||||
#include "main.h"
|
||||
|
||||
//#ifdef HAVE_LIBNCURSES
|
||||
//#include <ncurses.h>
|
||||
//#endif
|
||||
|
||||
extern Context context;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportHistoryMonthly (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-history-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Month");
|
||||
table.addColumn ("Added");
|
||||
table.addColumn ("Completed");
|
||||
table.addColumn ("Deleted");
|
||||
table.addColumn ("Net");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
table.setColumnUnderline (1);
|
||||
table.setColumnUnderline (2);
|
||||
table.setColumnUnderline (3);
|
||||
table.setColumnUnderline (4);
|
||||
table.setColumnUnderline (5);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
table.setColumnJustification (2, Table::right);
|
||||
table.setColumnJustification (3, Table::right);
|
||||
table.setColumnJustification (4, Table::right);
|
||||
table.setColumnJustification (5, Table::right);
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup [i->first];
|
||||
totalCompleted += completedGroup [i->first];
|
||||
totalDeleted += deletedGroup [i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
table.addCell (row, 1, Date::monthName(m));
|
||||
|
||||
int net = 0;
|
||||
|
||||
if (addedGroup.find (i->first) != addedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 2, addedGroup[i->first]);
|
||||
net +=addedGroup[i->first];
|
||||
}
|
||||
|
||||
if (completedGroup.find (i->first) != completedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 3, completedGroup[i->first]);
|
||||
net -= completedGroup[i->first];
|
||||
}
|
||||
|
||||
if (deletedGroup.find (i->first) != deletedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 4, deletedGroup[i->first]);
|
||||
net -= deletedGroup[i->first];
|
||||
}
|
||||
|
||||
table.addCell (row, 5, net);
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net)
|
||||
table.setCellColor (row, 5, net > 0 ? Color (Color::red) :
|
||||
Color (Color::green));
|
||||
}
|
||||
|
||||
if (table.rowCount ())
|
||||
{
|
||||
table.addRow ();
|
||||
row = table.addRow ();
|
||||
|
||||
table.addCell (row, 1, "Average");
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false));
|
||||
table.addCell (row, 2, totalAdded / (table.rowCount () - 2));
|
||||
table.addCell (row, 3, totalCompleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 4, totalDeleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 5, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2));
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-history-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportHistoryAnnual (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-history-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Added");
|
||||
table.addColumn ("Completed");
|
||||
table.addColumn ("Deleted");
|
||||
table.addColumn ("Net");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
table.setColumnUnderline (1);
|
||||
table.setColumnUnderline (2);
|
||||
table.setColumnUnderline (3);
|
||||
table.setColumnUnderline (4);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
table.setColumnJustification (1, Table::right);
|
||||
table.setColumnJustification (2, Table::right);
|
||||
table.setColumnJustification (3, Table::right);
|
||||
table.setColumnJustification (4, Table::right);
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup [i->first];
|
||||
totalCompleted += completedGroup [i->first];
|
||||
totalDeleted += deletedGroup [i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
|
||||
int net = 0;
|
||||
|
||||
if (addedGroup.find (i->first) != addedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 1, addedGroup[i->first]);
|
||||
net +=addedGroup[i->first];
|
||||
}
|
||||
|
||||
if (completedGroup.find (i->first) != completedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 2, completedGroup[i->first]);
|
||||
net -= completedGroup[i->first];
|
||||
}
|
||||
|
||||
if (deletedGroup.find (i->first) != deletedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 3, deletedGroup[i->first]);
|
||||
net -= deletedGroup[i->first];
|
||||
}
|
||||
|
||||
table.addCell (row, 4, net);
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net)
|
||||
table.setCellColor (row, 4, net > 0 ? Color (Color::red) :
|
||||
Color (Color::green));
|
||||
}
|
||||
|
||||
if (table.rowCount ())
|
||||
{
|
||||
table.addRow ();
|
||||
row = table.addRow ();
|
||||
|
||||
table.addCell (row, 0, "Average");
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false));
|
||||
table.addCell (row, 1, totalAdded / (table.rowCount () - 2));
|
||||
table.addCell (row, 2, totalCompleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 3, totalDeleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 4, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2));
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-history-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportGHistoryMonthly (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-ghistory-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int widthOfBar = context.getWidth () - 15; // 15 == strlen ("2008 September ")
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Month");
|
||||
table.addColumn ("Number Added/Completed/Deleted");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
table.setColumnUnderline (1);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
Color color_add (context.config.get ("color.history.add"));
|
||||
Color color_done (context.config.get ("color.history.done"));
|
||||
Color color_delete (context.config.get ("color.history.delete"));
|
||||
|
||||
// Determine the longest line, and the longest "added" line.
|
||||
int maxAddedLine = 0;
|
||||
int maxRemovedLine = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine)
|
||||
maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first];
|
||||
|
||||
if (addedGroup[i->first] > maxAddedLine)
|
||||
maxAddedLine = addedGroup[i->first];
|
||||
}
|
||||
|
||||
int maxLine = maxAddedLine + maxRemovedLine;
|
||||
if (maxLine > 0)
|
||||
{
|
||||
unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine;
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup[i->first];
|
||||
totalCompleted += completedGroup[i->first];
|
||||
totalDeleted += deletedGroup[i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
table.addCell (row, 1, Date::monthName(m));
|
||||
|
||||
unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine;
|
||||
unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine;
|
||||
unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine;
|
||||
|
||||
std::string bar = "";
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
{
|
||||
char number[24];
|
||||
std::string aBar = "";
|
||||
if (addedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", addedGroup[i->first]);
|
||||
aBar = number;
|
||||
while (aBar.length () < addedBar)
|
||||
aBar = " " + aBar;
|
||||
}
|
||||
|
||||
std::string cBar = "";
|
||||
if (completedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", completedGroup[i->first]);
|
||||
cBar = number;
|
||||
while (cBar.length () < completedBar)
|
||||
cBar = " " + cBar;
|
||||
}
|
||||
|
||||
std::string dBar = "";
|
||||
if (deletedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", deletedGroup[i->first]);
|
||||
dBar = number;
|
||||
while (dBar.length () < deletedBar)
|
||||
dBar = " " + dBar;
|
||||
}
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
|
||||
bar += color_add.colorize (aBar);
|
||||
bar += color_done.colorize (cBar);
|
||||
bar += color_delete.colorize (dBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+";
|
||||
std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X";
|
||||
std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-";
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
bar += aBar + cBar + dBar;
|
||||
}
|
||||
|
||||
table.addCell (row, 2, bar);
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
{
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
out << "Legend: "
|
||||
<< color_add.colorize ("added")
|
||||
<< ", "
|
||||
<< color_done.colorize ("completed")
|
||||
<< ", "
|
||||
<< color_delete.colorize ("deleted")
|
||||
<< optionalBlankLine ()
|
||||
<< "\n";
|
||||
else
|
||||
out << "Legend: + added, X completed, - deleted\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-ghistory-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportGHistoryAnnual (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-ghistory-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int widthOfBar = context.getWidth () - 5; // 5 == strlen ("2008 ")
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Number Added/Completed/Deleted");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
Color color_add (context.config.get ("color.history.add"));
|
||||
Color color_done (context.config.get ("color.history.done"));
|
||||
Color color_delete (context.config.get ("color.history.delete"));
|
||||
|
||||
// Determine the longest line, and the longest "added" line.
|
||||
int maxAddedLine = 0;
|
||||
int maxRemovedLine = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine)
|
||||
maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first];
|
||||
|
||||
if (addedGroup[i->first] > maxAddedLine)
|
||||
maxAddedLine = addedGroup[i->first];
|
||||
}
|
||||
|
||||
int maxLine = maxAddedLine + maxRemovedLine;
|
||||
if (maxLine > 0)
|
||||
{
|
||||
unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine;
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup[i->first];
|
||||
totalCompleted += completedGroup[i->first];
|
||||
totalDeleted += deletedGroup[i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
|
||||
unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine;
|
||||
unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine;
|
||||
unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine;
|
||||
|
||||
std::string bar = "";
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
{
|
||||
char number[24];
|
||||
std::string aBar = "";
|
||||
if (addedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", addedGroup[i->first]);
|
||||
aBar = number;
|
||||
while (aBar.length () < addedBar)
|
||||
aBar = " " + aBar;
|
||||
}
|
||||
|
||||
std::string cBar = "";
|
||||
if (completedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", completedGroup[i->first]);
|
||||
cBar = number;
|
||||
while (cBar.length () < completedBar)
|
||||
cBar = " " + cBar;
|
||||
}
|
||||
|
||||
std::string dBar = "";
|
||||
if (deletedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", deletedGroup[i->first]);
|
||||
dBar = number;
|
||||
while (dBar.length () < deletedBar)
|
||||
dBar = " " + dBar;
|
||||
}
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
bar += color_add.colorize (aBar);
|
||||
bar += color_done.colorize (cBar);
|
||||
bar += color_delete.colorize (dBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+";
|
||||
std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X";
|
||||
std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-";
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
bar += aBar + cBar + dBar;
|
||||
}
|
||||
|
||||
table.addCell (row, 1, bar);
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
{
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
out << "Legend: "
|
||||
<< color_add.colorize ("added")
|
||||
<< ", "
|
||||
<< color_done.colorize ("completed")
|
||||
<< ", "
|
||||
<< color_delete.colorize ("deleted")
|
||||
<< optionalBlankLine ()
|
||||
<< "\n";
|
||||
else
|
||||
out << "Legend: + added, X completed, - deleted\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-ghistory-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
15
src/main.h
15
src/main.h
|
@ -103,10 +103,6 @@ int shortUsage (std::string&);
|
|||
int longUsage (std::string&);
|
||||
int handleInfo (std::string&);
|
||||
int handleReportSummary (std::string&);
|
||||
int handleReportHistoryMonthly (std::string&);
|
||||
int handleReportHistoryAnnual (std::string&);
|
||||
int handleReportGHistoryMonthly (std::string&);
|
||||
int handleReportGHistoryAnnual (std::string&);
|
||||
int handleReportCalendar (std::string&);
|
||||
int handleReportStats (std::string&);
|
||||
int handleReportTimesheet (std::string&);
|
||||
|
@ -116,6 +112,17 @@ std::string getDueDate (Task&, const std::string&);
|
|||
std::string onProjectChange (Task&, bool scope = true);
|
||||
std::string onProjectChange (Task&, Task&);
|
||||
|
||||
// burndown.cpp
|
||||
int handleReportBurndownDaily (std::string&);
|
||||
int handleReportBurndownWeekly (std::string&);
|
||||
int handleReportBurndownMonthly (std::string&);
|
||||
|
||||
// history.cpp
|
||||
int handleReportHistoryMonthly (std::string&);
|
||||
int handleReportHistoryAnnual (std::string&);
|
||||
int handleReportGHistoryMonthly (std::string&);
|
||||
int handleReportGHistoryAnnual (std::string&);
|
||||
|
||||
// custom.cpp
|
||||
int handleCustomReport (const std::string&, std::string&);
|
||||
void validReportColumns (const std::vector <std::string>&);
|
||||
|
|
777
src/report.cpp
777
src/report.cpp
|
@ -187,6 +187,18 @@ int shortUsage (std::string& outs)
|
|||
table.addCell (row, 1, "task ghistory.annual");
|
||||
table.addCell (row, 2, "Shows a graphical report of task history, by year.");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task burndown.daily");
|
||||
table.addCell (row, 2, "Shows a graphical burndown chart, by day.");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task burndown.weekly");
|
||||
table.addCell (row, 2, "Shows a graphical burndown chart, by week.");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task burndown.monthly");
|
||||
table.addCell (row, 2, "Shows a graphical burndown chart, by month.");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task calendar [due|month year|year]");
|
||||
table.addCell (row, 2, "Shows a calendar, with due tasks marked.");
|
||||
|
@ -819,771 +831,6 @@ int handleReportSummary (std::string& outs)
|
|||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportHistoryMonthly (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-history-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Month");
|
||||
table.addColumn ("Added");
|
||||
table.addColumn ("Completed");
|
||||
table.addColumn ("Deleted");
|
||||
table.addColumn ("Net");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
table.setColumnUnderline (1);
|
||||
table.setColumnUnderline (2);
|
||||
table.setColumnUnderline (3);
|
||||
table.setColumnUnderline (4);
|
||||
table.setColumnUnderline (5);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
table.setColumnJustification (2, Table::right);
|
||||
table.setColumnJustification (3, Table::right);
|
||||
table.setColumnJustification (4, Table::right);
|
||||
table.setColumnJustification (5, Table::right);
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup [i->first];
|
||||
totalCompleted += completedGroup [i->first];
|
||||
totalDeleted += deletedGroup [i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
table.addCell (row, 1, Date::monthName(m));
|
||||
|
||||
int net = 0;
|
||||
|
||||
if (addedGroup.find (i->first) != addedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 2, addedGroup[i->first]);
|
||||
net +=addedGroup[i->first];
|
||||
}
|
||||
|
||||
if (completedGroup.find (i->first) != completedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 3, completedGroup[i->first]);
|
||||
net -= completedGroup[i->first];
|
||||
}
|
||||
|
||||
if (deletedGroup.find (i->first) != deletedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 4, deletedGroup[i->first]);
|
||||
net -= deletedGroup[i->first];
|
||||
}
|
||||
|
||||
table.addCell (row, 5, net);
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net)
|
||||
table.setCellColor (row, 5, net > 0 ? Color (Color::red) :
|
||||
Color (Color::green));
|
||||
}
|
||||
|
||||
if (table.rowCount ())
|
||||
{
|
||||
table.addRow ();
|
||||
row = table.addRow ();
|
||||
|
||||
table.addCell (row, 1, "Average");
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false));
|
||||
table.addCell (row, 2, totalAdded / (table.rowCount () - 2));
|
||||
table.addCell (row, 3, totalCompleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 4, totalDeleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 5, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2));
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-history-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportHistoryAnnual (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-history-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Added");
|
||||
table.addColumn ("Completed");
|
||||
table.addColumn ("Deleted");
|
||||
table.addColumn ("Net");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
table.setColumnUnderline (1);
|
||||
table.setColumnUnderline (2);
|
||||
table.setColumnUnderline (3);
|
||||
table.setColumnUnderline (4);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
table.setColumnJustification (1, Table::right);
|
||||
table.setColumnJustification (2, Table::right);
|
||||
table.setColumnJustification (3, Table::right);
|
||||
table.setColumnJustification (4, Table::right);
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup [i->first];
|
||||
totalCompleted += completedGroup [i->first];
|
||||
totalDeleted += deletedGroup [i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
|
||||
int net = 0;
|
||||
|
||||
if (addedGroup.find (i->first) != addedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 1, addedGroup[i->first]);
|
||||
net +=addedGroup[i->first];
|
||||
}
|
||||
|
||||
if (completedGroup.find (i->first) != completedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 2, completedGroup[i->first]);
|
||||
net -= completedGroup[i->first];
|
||||
}
|
||||
|
||||
if (deletedGroup.find (i->first) != deletedGroup.end ())
|
||||
{
|
||||
table.addCell (row, 3, deletedGroup[i->first]);
|
||||
net -= deletedGroup[i->first];
|
||||
}
|
||||
|
||||
table.addCell (row, 4, net);
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && net)
|
||||
table.setCellColor (row, 4, net > 0 ? Color (Color::red) :
|
||||
Color (Color::green));
|
||||
}
|
||||
|
||||
if (table.rowCount ())
|
||||
{
|
||||
table.addRow ();
|
||||
row = table.addRow ();
|
||||
|
||||
table.addCell (row, 0, "Average");
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
table.setRowColor (row, Color (Color::nocolor, Color::nocolor, false, true, false));
|
||||
table.addCell (row, 1, totalAdded / (table.rowCount () - 2));
|
||||
table.addCell (row, 2, totalCompleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 3, totalDeleted / (table.rowCount () - 2));
|
||||
table.addCell (row, 4, (totalAdded - totalCompleted - totalDeleted) / (table.rowCount () - 2));
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-history-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportGHistoryMonthly (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-ghistory-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfMonth ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int widthOfBar = context.getWidth () - 15; // 15 == strlen ("2008 September ")
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Month");
|
||||
table.addColumn ("Number Added/Completed/Deleted");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
table.setColumnUnderline (1);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
Color color_add (context.config.get ("color.history.add"));
|
||||
Color color_done (context.config.get ("color.history.done"));
|
||||
Color color_delete (context.config.get ("color.history.delete"));
|
||||
|
||||
// Determine the longest line, and the longest "added" line.
|
||||
int maxAddedLine = 0;
|
||||
int maxRemovedLine = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine)
|
||||
maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first];
|
||||
|
||||
if (addedGroup[i->first] > maxAddedLine)
|
||||
maxAddedLine = addedGroup[i->first];
|
||||
}
|
||||
|
||||
int maxLine = maxAddedLine + maxRemovedLine;
|
||||
if (maxLine > 0)
|
||||
{
|
||||
unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine;
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup[i->first];
|
||||
totalCompleted += completedGroup[i->first];
|
||||
totalDeleted += deletedGroup[i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
table.addCell (row, 1, Date::monthName(m));
|
||||
|
||||
unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine;
|
||||
unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine;
|
||||
unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine;
|
||||
|
||||
std::string bar = "";
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
{
|
||||
char number[24];
|
||||
std::string aBar = "";
|
||||
if (addedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", addedGroup[i->first]);
|
||||
aBar = number;
|
||||
while (aBar.length () < addedBar)
|
||||
aBar = " " + aBar;
|
||||
}
|
||||
|
||||
std::string cBar = "";
|
||||
if (completedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", completedGroup[i->first]);
|
||||
cBar = number;
|
||||
while (cBar.length () < completedBar)
|
||||
cBar = " " + cBar;
|
||||
}
|
||||
|
||||
std::string dBar = "";
|
||||
if (deletedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", deletedGroup[i->first]);
|
||||
dBar = number;
|
||||
while (dBar.length () < deletedBar)
|
||||
dBar = " " + dBar;
|
||||
}
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
|
||||
bar += color_add.colorize (aBar);
|
||||
bar += color_done.colorize (cBar);
|
||||
bar += color_delete.colorize (dBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+";
|
||||
std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X";
|
||||
std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-";
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
bar += aBar + cBar + dBar;
|
||||
}
|
||||
|
||||
table.addCell (row, 2, bar);
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
{
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
out << "Legend: "
|
||||
<< color_add.colorize ("added")
|
||||
<< ", "
|
||||
<< color_done.colorize ("completed")
|
||||
<< ", "
|
||||
<< color_delete.colorize ("deleted")
|
||||
<< optionalBlankLine ()
|
||||
<< "\n";
|
||||
else
|
||||
out << "Legend: + added, X completed, - deleted\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-ghistory-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportGHistoryAnnual (std::string& outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-ghistory-command"))
|
||||
{
|
||||
std::map <time_t, int> groups; // Represents any month with data
|
||||
std::map <time_t, int> addedGroup; // Additions by month
|
||||
std::map <time_t, int> completedGroup; // Completions by month
|
||||
std::map <time_t, int> deletedGroup; // Deletions by month
|
||||
|
||||
// Scan the pending tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
Date entry (task->get ("entry"));
|
||||
|
||||
Date end;
|
||||
if (task->has ("end"))
|
||||
end = Date (task->get ("end"));
|
||||
|
||||
time_t epoch = entry.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
// Every task has an entry date.
|
||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
||||
else
|
||||
addedGroup[epoch] = 1;
|
||||
|
||||
// All deleted tasks have an end date.
|
||||
if (task->getStatus () == Task::deleted)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
||||
else
|
||||
deletedGroup[epoch] = 1;
|
||||
}
|
||||
|
||||
// All completed tasks have an end date.
|
||||
else if (task->getStatus () == Task::completed)
|
||||
{
|
||||
epoch = end.startOfYear ().toEpoch ();
|
||||
groups[epoch] = 0;
|
||||
|
||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
||||
else
|
||||
completedGroup[epoch] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int widthOfBar = context.getWidth () - 5; // 5 == strlen ("2008 ")
|
||||
|
||||
// Now build the table.
|
||||
Table table;
|
||||
table.setDateFormat (context.config.get ("dateformat"));
|
||||
table.addColumn ("Year");
|
||||
table.addColumn ("Number Added/Completed/Deleted");
|
||||
|
||||
if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) &&
|
||||
context.config.getBoolean ("fontunderline"))
|
||||
{
|
||||
table.setColumnUnderline (0);
|
||||
}
|
||||
else
|
||||
table.setTableDashedUnderline ();
|
||||
|
||||
Color color_add (context.config.get ("color.history.add"));
|
||||
Color color_done (context.config.get ("color.history.done"));
|
||||
Color color_delete (context.config.get ("color.history.delete"));
|
||||
|
||||
// Determine the longest line, and the longest "added" line.
|
||||
int maxAddedLine = 0;
|
||||
int maxRemovedLine = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
if (completedGroup[i->first] + deletedGroup[i->first] > maxRemovedLine)
|
||||
maxRemovedLine = completedGroup[i->first] + deletedGroup[i->first];
|
||||
|
||||
if (addedGroup[i->first] > maxAddedLine)
|
||||
maxAddedLine = addedGroup[i->first];
|
||||
}
|
||||
|
||||
int maxLine = maxAddedLine + maxRemovedLine;
|
||||
if (maxLine > 0)
|
||||
{
|
||||
unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine;
|
||||
|
||||
int totalAdded = 0;
|
||||
int totalCompleted = 0;
|
||||
int totalDeleted = 0;
|
||||
|
||||
int priorYear = 0;
|
||||
int row = 0;
|
||||
foreach (i, groups)
|
||||
{
|
||||
row = table.addRow ();
|
||||
|
||||
totalAdded += addedGroup[i->first];
|
||||
totalCompleted += completedGroup[i->first];
|
||||
totalDeleted += deletedGroup[i->first];
|
||||
|
||||
Date dt (i->first);
|
||||
int m, d, y;
|
||||
dt.toMDY (m, d, y);
|
||||
|
||||
if (y != priorYear)
|
||||
{
|
||||
table.addCell (row, 0, y);
|
||||
priorYear = y;
|
||||
}
|
||||
|
||||
unsigned int addedBar = (widthOfBar * addedGroup[i->first]) / maxLine;
|
||||
unsigned int completedBar = (widthOfBar * completedGroup[i->first]) / maxLine;
|
||||
unsigned int deletedBar = (widthOfBar * deletedGroup[i->first]) / maxLine;
|
||||
|
||||
std::string bar = "";
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
{
|
||||
char number[24];
|
||||
std::string aBar = "";
|
||||
if (addedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", addedGroup[i->first]);
|
||||
aBar = number;
|
||||
while (aBar.length () < addedBar)
|
||||
aBar = " " + aBar;
|
||||
}
|
||||
|
||||
std::string cBar = "";
|
||||
if (completedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", completedGroup[i->first]);
|
||||
cBar = number;
|
||||
while (cBar.length () < completedBar)
|
||||
cBar = " " + cBar;
|
||||
}
|
||||
|
||||
std::string dBar = "";
|
||||
if (deletedGroup[i->first])
|
||||
{
|
||||
sprintf (number, "%d", deletedGroup[i->first]);
|
||||
dBar = number;
|
||||
while (dBar.length () < deletedBar)
|
||||
dBar = " " + dBar;
|
||||
}
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
bar += color_add.colorize (aBar);
|
||||
bar += color_done.colorize (cBar);
|
||||
bar += color_delete.colorize (dBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string aBar = ""; while (aBar.length () < addedBar) aBar += "+";
|
||||
std::string cBar = ""; while (cBar.length () < completedBar) cBar += "X";
|
||||
std::string dBar = ""; while (dBar.length () < deletedBar) dBar += "-";
|
||||
|
||||
bar += std::string (leftOffset - aBar.length (), ' ');
|
||||
bar += aBar + cBar + dBar;
|
||||
}
|
||||
|
||||
table.addCell (row, 1, bar);
|
||||
}
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
if (table.rowCount ())
|
||||
{
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< "\n";
|
||||
|
||||
if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor"))
|
||||
out << "Legend: "
|
||||
<< color_add.colorize ("added")
|
||||
<< ", "
|
||||
<< color_done.colorize ("completed")
|
||||
<< ", "
|
||||
<< color_delete.colorize ("deleted")
|
||||
<< optionalBlankLine ()
|
||||
<< "\n";
|
||||
else
|
||||
out << "Legend: + added, X completed, - deleted\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
out << "No tasks.\n";
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-ghistory-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleReportTimesheet (std::string& outs)
|
||||
{
|
||||
|
|
|
@ -15,7 +15,8 @@ OBJECTS = ../t-TDB.o ../t-Task.o ../t-text.o ../t-Date.o ../t-Table.o \
|
|||
../t-Hooks.o ../t-API.o ../t-rx.o ../t-Taskmod.o ../t-dependency.o \
|
||||
../t-Transport.o ../t-TransportSSH.o ../t-Sensor.o ../t-Thread.o \
|
||||
../t-Lisp.o ../t-Rectangle.o ../t-Tree.o ../t-TransportRSYNC.o \
|
||||
../t-TransportCurl.o ../t-Uri.o ../t-diag.o
|
||||
../t-TransportCurl.o ../t-Uri.o ../t-diag.o ../t-burndown.o \
|
||||
../t-history.o
|
||||
|
||||
all: $(PROJECT)
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ Context context;
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
int main (int argc, char** argv)
|
||||
{
|
||||
UnitTest t (158);
|
||||
UnitTest t (162);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -347,6 +347,24 @@ int main (int argc, char** argv)
|
|||
// Date::operator-
|
||||
Date r22 (1234567890);
|
||||
t.is ((r22 - 1).toEpoch (), 1234567889, "1234567890 - 1 = 1234567889");
|
||||
|
||||
// Date::operator--
|
||||
Date r23 (11, 7, 2010, 23, 59, 59);
|
||||
r23--;
|
||||
t.is (r23.toString ("YMDHNS"), "20101106235959", "decrement across fall DST boundary");
|
||||
|
||||
Date r24 (3, 14, 2010, 23, 59, 59);
|
||||
r24--;
|
||||
t.is (r24.toString ("YMDHNS"), "20100313235959", "decrement across spring DST boundary");
|
||||
|
||||
// Date::operator++
|
||||
Date r25 (11, 6, 2010, 23, 59, 59);
|
||||
r25++;
|
||||
t.is (r25.toString ("YMDHNS"), "20101107235959", "increment across fall DST boundary");
|
||||
|
||||
Date r26 (3, 13, 2010, 23, 59, 59);
|
||||
r26++;
|
||||
t.is (r26.toString ("YMDHNS"), "20100314235959", "increment across spring DST boundary");
|
||||
}
|
||||
|
||||
catch (std::string& e)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue