diff --git a/ChangeLog b/ChangeLog index 08ec3adaa..b2da7570c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 diff --git a/NEWS b/NEWS index c4f90168b..e3d9726b0 100644 --- a/NEWS +++ b/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 diff --git a/doc/man/task.1 b/doc/man/task.1 index 7f57db19a..0e3fbee37 100644 --- a/doc/man/task.1 +++ b/doc/man/task.1 @@ -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. diff --git a/src/Cmd.cpp b/src/Cmd.cpp index ef97101aa..9acd1ee35 100644 --- a/src/Cmd.cpp +++ b/src/Cmd.cpp @@ -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")); diff --git a/src/Config.cpp b/src/Config.cpp index b287b1e8a..f9df2dbef 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -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" diff --git a/src/Context.cpp b/src/Context.cpp index 0af4d07e4..a3ed18cb3 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -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); } diff --git a/src/Date.cpp b/src/Date.cpp index c83cc06b5..cb3272021 100644 --- a/src/Date.cpp +++ b/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) { diff --git a/src/Date.h b/src/Date.h index 1cb93b497..3db880936 100644 --- a/src/Date.h +++ b/src/Date.h @@ -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&); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index e9e9d28f2..40ed5715c 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -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"); diff --git a/src/Makefile.am b/src/Makefile.am index 3203b4d00..55f4b1fce 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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) diff --git a/src/burndown.cpp b/src/burndown.cpp new file mode 100644 index 000000000..7b36a7686 --- /dev/null +++ b/src/burndown.cpp @@ -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 // TODO Remove +//#include +#include +//#include +#include +//#include +//#include +//#include +//#include +//#include +//#include +#include + +#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 +//#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 & 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 groups; + std::map pendingGroup; + std::map startedGroup; + std::map doneGroup; + + std::map addGroup; + std::map removeGroup; + + // Scan the pending tasks, applying any filter. + std::vector 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 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 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; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/history.cpp b/src/history.cpp new file mode 100644 index 000000000..9f15516ef --- /dev/null +++ b/src/history.cpp @@ -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 +//#include +#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include + +#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 +//#endif + +extern Context context; + +//////////////////////////////////////////////////////////////////////////////// +int handleReportHistoryMonthly (std::string& outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-history-command")) + { + std::map groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector 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 groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector 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 groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector 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 groups; // Represents any month with data + std::map addedGroup; // Additions by month + std::map completedGroup; // Completions by month + std::map deletedGroup; // Deletions by month + + // Scan the pending tasks. + std::vector 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; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/main.h b/src/main.h index bed021285..8f91473aa 100644 --- a/src/main.h +++ b/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 &); diff --git a/src/report.cpp b/src/report.cpp index cce14671a..759d2a856 100644 --- a/src/report.cpp +++ b/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 groups; // Represents any month with data - std::map addedGroup; // Additions by month - std::map completedGroup; // Completions by month - std::map deletedGroup; // Deletions by month - - // Scan the pending tasks. - std::vector 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 groups; // Represents any month with data - std::map addedGroup; // Additions by month - std::map completedGroup; // Completions by month - std::map deletedGroup; // Deletions by month - - // Scan the pending tasks. - std::vector 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 groups; // Represents any month with data - std::map addedGroup; // Additions by month - std::map completedGroup; // Completions by month - std::map deletedGroup; // Deletions by month - - // Scan the pending tasks. - std::vector 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 groups; // Represents any month with data - std::map addedGroup; // Additions by month - std::map completedGroup; // Completions by month - std::map deletedGroup; // Deletions by month - - // Scan the pending tasks. - std::vector 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) { diff --git a/src/tests/Makefile b/src/tests/Makefile index 7633b16f3..f0ae9ebcb 100644 --- a/src/tests/Makefile +++ b/src/tests/Makefile @@ -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) diff --git a/src/tests/date.t.cpp b/src/tests/date.t.cpp index ca7641a53..17f2f067c 100644 --- a/src/tests/date.t.cpp +++ b/src/tests/date.t.cpp @@ -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)