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:
Paul Beckingham 2010-11-21 00:28:34 -05:00
parent 94480c23d2
commit 652b7d9c8d
16 changed files with 1594 additions and 782 deletions

View file

@ -2,6 +2,7 @@
------ current release --------------------------- ------ current release ---------------------------
1.9.4 () 1.9.4 ()
+ Added burndown charts - burndown.daily, burndown.weekly, burndown.monthly.
+ Fixed bug #529, where the 'depends' attribute was not mentioned in the + Fixed bug #529, where the 'depends' attribute was not mentioned in the
task man page (thanks to Dirk Deimeke). task man page (thanks to Dirk Deimeke).
+ Fixed bug #535 which omitted the holidays-NO.rc file from the packages + Fixed bug #535 which omitted the holidays-NO.rc file from the packages

5
NEWS
View file

@ -1,14 +1,15 @@
New Features in taskwarrior 1.9.4 New Features in taskwarrior 1.9.4
- - New burndown charts.
Please refer to the ChangeLog file for full details. There are too many to Please refer to the ChangeLog file for full details. There are too many to
list here. list here.
New commands in taskwarrior 1.9.4 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 New configuration options in taskwarrior 1.9.4

View file

@ -110,6 +110,18 @@ Shows a graphical report of task status by month. Alias to ghistory.monthly.
.B ghistory.annual .B ghistory.annual
Shows a graphical report of task status by year. 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 .TP
.B calendar [ y | due [y] | month year [y] | year ] .B calendar [ y | due [y] | month year [y] | year ]
Shows a monthly calendar with due tasks marked. Shows a monthly calendar with due tasks marked.

View file

@ -142,6 +142,9 @@ void Cmd::load ()
commands.push_back ("history.annual"); commands.push_back ("history.annual");
commands.push_back ("ghistory.monthly"); commands.push_back ("ghistory.monthly");
commands.push_back ("ghistory.annual"); 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 whose names are localized.
commands.push_back (context.stringtable.get (CMD_ADD, "add")); commands.push_back (context.stringtable.get (CMD_ADD, "add"));

View file

@ -136,6 +136,10 @@ std::string Config::defaults =
"color.history.done=color0 on rgb050 # Color of completed tasks in ghistory report\n" "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" "color.history.delete=color0 on rgb550 # Color of deleted tasks in ghistory report\n"
"\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.added=rgb005 # Color of added tasks in sync output\n"
"color.sync.changed=rgb550 # Color of changed 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" "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.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" "color.history.delete=black on yellow # Color of deleted tasks in ghistory report\n"
"\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.added=green # Color of added tasks in sync output\n"
"color.sync.changed=yellow # Color of changed 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" "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.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=export.yaml # Prefer YAML over CSV or iCal export\n"
"alias.export.vcalendar=export.ical # They are the same\n" "alias.export.vcalendar=export.ical # They are the same\n"
"alias.burndown=burndown.weekly # Prefer the weekly burndown chart\n"
"\n" "\n"
"# Fields: id, uuid, project, priority, priority_long, entry, start, end,\n" "# Fields: id, uuid, project, priority, priority_long, entry, start, end,\n"
"# due, countdown, countdown_compact, age, age_compact, active, tags,\n" "# due, countdown, countdown_compact, age, age_compact, active, tags,\n"

View file

