Logo Search packages:      
Sourcecode: task version File versions  Download package

report.cpp

////////////////////////////////////////////////////////////////////////////////
// task - a command line task list manager.
//
// Copyright 2006 - 2009, Paul Beckingham.
// 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 <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pwd.h>
#include <time.h>

#include "Context.h"
#include "Date.h"
#include "Table.h"
#include "text.h"
#include "util.h"
#include "main.h"

#ifdef HAVE_LIBNCURSES
#include <ncurses.h>
#endif

extern Context context;

////////////////////////////////////////////////////////////////////////////////
int shortUsage (std::string &outs)
{
  Table table;

  table.addColumn (" ");
  table.addColumn (" ");
  table.addColumn (" ");

  table.setColumnJustification (0, Table::left);
  table.setColumnJustification (1, Table::left);
  table.setColumnJustification (2, Table::left);

  table.setColumnWidth (0, Table::minimum);
  table.setColumnWidth (1, Table::minimum);
  table.setColumnWidth (2, Table::flexible);
  table.setTableWidth (context.getWidth ());
  table.setDateFormat (context.config.get ("dateformat", "m/d/Y"));

  int row = table.addRow ();
  table.addCell (row, 0, "Usage:");
  table.addCell (row, 1, "task");

  row = table.addRow ();
  table.addCell (row, 1, "task add [tags] [attrs] desc...");
  table.addCell (row, 2, "Adds a new task.");

  row = table.addRow ();
  table.addCell (row, 1, "task append [tags] [attrs] desc...");
  table.addCell (row, 2, "Appends more description to an existing task.");

  row = table.addRow ();
  table.addCell (row, 1, "task annotate ID desc...");
  table.addCell (row, 2, "Adds an annotation to an existing task.");

  row = table.addRow ();
  table.addCell (row, 1, "task ID [tags] [attrs] [desc...]");
  table.addCell (row, 2, "Modifies the existing task with provided arguments.");

  row = table.addRow ();
  table.addCell (row, 1, "task ID /from/to/g");
  table.addCell (row, 2, "Performs substitution on the task description and "
                         "annotations.  The 'g' is optional, and causes "
                         "substitutions for all matching text, not just the "
                         "first occurrence.");

  row = table.addRow ();
  table.addCell (row, 1, "task edit ID");
  table.addCell (row, 2, "Launches an editor to let you modify all aspects of a task directly, therefore it is to be used carefully.");

  row = table.addRow ();
  table.addCell (row, 1, "task undo");
  table.addCell (row, 2, "Reverts the most recent action.");

#ifdef FEATURE_SHELL
  row = table.addRow ();
  table.addCell (row, 1, "task shell");
  table.addCell (row, 2, "Launches an interactive shell.");
#endif

  row = table.addRow ();
  table.addCell (row, 1, "task duplicate ID [tags] [attrs] [desc...]");
  table.addCell (row, 2, "Duplicates the specified task, and allows modifications.");

  row = table.addRow ();
  table.addCell (row, 1, "task delete ID");
  table.addCell (row, 2, "Deletes the specified task.");

  row = table.addRow ();
  table.addCell (row, 1, "task info ID");
  table.addCell (row, 2, "Shows all data, metadata for specified task.");

  row = table.addRow ();
  table.addCell (row, 1, "task start ID");
  table.addCell (row, 2, "Marks specified task as started.");

  row = table.addRow ();
  table.addCell (row, 1, "task stop ID");
  table.addCell (row, 2, "Removes the 'start' time from a task.");

  row = table.addRow ();
  table.addCell (row, 1, "task done ID [tags] [attrs] [desc...]");
  table.addCell (row, 2, "Marks the specified task as completed.");

  row = table.addRow ();
  table.addCell (row, 1, "task projects");
  table.addCell (row, 2, "Shows a list of all project names used, and how many tasks are in each.");

  row = table.addRow ();
  table.addCell (row, 1, "task tags");
  table.addCell (row, 2, "Shows a list of all tags used.");

  row = table.addRow ();
  table.addCell (row, 1, "task summary");
  table.addCell (row, 2, "Shows a report of task status by project.");

  row = table.addRow ();
  table.addCell (row, 1, "task timesheet [weeks]");
  table.addCell (row, 2, "Shows a weekly report of tasks completed and started.");

  row = table.addRow ();
  table.addCell (row, 1, "task history");
  table.addCell (row, 2, "Shows a report of task history, by month.");

  row = table.addRow ();
  table.addCell (row, 1, "task ghistory");
  table.addCell (row, 2, "Shows a graphical report of task history, 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.");

  row = table.addRow ();
  table.addCell (row, 1, "task stats");
  table.addCell (row, 2, "Shows task database statistics.");

  row = table.addRow ();
  table.addCell (row, 1, "task import");
  table.addCell (row, 2, "Imports tasks from a variety of formats.");

  row = table.addRow ();
  table.addCell (row, 1, "task export");
  table.addCell (row, 2, "Lists all tasks in CSV format.");

  row = table.addRow ();
  table.addCell (row, 1, "task color");
  table.addCell (row, 2, "Displays all possible colors.");

  row = table.addRow ();
  table.addCell (row, 1, "task version");
  table.addCell (row, 2, "Shows the task version number.");

  row = table.addRow ();
  table.addCell (row, 1, "task help");
  table.addCell (row, 2, "Shows the long usage text.");

  // Add custom reports here...
  std::vector <std::string> all;
  context.cmd.allCustomReports (all);
  foreach (report, all)
  {
    std::string command = std::string ("task ") + *report + std::string (" [tags] [attrs] desc...");
    std::string description = context.config.get (
      std::string ("report.") + *report + ".description", std::string ("(missing description)"));

    row = table.addRow ();
    table.addCell (row, 1, command);
    table.addCell (row, 2, description);
  }

  std::stringstream out;
  out << table.render ()
      << std::endl
      << "See http://taskwarrior.org/wiki/taskwarrior/Download for the latest "
      << "releases and a full tutorial.  New releases containing fixes and "
      << "enhancements are made frequently.  Join in the discussion of task, "
      << "present and future, at http://taskwarrior.org"
      << std::endl
      << std::endl;

  outs = out.str ();
  return 0;
}

////////////////////////////////////////////////////////////////////////////////
int longUsage (std::string &outs)
{
  std::string shortUsageString;
  std::stringstream out;

  (void)shortUsage(shortUsageString);

  out << shortUsageString
      << "ID is the numeric identifier displayed by the 'task list' command. "
      << "You can specify multiple IDs for task commands, and multiple tasks "
      << "will be affected.  To specify multiple IDs make sure you use one "
      << "of these forms:"                                                    << "\n"
      << "  task delete 1,2,3"                                                << "\n"
      << "  task info 1-3"                                                    << "\n"
      << "  task pri:H 1,2-5,19"                                              << "\n"
      <<                                                                         "\n"
      << "Tags are arbitrary words, any quantity:"                            << "\n"
      << "  +tag               The + means add the tag"                       << "\n"
      << "  -tag               The - means remove the tag"                    << "\n"
      <<                                                                         "\n"
      << "Attributes are:"                                                    << "\n"
      << "  project:           Project name"                                  << "\n"
      << "  priority:          Priority"                                      << "\n"
      << "  due:               Due date"                                      << "\n"
      << "  recur:             Recurrence frequency"                          << "\n"
      << "  until:             Recurrence end date"                           << "\n"
      << "  fg:                Foreground color"                              << "\n"
      << "  bg:                Background color"                              << "\n"
      << "  limit:             Desired number of rows in report"              << "\n"
      << "  wait:              Date until task becomes pending"               << "\n"
      <<                                                                         "\n"
      << "Attribute modifiers improve filters.  Supported modifiers are:"     << "\n"
      << "  before     (synonyms under, below)"                               << "\n"
      << "  after      (synonyms over, above)"                                << "\n"
      << "  none"                                                             << "\n"
      << "  any"                                                              << "\n"
      << "  is         (synonym equals)"                                      << "\n"
      << "  isnt       (synonym not)"                                         << "\n"
      << "  has        (synonym contain)"                                     << "\n"
      << "  hasnt"                                                            << "\n"
      << "  startswith (synonym left)"                                        << "\n"
      << "  endswith   (synonym right)"                                       << "\n"
      <<                                                                         "\n"
      << "  For example:"                                                     << "\n"
      << "    task list due.before:eom priority.not:L"                        << "\n"
      <<                                                                         "\n"
      << "The default .taskrc file can be overridden with:"                   << "\n"
      << "  task rc:<alternate file> ..."                                     << "\n"
      <<                                                                         "\n"
      << "The values in .taskrc (or alternate) can be overridden with:"       << "\n"
      << "  task ... rc.<name>:<value>"                                       << "\n"
      <<                                                                         "\n"
      << "Any command or attribute name may be abbreviated if still unique:"  << "\n"
      << "  task list project:Home"                                           << "\n"
      << "  task li       pro:Home"                                           << "\n"
      <<                                                                         "\n"
      << "Some task descriptions need to be escaped because of the shell:"    << "\n"
      << "  task add \"quoted ' quote\""                                      << "\n"
      << "  task add escaped \\' quote"                                       << "\n"
      <<                                                                         "\n"
      << "The argument -- tells task to treat all other args as description." << "\n"
      << "  task add -- project:Home needs scheduling"                        << "\n"
      <<                                                                         "\n"
      << "Many characters have special meaning to the shell, including:"      << "\n"
      << "  $ ! ' \" ( ) ; \\ ` * ? { } [ ] < > | & % # ~"                    << "\n"
      << std::endl;

  outs = out.str();
  return 0;
}

////////////////////////////////////////////////////////////////////////////////
// Display all information for the given task.
int handleInfo (std::string &outs)
{
  int rc = 0;
  // Get all the tasks.
  std::vector <Task> tasks;
  context.tdb.lock (context.config.get ("locking", true));
  handleRecurrence ();
  context.tdb.loadPending (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  // Filter sequence.
  context.filter.applySequence (tasks, context.sequence);

  // Find the task.
  std::stringstream out;
  foreach (task, tasks)
  {
    Table table;
    table.setTableWidth (context.getWidth ());
    table.setDateFormat (context.config.get ("dateformat", "m/d/Y"));

    table.addColumn ("Name");
    table.addColumn ("Value");

    if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
        context.config.get (std::string ("fontunderline"), "true"))
    {
      table.setColumnUnderline (0);
      table.setColumnUnderline (1);
    }
    else
      table.setTableDashedUnderline ();

    table.setColumnWidth (0, Table::minimum);
    table.setColumnWidth (1, Table::flexible);

    table.setColumnJustification (0, Table::left);
    table.setColumnJustification (1, Table::left);
    Date now;

    int row = table.addRow ();
    table.addCell (row, 0, "ID");
    table.addCell (row, 1, task->id);

    std::string status = ucFirst (Task::statusToText (task->getStatus ()));

    if (task->has ("parent"))
      status += " (Recurring)";

    row = table.addRow ();
    table.addCell (row, 0, "Status");
    table.addCell (row, 1, status);

    row = table.addRow ();
    table.addCell (row, 0, "Description");
    table.addCell (row, 1, getFullDescription (*task));

    if (task->has ("project"))
    {
      row = table.addRow ();
      table.addCell (row, 0, "Project");
      table.addCell (row, 1, task->get ("project"));
    }

    if (task->has ("priority"))
    {
      row = table.addRow ();
      table.addCell (row, 0, "Priority");
      table.addCell (row, 1, task->get ("priority"));
    }

    if (task->getStatus () == Task::recurring ||
        task->has ("parent"))
    {
      if (task->has ("recur"))
      {
        row = table.addRow ();
        table.addCell (row, 0, "Recurrence");
        table.addCell (row, 1, task->get ("recur"));
      }

      if (task->has ("until"))
      {
        row = table.addRow ();
        table.addCell (row, 0, "Recur until");
        table.addCell (row, 1, task->get ("until"));
      }

      if (task->has ("mask"))
      {
        row = table.addRow ();
        table.addCell (row, 0, "Mask");
        table.addCell (row, 1, task->get ("mask"));
      }

      if (task->has ("parent"))
      {
        row = table.addRow ();
        table.addCell (row, 0, "Parent task");
        table.addCell (row, 1, task->get ("parent"));
      }

      row = table.addRow ();
      table.addCell (row, 0, "Mask Index");
      table.addCell (row, 1, task->get ("imask"));
    }

    // due (colored)
    bool imminent = false;
    bool overdue = false;
    if (task->has ("due"))
    {
      row = table.addRow ();
      table.addCell (row, 0, "Due");

      Date dt (::atoi (task->get ("due").c_str ()));
      std::string due = getDueDate (*task);
      table.addCell (row, 1, due);

      overdue = (dt < now) ? true : false;
      Date nextweek = now + 7 * 86400;
      imminent = dt < nextweek ? true : false;

      if (context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false))
      {
        if (overdue)
          table.setCellFg (row, 1, Text::colorCode (context.config.get ("color.overdue", "red")));
        else if (imminent)
          table.setCellFg (row, 1, Text::colorCode (context.config.get ("color.due", "yellow")));
      }
    }

    // wait
    if (task->has ("wait"))
    {
      row = table.addRow ();
      table.addCell (row, 0, "Waiting until");
      Date dt (::atoi (task->get ("wait").c_str ()));
      table.addCell (row, 1, dt.toString (context.config.get ("dateformat", "m/d/Y")));
    }

    // start
    if (task->has ("start"))
    {
      row = table.addRow ();
      table.addCell (row, 0, "Start");
      Date dt (::atoi (task->get ("start").c_str ()));
      table.addCell (row, 1, dt.toString (context.config.get ("dateformat", "m/d/Y")));
    }

    // end
    if (task->has ("end"))
    {
      row = table.addRow ();
      table.addCell (row, 0, "End");
      Date dt (::atoi (task->get ("end").c_str ()));
      table.addCell (row, 1, dt.toString (context.config.get ("dateformat", "m/d/Y")));
    }

    // tags ...
    std::vector <std::string> tags;
    task->getTags (tags);
    if (tags.size ())
    {
      std::string allTags;
      join (allTags, " ", tags);

      row = table.addRow ();
      table.addCell (row, 0, "Tags");
      table.addCell (row, 1, allTags);
    }

    // uuid
    row = table.addRow ();
    table.addCell (row, 0, "UUID");
    table.addCell (row, 1, task->get ("uuid"));

    // entry
    row = table.addRow ();
    table.addCell (row, 0, "Entered");
    Date dt (::atoi (task->get ("entry").c_str ()));
    std::string entry = dt.toString (context.config.get ("dateformat", "m/d/Y"));

    std::string age;
    std::string created = task->get ("entry");
    if (created.length ())
    {
      Date dt (::atoi (created.c_str ()));
      age = formatSeconds ((time_t) (now - dt));
    }

    table.addCell (row, 1, entry + " (" + age + ")");

    // fg
    std::string color = task->get ("fg");
    if (color != "")
    {
      row = table.addRow ();
      table.addCell (row, 0, "Foreground color");
      table.addCell (row, 1, color);
    }

    // bg
    color = task->get ("bg");
    if (color != "")
    {
      row = table.addRow ();
      table.addCell (row, 0, "Background color");
      table.addCell (row, 1, color);
    }

    out << optionalBlankLine ()
        << table.render ()
        << std::endl;
  }

  if (! tasks.size ()) {
    out << "No matches." << std::endl;
    rc = 1;
  }

  outs = out.str ();
  return rc;
}