@ -218,6 +218,9 @@ int Context::dispatch (std::string &out)
else if (cmd.command == "history.annual") { rc = handleReportHistoryAnnual (out); } else if (cmd.command == "history.annual") { rc = handleReportHistoryAnnual (out); }
else if (cmd.command == "ghistory.monthly") { rc = handleReportGHistoryMonthly (out); } else if (cmd.command == "ghistory.monthly") { rc = handleReportGHistoryMonthly (out); }
else if (cmd.command == "ghistory.annual") { rc = handleReportGHistoryAnnual (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 == "summary") { rc = handleReportSummary (out); }
else if (cmd.command == "calendar") { rc = handleReportCalendar (out); } else if (cmd.command == "calendar") { rc = handleReportCalendar (out); }
else if (cmd.command == "timesheet") { rc = handleReportTimesheet (out); } else if (cmd.command == "timesheet") { rc = handleReportTimesheet (out); }

View file

@ -463,7 +463,7 @@ const std::string Date::toString (const std::string& format /*= "m/d/Y" */) cons
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
Date Date::startOfDay () const 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); Date sow (mT);
sow -= (dayOfWeek () * 86400); 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 Date Date::startOfMonth () const
{ {
return Date (month (), 1, year ()); return Date (month (), 1, year (), 0, 0, 0);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
Date Date::startOfYear () const 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; 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) bool Date::isEpoch (const std::string& input)
{ {

View file

@ -97,6 +97,11 @@ public:
time_t operator- (const Date&); time_t operator- (const Date&);
void operator-- (); // Prefix
void operator-- (int); // Postfix
void operator++ (); // Prefix
void operator++ (int); // Postfix
private: private:
bool isEpoch (const std::string&); bool isEpoch (const std::string&);
bool isRelativeDate (const std::string&); bool isRelativeDate (const std::string&);

View file

@ -155,6 +155,8 @@ Hooks::Hooks ()
validProgramEvents.push_back ("post-ghistory-command"); validProgramEvents.push_back ("post-ghistory-command");
validProgramEvents.push_back ("pre-history-command"); validProgramEvents.push_back ("pre-history-command");
validProgramEvents.push_back ("post-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 ("pre-import-command");
validProgramEvents.push_back ("post-import-command"); validProgramEvents.push_back ("post-import-command");
validProgramEvents.push_back ("pre-info-command"); validProgramEvents.push_back ("pre-info-command");

View file

@ -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 \ Task.cpp Task.h Taskmod.cpp Taskmod.h Thread.cpp Thread.h \
Timer.cpp Timer.h Transport.cpp Transport.h TransportSSH.cpp \ Timer.cpp Timer.h Transport.cpp Transport.h TransportSSH.cpp \
TransportSSH.h TransportRSYNC.cpp TransportRSYNC.h \ TransportSSH.h TransportRSYNC.cpp TransportRSYNC.h \
TransportCurl.cpp TransportCurl.h Tree.cpp Tree.h command.cpp \ TransportCurl.cpp TransportCurl.h Tree.cpp Tree.h burndown.cpp \
custom.cpp dependency.cpp diag.cpp edit.cpp export.cpp i18n.h \ command.cpp custom.cpp dependency.cpp diag.cpp edit.cpp \
import.cpp interactive.cpp main.cpp main.h recur.cpp report.cpp \ export.cpp history.cpp i18n.h import.cpp interactive.cpp \
rules.cpp rx.cpp rx.h text.cpp text.h util.cpp util.h Uri.cpp \ main.cpp main.h recur.cpp report.cpp rules.cpp rx.cpp rx.h \
Uri.h text.cpp text.h util.cpp util.h Uri.cpp Uri.h
task_CPPFLAGS=$(LUA_CFLAGS) task_CPPFLAGS=$(LUA_CFLAGS)
task_LDFLAGS=$(LUA_LFLAGS) task_LDFLAGS=$(LUA_LFLAGS)

627
src/burndown.cpp Normal file
View 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
View 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;
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -103,10 +103,6 @@ int shortUsage (std::string&);
int longUsage (std::string&); int longUsage (std::string&);
int handleInfo (std::string&); int handleInfo (std::string&);
int handleReportSummary (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 handleReportCalendar (std::string&);
int handleReportStats (std::string&); int handleReportStats (std::string&);
int handleReportTimesheet (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&, bool scope = true);
std::string onProjectChange (Task&, Task&); 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 // custom.cpp
int handleCustomReport (const std::string&, std::string&); int handleCustomReport (const std::string&, std::string&);
void validReportColumns (const std::vector <std::string>&); void validReportColumns (const std::vector <std::string>&);

View file

@ -187,6 +187,18 @@ int shortUsage (std::string& outs)
table.addCell (row, 1, "task ghistory.annual"); table.addCell (row, 1, "task ghistory.annual");
table.addCell (row, 2, "Shows a graphical report of task history, by year."); 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 (); row = table.addRow ();
table.addCell (row, 1, "task calendar [due|month year|year]"); table.addCell (row, 1, "task calendar [due|month year|year]");
table.addCell (row, 2, "Shows a calendar, with due tasks marked."); table.addCell (row, 2, "Shows a calendar, with due tasks marked.");
@ -819,771 +831,6 @@ int handleReportSummary (std::string& outs)
return rc; 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) int handleReportTimesheet (std::string& outs)
{ {

View file

@ -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-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-Transport.o ../t-TransportSSH.o ../t-Sensor.o ../t-Thread.o \
../t-Lisp.o ../t-Rectangle.o ../t-Tree.o ../t-TransportRSYNC.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) all: $(PROJECT)

View file

@ -34,7 +34,7 @@ Context context;
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int main (int argc, char** argv) int main (int argc, char** argv)
{ {
UnitTest t (158); UnitTest t (162);
try try
{ {
@ -347,6 +347,24 @@ int main (int argc, char** argv)
// Date::operator- // Date::operator-
Date r22 (1234567890); Date r22 (1234567890);
t.is ((r22 - 1).toEpoch (), 1234567889, "1234567890 - 1 = 1234567889"); 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) catch (std::string& e)