////////////////////////////////////////////////////////////////////////////////
// Project  Remaining  Avg Age  Complete  0%                  100%
// A               12      13d       55%  XXXXXXXXXXXXX-----------
// B              109   3d 12h       10%  XXX---------------------
int handleReportSummary (std::string &outs)
{
  int rc = 0;
  // Scan the pending tasks.
  std::vector <Task> tasks;
  context.tdb.lock (context.config.get ("locking", true));
  handleRecurrence ();
  context.tdb.load (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  // Generate unique list of project names from all pending tasks.
  std::map <std::string, bool> allProjects;
  foreach (task, tasks)
    if (task->getStatus () == Task::pending)
      allProjects[task->get ("project")] = false;

  // Initialize counts, sum.
  std::map <std::string, int> countPending;
  std::map <std::string, int> countCompleted;
  std::map <std::string, double> sumEntry;
  std::map <std::string, int> counter;
  time_t now = time (NULL);

  // Initialize counters.
  foreach (project, allProjects)
  {
    countPending   [project->first] = 0;
    countCompleted [project->first] = 0;
    sumEntry       [project->first] = 0.0;
    counter        [project->first] = 0;
  }

  // Count the various tasks.
  foreach (task, tasks)
  {
    std::string project = task->get ("project");
    ++counter[project];

    if (task->getStatus () == Task::pending ||
        task->getStatus () == Task::waiting)
    {
      ++countPending[project];

      time_t entry = ::atoi (task->get ("entry").c_str ());
      if (entry)
        sumEntry[project] = sumEntry[project] + (double) (now - entry);
    }

    else if (task->getStatus () == Task::completed)
    {
      ++countCompleted[project];

      time_t entry = ::atoi (task->get ("entry").c_str ());
      time_t end   = ::atoi (task->get ("end").c_str ());
      if (entry && end)
        sumEntry[project] = sumEntry[project] + (double) (end - entry);
    }
  }

  // Create a table for output.
  Table table;
  table.addColumn ("Project");
  table.addColumn ("Remaining");
  table.addColumn ("Avg age");
  table.addColumn ("Complete");
  table.addColumn ("0%                        100%");

  if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
      context.config.get (std::string ("fontunderline"), "true"))
  {
    table.setColumnUnderline (0);
    table.setColumnUnderline (1);
    table.setColumnUnderline (2);
    table.setColumnUnderline (3);
  }
  else
    table.setTableDashedUnderline ();

  table.setColumnJustification (1, Table::right);
  table.setColumnJustification (2, Table::right);
  table.setColumnJustification (3, Table::right);

  table.sortOn (0, Table::ascendingCharacter);
  table.setDateFormat (context.config.get ("dateformat", "m/d/Y"));

  int barWidth = 30;
  foreach (i, allProjects)
  {
    if (countPending[i->first] > 0)
    {
      int row = table.addRow ();
      table.addCell (row, 0, (i->first == "" ? "(none)" : i->first));
      table.addCell (row, 1, countPending[i->first]);
      if (counter[i->first])
      {
        std::string age;
        age = formatSeconds ((time_t) (sumEntry[i->first] / counter[i->first]));
        table.addCell (row, 2, age);
      }

      int c = countCompleted[i->first];
      int p = countPending[i->first];
      int completedBar = (c * barWidth) / (c + p);

      std::string bar;
      if (context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false))
      {
        bar = "\033[42m";
        for (int b = 0; b < completedBar; ++b)
          bar += " ";

        bar += "\033[40m";
        for (int b = 0; b < barWidth - completedBar; ++b)
          bar += " ";

        bar += "\033[0m";
      }
      else
      {
        for (int b = 0; b < completedBar; ++b)
          bar += "=";

        for (int b = 0; b < barWidth - completedBar; ++b)
          bar += " ";
      }
      table.addCell (row, 4, bar);

      char percent[12];
      sprintf (percent, "%d%%", 100 * c / (c + p));
      table.addCell (row, 3, percent);
    }
  }

  std::stringstream out;
  if (table.rowCount ())
    out << optionalBlankLine ()
        << table.render ()
        << optionalBlankLine ()
        << table.rowCount ()
        << (table.rowCount () == 1 ? " project" : " projects")
        << std::endl;
  else {
    out << "No projects." << std::endl;
    rc = 1;
  }

  outs = out.str ();
  return rc;
}

////////////////////////////////////////////////////////////////////////////////
// A summary of the most important pending tasks.
//
// For every project, pull important tasks to present as an 'immediate' task
// list.  This hides the overwhelming quantity of other tasks.
//
// Present at most three tasks for every project.  Select the tasks using
// these criteria:
//   - due:< 1wk, pri:*
//   - due:*, pri:H
//   - pri:H
//   - due:*, pri:M
//   - pri:M
//   - due:*, pri:L
//   - pri:L
//   - due:*, pri:*        <-- everything else
//
// Make the "three" tasks a configurable number
//
int handleReportNext (std::string &outs)
{
  // Load report configuration.
  std::string columnList = context.config.get ("report.next.columns");
  std::string labelList  = context.config.get ("report.next.labels");
  std::string sortList   = context.config.get ("report.next.sort");
  std::string filterList = context.config.get ("report.next.filter");

  std::vector <std::string> filterArgs;
  split (filterArgs, filterList, ' ');
  {
    Cmd cmd ("next");
    Task task;
    Sequence sequence;
    Subst subst;
    Filter filter;
    context.parse (filterArgs, cmd, task, sequence, subst, filter);

    context.sequence.combine (sequence);

    // Allow limit to be overridden by the command line.
    if (!context.task.has ("limit") && task.has ("limit"))
      context.task.set ("limit", task.get ("limit"));

    foreach (att, filter)
      context.filter.push_back (*att);
  }

  // Get all the tasks.
  std::vector <Task> tasks;
  context.tdb.lock (context.config.get ("locking", true));
  handleRecurrence ();
  context.tdb.load (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  // Restrict to matching subset.
  gatherNextTasks (tasks);

  return runCustomReport (
    "next",
    columnList,
    labelList,
    sortList,
    filterList,
    tasks,
    outs);
}

////////////////////////////////////////////////////////////////////////////////
// Year Month    Added Completed Deleted
// 2006 November    87        63      14
//      December    21         6       1
// 2007 January      3        12       0
time_t monthlyEpoch (const std::string& date)
{
  // Convert any date in epoch form to m/d/y, then convert back
  // to epoch form for the date m/1/y.
  if (date.length ())
  {
    Date d1 (::atoi (date.c_str ()));
    int m, d, y;
    d1.toMDY (m, d, y);
    Date d2 (m, 1, y);
    time_t epoch;
    d2.toEpoch (epoch);
    return epoch;
 }

  return 0;
}

int handleReportHistory (std::string &outs)
{
  int rc = 0;
  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.get ("locking", true));
  handleRecurrence ();
  context.tdb.load (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  foreach (task, tasks)
  {
    time_t epoch = monthlyEpoch (task->get ("entry"));
    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 = monthlyEpoch (task->get ("end"));
      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 = monthlyEpoch (task->get ("end"));
      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", "m/d/Y"));
  table.addColumn ("Year");
  table.addColumn ("Month");
  table.addColumn ("Added");
  table.addColumn ("Completed");
  table.addColumn ("Deleted");
  table.addColumn ("Net");

  if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
      context.config.get (std::string ("fontunderline"), "true"))
  {
    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.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) && net)
      table.setCellFg (row, 5, net > 0 ? Text::red: Text::green);
  }

  if (table.rowCount ())
  {
    table.addRow ();
    row = table.addRow ();

    table.addCell (row, 1, "Average");
    if (context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) table.setRowFg (row, Text::bold);
    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 ()
        << std::endl;
  else {
    out << "No tasks." << std::endl;
    rc = 1;
  }

  outs = out.str ();
  return rc;
}

////////////////////////////////////////////////////////////////////////////////
int handleReportGHistory (std::string &outs)
{
  int rc = 0;
  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.get ("locking", true));
  handleRecurrence ();
  context.tdb.load (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  foreach (task, tasks)
  {
    time_t epoch = monthlyEpoch (task->get ("entry"));
    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 = monthlyEpoch (task->get ("end"));
      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 = monthlyEpoch (task->get ("end"));
      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", "m/d/Y"));
  table.addColumn ("Year");
  table.addColumn ("Month");
  table.addColumn ("Number Added/Completed/Deleted");

  if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
      context.config.get (std::string ("fontunderline"), "true"))
  {
    table.setColumnUnderline (0);
    table.setColumnUnderline (1);
  }
  else
    table.setTableDashedUnderline ();

  // 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.get ("color", true) || context.config.get (std::string ("_forcecolor"), false))
      {
        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;
        }

        while (bar.length () < leftOffset - aBar.length ())
          bar += " ";

        bar += Text::colorize (Text::black, Text::on_red,    aBar);
        bar += Text::colorize (Text::black, Text::on_green,  cBar);
        bar += Text::colorize (Text::black, Text::on_yellow, 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 += "-";

        while (bar.length () < leftOffset - aBar.length ())
          bar += " ";

        bar += aBar + cBar + dBar;
      }

      table.addCell (row, 2, bar);
    }
  }

  std::stringstream out;
  if (table.rowCount ())
  {
    out << optionalBlankLine ()
        << table.render ()
        << std::endl;

    if (context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false))
      out << "Legend: "
          << Text::colorize (Text::black, Text::on_red, "added")
          << ", "
          << Text::colorize (Text::black, Text::on_green, "completed")
          << ", "
          << Text::colorize (Text::black, Text::on_yellow, "deleted")
          << optionalBlankLine ()
          << std::endl;
    else
      out << "Legend: + added, X completed, - deleted" << std::endl;
  }
  else {
    out << "No tasks." << std::endl;
    rc = 1;
  }

  outs = out.str ();
  return rc;
}

////////////////////////////////////////////////////////////////////////////////
int handleReportTimesheet (std::string &outs)
{
  // Scan the pending tasks.
  std::vector <Task> tasks;
  context.tdb.lock (context.config.get ("locking", true));
  handleRecurrence ();
  context.tdb.load (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  // Just do this once.
  int width = context.getWidth ();

  // What day of the week does the user consider the first?
  int weekStart = Date::dayOfWeek (context.config.get ("weekstart", "Sunday"));
  if (weekStart != 0 && weekStart != 1)
    throw std::string ("The 'weekstart' configuration variable may "
                       "only contain 'Sunday' or 'Monday'.");

  // Determine the date of the first day of the most recent report.
  Date today;
  Date start;
  start -= (((today.dayOfWeek () - weekStart) + 7) % 7) * 86400;

  // Roll back to midnight.
  start = Date (start.month (), start.day (), start.year ());
  Date end = start + (7 * 86400);

  // Determine how many reports to run.
  int quantity = 1;
  if (context.sequence.size () == 1)
    quantity = context.sequence[0];

  bool color = context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false);

  std::stringstream out;
  for (int week = 0; week < quantity; ++week)
  {
    Date endString (end);
    endString -= 86400;
    out << std::endl
        << (color ? Text::colorize (Text::bold, Text::nocolor) : "")
        << start.toString (context.config.get ("dateformat", "m/d/Y"))
        << " - "
        << endString.toString (context.config.get ("dateformat", "m/d/Y"))
        << (color ? Text::colorize () : "")
        << std::endl;

    // Render the completed table.
    Table completed;
    completed.setTableWidth (width);
    completed.addColumn ("   ");
    completed.addColumn ("Project");
    completed.addColumn ("Due");
    completed.addColumn ("Description");

    if (color && context.config.get (std::string ("fontunderline"), "true"))
    {
      completed.setColumnUnderline (1);
      completed.setColumnUnderline (2);
      completed.setColumnUnderline (3);
    }
    else
      completed.setTableDashedUnderline ();

    completed.setColumnWidth (0, Table::minimum);
    completed.setColumnWidth (1, Table::minimum);
    completed.setColumnWidth (2, Table::minimum);
    completed.setColumnWidth (3, Table::flexible);

    completed.setColumnJustification (0, Table::left);
    completed.setColumnJustification (1, Table::left);
    completed.setColumnJustification (2, Table::right);
    completed.setColumnJustification (3, Table::left);

    foreach (task, tasks)
    {
      // If task completed within range.
      if (task->getStatus () == Task::completed)
      {
        Date compDate (::atoi (task->get ("end").c_str ()));
        if (compDate >= start && compDate < end)
        {
          int row = completed.addRow ();
          completed.addCell (row, 1, task->get ("project"));
          completed.addCell (row, 2, getDueDate (*task));
          completed.addCell (row, 3, getFullDescription (*task));

          if (color)
          {
            Text::color fg = Text::colorCode (task->get ("fg"));
            Text::color bg = Text::colorCode (task->get ("bg"));
            autoColorize (*task, fg, bg);
            completed.setRowFg (row, fg);
            completed.setRowBg (row, bg);
          }
        }
      }
    }

    out << "  Completed (" << completed.rowCount () << " tasks)" << std::endl;

    if (completed.rowCount ())
      out << completed.render ()
          << std::endl;

    // Now render the started table.
    Table started;
    started.setTableWidth (width);
    started.addColumn ("   ");
    started.addColumn ("Project");
    started.addColumn ("Due");
    started.addColumn ("Description");

    if (color && context.config.get (std::string ("fontunderline"), "true"))
    {
      completed.setColumnUnderline (1);
      completed.setColumnUnderline (2);
      completed.setColumnUnderline (3);
    }
    else
      completed.setTableDashedUnderline ();

    started.setColumnWidth (0, Table::minimum);
    started.setColumnWidth (1, Table::minimum);
    started.setColumnWidth (2, Table::minimum);
    started.setColumnWidth (3, Table::flexible);

    started.setColumnJustification (0, Table::left);
    started.setColumnJustification (1, Table::left);
    started.setColumnJustification (2, Table::right);
    started.setColumnJustification (3, Table::left);
    foreach (task, tasks)
    {
      // If task started within range, but not completed withing range.
      if (task->getStatus () == Task::pending &&
          task->has ("start"))
      {
        Date startDate (::atoi (task->get ("start").c_str ()));
        if (startDate >= start && startDate < end)
        {
          int row = started.addRow ();
          started.addCell (row, 1, task->get ("project"));
          started.addCell (row, 2, getDueDate (*task));
          started.addCell (row, 3, getFullDescription (*task));

          if (color)
          {
            Text::color fg = Text::colorCode (task->get ("fg"));
            Text::color bg = Text::colorCode (task->get ("bg"));
            autoColorize (*task, fg, bg);
            started.setRowFg (row, fg);
            started.setRowBg (row, bg);
          }
        }
      }
    }

    out << "  Started (" << started.rowCount () << " tasks)" << std::endl;

    if (started.rowCount ())
      out << started.render ()
          << std::endl
          << std::endl;

    // Prior week.
    start -= 7 * 86400;
    end   -= 7 * 86400;
  }

  outs = out.str ();
  return 0;
}

////////////////////////////////////////////////////////////////////////////////
std::string renderMonths (
  int firstMonth,
  int firstYear,
  const Date& today,
  std::vector <Task>& all,
  int monthsPerLine)
{
  Table table;
  table.setDateFormat (context.config.get ("dateformat", "m/d/Y"));

  // What day of the week does the user consider the first?
  int weekStart = Date::dayOfWeek (context.config.get ("weekstart", "Sunday"));
  if (weekStart != 0 && weekStart != 1)
    throw std::string ("The 'weekstart' configuration variable may "
                       "only contain 'Sunday' or 'Monday'.");

  // Build table for the number of months to be displayed.
  for (int i = 0 ; i < (monthsPerLine * 8); i += 8)
  {
    if (weekStart == 1)
    {
      table.addColumn (" ");
      table.addColumn ("Mo");
      table.addColumn ("Tu");
      table.addColumn ("We");
      table.addColumn ("Th");
      table.addColumn ("Fr");
      table.addColumn ("Sa");
      table.addColumn ("Su");
    }
    else
    {
      table.addColumn (" ");
      table.addColumn ("Su");
      table.addColumn ("Mo");
      table.addColumn ("Tu");
      table.addColumn ("We");
      table.addColumn ("Th");
      table.addColumn ("Fr");
      table.addColumn ("Sa");
    }

    if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
        context.config.get (std::string ("fontunderline"), "true"))
    {
      table.setColumnUnderline (i + 1);
      table.setColumnUnderline (i + 2);
      table.setColumnUnderline (i + 3);
      table.setColumnUnderline (i + 4);
      table.setColumnUnderline (i + 5);
      table.setColumnUnderline (i + 6);
      table.setColumnUnderline (i + 7);
    }
    else
      table.setTableDashedUnderline ();

    table.setColumnJustification (i + 0, Table::right);
    table.setColumnJustification (i + 1, Table::right);
    table.setColumnJustification (i + 2, Table::right);
    table.setColumnJustification (i + 3, Table::right);
    table.setColumnJustification (i + 4, Table::right);
    table.setColumnJustification (i + 5, Table::right);
    table.setColumnJustification (i + 6, Table::right);
    table.setColumnJustification (i + 7, Table::right);

    // This creates a nice gap between the months.
    table.setColumnWidth (i + 0, 4);
  }

  // At most, we need 6 rows.
  table.addRow ();
  table.addRow ();
  table.addRow ();
  table.addRow ();
  table.addRow ();
  table.addRow ();

  // Set number of days per month, months to render, and years to render.
  std::vector<int> years;
  std::vector<int> months;
  std::vector<int> daysInMonth;
  int thisYear = firstYear;
  int thisMonth = firstMonth;
  for (int i = 0 ; i < monthsPerLine ; i++)
  {
    if (thisMonth < 13)
    {
      years.push_back (thisYear);
    }
    else
    {
      thisMonth -= 12;
      years.push_back (++thisYear);
    }
    months.push_back (thisMonth);
    daysInMonth.push_back (Date::daysInMonth (thisMonth++, thisYear));
  }

  int row = 0;

  // Loop through months to be added on this line.
  for (int mpl = 0; mpl < monthsPerLine ; mpl++)
  {
    // Reset row counter for subsequent months
    if (mpl != 0)
      row = 0;

    // Loop through days in month and add to table.
    for (int d = 1; d <= daysInMonth[mpl]; ++d)
    {
      Date temp (months[mpl], d, years[mpl]);
      int dow = temp.dayOfWeek ();
      int woy = temp.weekOfYear (weekStart);

      if (context.config.get ("displayweeknumber", true))
        table.addCell (row, (8 * mpl), woy);

      // Calculate column id.
      int thisCol = dow +                       // 0 = Sunday
                    (weekStart == 1 ? 0 : 1) +  // Offset for weekStart
                    (8 * mpl);                  // Columns in 1 month

      if (thisCol == (8 * mpl))
        thisCol += 7;

      table.addCell (row, thisCol, d);

      if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
          today.day ()   == d            &&
          today.month () == months[mpl]  &&
          today.year ()  == years[mpl])
        table.setCellFg (row, thisCol, Text::cyan);

      foreach (task, all)
      {
        if (task->getStatus () == Task::pending &&
            task->has ("due"))
        {
          Date due (::atoi (task->get ("due").c_str ()));

          if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
              due.day ()   == d               &&
              due.month () == months[mpl] &&
              due.year ()  == years[mpl])
          {
            table.setCellFg (row, thisCol, Text::black);
            table.setCellBg (row, thisCol, due < today ? Text::on_red : Text::on_yellow);
          }
        }
      }

      // Check for end of week, and...
      int eow = 6;
      if (weekStart == 1)
        eow = 0;
      if (dow == eow && d < daysInMonth[mpl])
        row++;
    }
  }

  return table.render ();
}

////////////////////////////////////////////////////////////////////////////////
int handleReportCalendar (std::string &outs)
{
  // Each month requires 28 text columns width.  See how many will actually
  // fit.  But if a preference is specified, and it fits, use it.
  int width = context.getWidth ();
  int preferredMonthsPerLine = (context.config.get (std::string ("monthsperline"), 0));
  int monthsThatFit = width / 26;

  int monthsPerLine = monthsThatFit;
  if (preferredMonthsPerLine != 0 && preferredMonthsPerLine < monthsThatFit)
    monthsPerLine = preferredMonthsPerLine;

  // Get all the tasks.
  std::vector <Task> tasks;
  Filter filter;
  context.tdb.lock (context.config.get ("locking", true));
  handleRecurrence ();
  context.tdb.loadPending (tasks, filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  Date today;
  bool getpendingdate = false;
  int monthsToDisplay = 1;
  int mFrom = today.month ();
  int yFrom = today.year ();
  int mTo;
  int yTo;

  // Determine what to do
  int numberOfArgs = context.args.size();

  if (numberOfArgs == 1 ) {
    // task cal
    monthsToDisplay = monthsPerLine;
    mFrom = today.month();
    yFrom = today.year();
  }
  else if (numberOfArgs == 2 ) {
    if (context.args[1] == "y") {
      // task cal y
      monthsToDisplay = 12;
      mFrom = today.month();
      yFrom = today.year();
    }
    else if (context.args[1] == "due") {
      // task cal due
      monthsToDisplay = monthsPerLine;
      getpendingdate = true;
    }
    else {
      // task cal 2010
      monthsToDisplay = 12;
      mFrom = 1;
      yFrom = ::atoi( context.args[1].data());
    }
  }
  else if (numberOfArgs == 3 ) {
    if (context.args[2] == "y") {
      // task cal due y
      monthsToDisplay = 12;
      getpendingdate = true;
    }
    else {
      // task cal 8 2010
      monthsToDisplay = monthsPerLine;
      mFrom = ::atoi( context.args[1].data());
      yFrom = ::atoi( context.args[2].data());
    }
  }
  else if (numberOfArgs == 4 ) {
    // task cal 8 2010 y
    monthsToDisplay = 12;
    mFrom = ::atoi( context.args[1].data());
    yFrom = ::atoi( context.args[2].data());
  }

  if (getpendingdate == true) {
    // Find the oldest pending due date.
    Date oldest (12,31,2037);
    foreach (task, tasks)
    {
      if (task->getStatus () == Task::pending)
      {
        if (task->has ("due"))
        {
          Date d (::atoi (task->get ("due").c_str ()));
          if (d < oldest) oldest = d;
        }
      }
    }
    mFrom = oldest.month();
    yFrom = oldest.year();
  }

  mTo = mFrom + monthsToDisplay - 1;
  yTo = yFrom;
  if (mTo > 12) {
    mTo -=12;
    yTo++;
  }

  std::stringstream out;
  out << std::endl;
  std::string output;

  while (yFrom < yTo || (yFrom == yTo && mFrom <= mTo))
  {
    int nextM = mFrom;
    int nextY = yFrom;

    // Print month headers (cheating on the width settings, yes)
    for (int i = 0 ; i < monthsPerLine ; i++)
    {
      std::string month = Date::monthName (nextM);

      //    12345678901234567890123456 = 26 chars wide
      //                ^^             = center
      //    <------->                  = 13 - (month.length / 2) + 1
      //                      <------> = 26 - above
      //   +--------------------------+
      //   |         July 2009        |
      //   |     Mo Tu We Th Fr Sa Su |
      //   |  27        1  2  3  4  5 |
      //   |  28  6  7  8  9 10 11 12 |
      //   |  29 13 14 15 16 17 18 19 |
      //   |  30 20 21 22 23 24 25 26 |
      //   |  31 27 28 29 30 31       |
      //   +--------------------------+

      int totalWidth = 26;
      int labelWidth = month.length () + 5;  // 5 = " 2009"
      int leftGap = (totalWidth / 2) - (labelWidth / 2);
      int rightGap = totalWidth - leftGap - labelWidth;

      out << std::setw (leftGap) << ' '
          << month
          << ' '
          << nextY
          << std::setw (rightGap) << ' ';

      if (++nextM > 12)
      {
        nextM = 1;
        nextY++;
      }
    }

    out << std::endl
        << optionalBlankLine ()
        << renderMonths (mFrom, yFrom, today, tasks, monthsPerLine)
        << std::endl;

    mFrom += monthsPerLine;
    if (mFrom > 12)
    {
      mFrom -= 12;
      ++yFrom;
    }
  }

  if (context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false))
    out << "Legend: "
        << Text::colorize (Text::cyan, Text::nocolor, "today")
        << ", "
        << Text::colorize (Text::black, Text::on_yellow, "due")
        << ", "
        << Text::colorize (Text::black, Text::on_red, "overdue")
        << "."
        << optionalBlankLine ()
        << std::endl;

  outs = out.str ();
  return 0;
}

////////////////////////////////////////////////////////////////////////////////
int handleReportStats (std::string &outs)
{
  std::stringstream out;

  // Go get the file sizes.
  size_t dataSize = 0;

  struct stat s;
  std::string location = expandPath (context.config.get ("data.location"));
  std::string file = location + "/pending.data";
  if (!stat (file.c_str (), &s))
    dataSize += s.st_size;

  file = location + "/completed.data";
  if (!stat (file.c_str (), &s))
    dataSize += s.st_size;

  file = location + "/undo.data";
  if (!stat (file.c_str (), &s))
    dataSize += s.st_size;

  std::vector <std::string> undo;
  slurp (file, undo, false);
  int undoCount = 0;
  foreach (tx, undo)
    if (tx->substr (0, 3) == "---")
      ++undoCount;

  // Get all the tasks.
  std::vector <Task> tasks;
  context.tdb.lock (context.config.get ("locking", true));
  handleRecurrence ();
  context.tdb.load (tasks, context.filter);
  context.tdb.commit ();
  context.tdb.unlock ();

  Date now;
  time_t earliest   = time (NULL);
  time_t latest     = 1;
  int totalT        = 0;
  int deletedT      = 0;
  int pendingT      = 0;
  int completedT    = 0;
  int waitingT      = 0;
  int taggedT       = 0;
  int annotationsT  = 0;
  int recurringT    = 0;
  float daysPending = 0.0;
  int descLength    = 0;
  std::map <std::string, int> allTags;
  std::map <std::string, int> allProjects;

  std::vector <Task>::iterator it;
  for (it = tasks.begin (); it != tasks.end (); ++it)
  {
    ++totalT;
    if (it->getStatus () == Task::deleted)   ++deletedT;
    if (it->getStatus () == Task::pending)   ++pendingT;
    if (it->getStatus () == Task::completed) ++completedT;
    if (it->getStatus () == Task::recurring) ++recurringT;
    if (it->getStatus () == Task::waiting)   ++waitingT;

    time_t entry = ::atoi (it->get ("entry").c_str ());
    if (entry < earliest) earliest = entry;
    if (entry > latest)   latest   = entry;

    if (it->getStatus () == Task::completed)
    {
      time_t end = ::atoi (it->get ("end").c_str ());
      daysPending += (end - entry) / 86400.0;
    }

    if (it->getStatus () == Task::pending)
      daysPending += (now - entry) / 86400.0;

    descLength += it->get ("description").length ();

    std::vector <Att> annotations;
    it->getAnnotations (annotations);
    annotationsT += annotations.size ();

    std::vector <std::string> tags;
    it->getTags (tags);
    if (tags.size ()) ++taggedT;

    foreach (t, tags)
      allTags[*t] = 0;

    std::string project = it->get ("project");
    if (project != "")
      allProjects[project] = 0;
  }

  // Create a table for output.
  Table table;
  table.setTableWidth (context.getWidth ());
  table.setTableIntraPadding (2);
  table.setDateFormat (context.config.get ("dateformat", "m/d/Y"));
  table.addColumn ("Category");
  table.addColumn ("Data");

  if ((context.config.get ("color", true) || context.config.get (std::string ("_forcecolor"), false)) &&
      context.config.get (std::string ("fontunderline"), "true"))
  {
    table.setColumnUnderline (0);
    table.setColumnUnderline (1);
  }
  else
    table.setTableDashedUnderline ();

  table.setColumnWidth (0, Table::minimum);
  table.setColumnWidth (1, Table::flexible);

  table.setColumnJustification (0, Table::left);
  table.setColumnJustification (1, Table::left);

  int row = table.addRow ();
  table.addCell (row, 0, "Pending");
  table.addCell (row, 1, pendingT);

  row = table.addRow ();
  table.addCell (row, 0, "Waiting");
  table.addCell (row, 1, waitingT);

  row = table.addRow ();
  table.addCell (row, 0, "Recurring");
  table.addCell (row, 1, recurringT);

  row = table.addRow ();
  table.addCell (row, 0, "Completed");
  table.addCell (row, 1, completedT);

  row = table.addRow ();
  table.addCell (row, 0, "Deleted");
  table.addCell (row, 1, deletedT);

  row = table.addRow ();
  table.addCell (row, 0, "Total");
  table.addCell (row, 1, totalT);

  row = table.addRow ();
  table.addCell (row, 0, "Annotations");
  table.addCell (row, 1, annotationsT);

  row = table.addRow ();
  table.addCell (row, 0, "Unique tags");
  table.addCell (row, 1, (int)allTags.size ());

  row = table.addRow ();
  table.addCell (row, 0, "Projects");
  table.addCell (row, 1, (int)allProjects.size ());

  row = table.addRow ();
  table.addCell (row, 0, "Data size");
  table.addCell (row, 1, formatBytes (dataSize));

  row = table.addRow ();
  table.addCell (row, 0, "Undo transactions");
  table.addCell (row, 1, undoCount);

  if (totalT)
  {
    row = table.addRow ();
    table.addCell (row, 0, "Tasks tagged");

    std::stringstream value;
    value << std::setprecision (3) << (100.0 * taggedT / totalT) << "%";
    table.addCell (row, 1, value.str ());
  }

  if (tasks.size ())
  {
    Date e (earliest);
    row = table.addRow ();
    table.addCell (row, 0, "Oldest task");
    table.addCell (row, 1, e.toString (context.config.get ("dateformat", "m/d/Y")));

    Date l (latest);
    row = table.addRow ();
    table.addCell (row, 0, "Newest task");
    table.addCell (row, 1, l.toString (context.config.get ("dateformat", "m/d/Y")));

    row = table.addRow ();
    table.addCell (row, 0, "Task used for");
    table.addCell (row, 1, formatSeconds (latest - earliest));
  }

  if (totalT)
  {
    row = table.addRow ();
    table.addCell (row, 0, "Task added every");
    table.addCell (row, 1, formatSeconds ((latest - earliest) / totalT));
  }

  if (completedT)
  {
    row = table.addRow ();
    table.addCell (row, 0, "Task completed every");
    table.addCell (row, 1, formatSeconds ((latest - earliest) / completedT));
  }

  if (deletedT)
  {
    row = table.addRow ();
    table.addCell (row, 0, "Task deleted every");
    table.addCell (row, 1, formatSeconds ((latest - earliest) / deletedT));
  }

  if (pendingT || completedT)
  {
    row = table.addRow ();
    table.addCell (row, 0, "Average time pending");
    table.addCell (row, 1, formatSeconds ((int) ((daysPending / (pendingT + completedT)) * 86400)));
  }

  if (totalT)
  {
    row = table.addRow ();
    table.addCell (row, 0, "Average desc length");
    std::stringstream value;
    value << (int) (descLength / totalT) << " characters";
    table.addCell (row, 1, value.str ());
  }

  out << optionalBlankLine ()
      << table.render ()
      << optionalBlankLine ();

  outs = out.str ();
  return 0;
}

////////////////////////////////////////////////////////////////////////////////
void gatherNextTasks (std::vector <Task>& tasks)
{
  // For counting tasks by project.
  std::map <std::string, int> countByProject;
  std::map <int, bool> matching;
  std::vector <Task> filtered;
  Date now;

  // How many items per project?  Default 2.
  int limit = context.config.get ("next", 2);

  // due:< 1wk, pri:*
  foreach (task, tasks)
  {
    if (task->has ("due"))
    {
      Date d (::atoi (task->get ("due").c_str ()));
      if (d < now + (7 * 24 * 60 * 60)) // if due:< 1wk
      {
        std::string project = task->get ("project");
        if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
        {
          ++countByProject[project];
          matching[task->id] = true;
          filtered.push_back (*task);
        }
      }
    }
  }

  // due:*, pri:H
  foreach (task, tasks)
  {
    if (task->has ("due"))
    {
      std::string priority = task->get ("priority");
      if (priority == "H")
      {
        std::string project = task->get ("project");
        if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
        {
          ++countByProject[project];
          matching[task->id] = true;
          filtered.push_back (*task);
        }
      }
    }
  }

  // pri:H
  foreach (task, tasks)
  {
    std::string priority = task->get ("priority");
    if (priority == "H")
    {
      std::string project = task->get ("project");
      if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
      {
        ++countByProject[project];
        matching[task->id] = true;
        filtered.push_back (*task);
      }
    }
  }

  // due:*, pri:M
  foreach (task, tasks)
  {
    if (task->has ("due"))
    {
      std::string priority = task->get ("priority");
      if (priority == "M")
      {
        std::string project = task->get ("project");
        if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
        {
          ++countByProject[project];
          matching[task->id] = true;
          filtered.push_back (*task);
        }
      }
    }
  }

  // pri:M
  foreach (task, tasks)
  {
    std::string priority = task->get ("priority");
    if (priority == "M")
    {
      std::string project = task->get ("project");
      if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
      {
        ++countByProject[project];
        matching[task->id] = true;
        filtered.push_back (*task);
      }
    }
  }

  // due:*, pri:L
  foreach (task, tasks)
  {
    if (task->has ("due"))
    {
      std::string priority = task->get ("priority");
      if (priority == "L")
      {
        std::string project = task->get ("project");
        if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
        {
          ++countByProject[project];
          matching[task->id] = true;
          filtered.push_back (*task);
        }
      }
    }
  }

  // pri:L
  foreach (task, tasks)
  {
    std::string priority = task->get ("priority");
    if (priority == "L")
    {
      std::string project = task->get ("project");
      if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
      {
        ++countByProject[project];
        matching[task->id] = true;
        filtered.push_back (*task);
      }
    }
  }

  // due:, pri:
  foreach (task, tasks)
  {
    if (task->has ("due"))
    {
      std::string priority = task->get ("priority");
      if (priority == "")
      {
        std::string project = task->get ("project");
        if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
        {
          ++countByProject[project];
          matching[task->id] = true;
          filtered.push_back (*task);
        }
      }
    }
  }

  // Filler.
  foreach (task, tasks)
  {
    std::string project = task->get ("project");
    if (countByProject[project] < limit && matching.find (task->id) == matching.end ())
    {
      ++countByProject[project];
      matching[task->id] = true;
      filtered.push_back (*task);
    }
  }

  tasks = filtered;
}

///////////////////////////////////////////////////////////////////////////////
std::string getFullDescription (Task& task)
{
  std::string desc = task.get ("description");

  std::vector <Att> annotations;
  task.getAnnotations (annotations);
  foreach (anno, annotations)
  {
    Date dt (::atoi (anno->name ().substr (11, std::string::npos).c_str ()));
    std::string when = dt.toString (context.config.get ("dateformat", "m/d/Y"));
    desc += "\n" + when + " " + anno->value ();
  }

  return desc;
}

///////////////////////////////////////////////////////////////////////////////
std::string getDueDate (Task& task)
{
  std::string due = task.get ("due");
  if (due.length ())
  {
    Date d (::atoi (due.c_str ()));
    due = d.toString (context.config.get ("dateformat", "m/d/Y"));
  }

  return due;
}

///////////////////////////////////////////////////////////////////////////////

Generated by  Doxygen 1.6.0   Back to index