diff --git a/ChangeLog b/ChangeLog index deae5c454..3ba115dda 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,9 @@ 1.9.0 () + Added feature #292 that permits alternate line coloration in reports (thanks to Richard Querin). + + Added feature #254 (#295) which gives task a second date format to be + used in the reports with more conversion sequences like weekday name + or weeknumber. The date format is set with variable "reportdateformat". + Added feature #307 that provides vim with syntax highlighting for .taskrc. + Added feature #336 which gives task a 'prepend' command for symmetry with the 'append' command. diff --git a/NEWS b/NEWS index d9c6f8212..25e266810 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,7 @@ New Features in task 1.9 - Supports nested .taskrc files with the new "include" statement" - New columns that include the time as well as date - New attribute modifiers + - New date format for reports - Improved .taskrc validation - Improved calendar report with custom coloring and optional task details output diff --git a/doc/man/taskrc.5 b/doc/man/taskrc.5 index b7a1db9cf..4e5f1bc59 100644 --- a/doc/man/taskrc.5 +++ b/doc/man/taskrc.5 @@ -148,33 +148,59 @@ tag names you have used, or just the ones used in active tasks. .TP .B dateformat=m/d/Y -This is a string of characters that define how task formats dates. The default value is: m/d/Y. -The string should contain the characters +.TP +.B reportdateformat=m/d/Y +This is a string of characters that define how task formats dates. If +.B reportdateformat +is set it will be used for the due date in the output of reports and "task info". + +The default value is: m/d/Y. The string should contain the characters .RS -m minimal-digit month, for example 1 or 12 +m minimal-digit month, for example 1 or 12 .br -d minimal-digit day, for example 1 or 30 +d minimal-digit day, for example 1 or 30 .br -y two-digit year, for example 09 +y two-digit year, for example 09 .br -D two-digit day, for example 01 or 30 +D two-digit day, for example 01 or 30 .br -M two-digit month, for example 01 or 12 +M two-digit month, for example 01 or 12 .br -Y four-digit year, for example 2009 +Y four-digit year, for example 2009 +.br +a short name of weekday, for example Mon or Wed +.br +A long name of weekday, for example Monday or Wednesday +.br +b short name of month, for example Jan or Aug +.br +B long name of month, for example January or August +.br +V weeknumber, for example 03 or 37 .RE The string may also contain other characters to act as spacers, or formatting. Examples for other -variable values: +values of dateformat: .RS .br -d/m/Y would output 24/7/2009 +d/m/Y would use for input and output 24/7/2009 .br -YMD would output 20090724 +yMD would use for input and output 090724 .br -m-d-y would output 07-24-09 +M-D-Y would use for input and output 07-24-2009 +.RE + +Examples for other values of reportdateformat: + +.RS +.br +a D b Y (V) would do an output as "Fri 24 Jul 2009 (30)" +.br +A, B D, Y would do an output as "Friday, July 24, 2009" +.br +vV a Y-M-D would do an output as "v30 Fri 2009-07.24" .RE .TP diff --git a/src/Config.cpp b/src/Config.cpp index 1366ce831..c5d97c179 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -149,6 +149,7 @@ void Config::createDefaultRC (const std::string& rc, const std::string& data) << "\n" << "# Dates\n" << "dateformat=m/d/Y # Preferred input and display date format\n" + << "#reportdateformat=m/d/Y # Preferred input and display date format\n" << "weekstart=Sunday # Sunday or Monday only\n" << "displayweeknumber=yes # Show week numbers on calendar\n" << "due=7 # Task is considered due in 7 days\n" diff --git a/src/Date.cpp b/src/Date.cpp index 7971f075e..65bb47dbb 100644 --- a/src/Date.cpp +++ b/src/Date.cpp @@ -33,6 +33,9 @@ #include "Date.h" #include "text.h" #include "util.h" +#include "Context.h" + +extern Context context; //////////////////////////////////////////////////////////////////////////////// // Defaults to "now". @@ -85,7 +88,7 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) if (i >= mdy.length () || ! isdigit (mdy[i])) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (m)."; } if (i + 1 < mdy.length () && @@ -106,7 +109,7 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) if (i >= mdy.length () || ! isdigit (mdy[i])) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (d)."; } if (i + 1 < mdy.length () && @@ -125,11 +128,11 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) // Double digit. case 'y': - if (i + 1 >= mdy.length () || + if (i + 1 >= mdy.length () || ! isdigit (mdy[i + 0]) || ! isdigit (mdy[i + 1])) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (y)."; } year = atoi (mdy.substr (i, 2).c_str ()) + 2000; @@ -141,7 +144,7 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) ! isdigit (mdy[i + 0]) || ! isdigit (mdy[i + 1])) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (M)."; } month = atoi (mdy.substr (i, 2).c_str ()); @@ -153,13 +156,24 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) ! isdigit (mdy[i + 0]) || ! isdigit (mdy[i + 1])) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (D)."; } day = atoi (mdy.substr (i, 2).c_str ()); i += 2; break; + case 'V': + if (i + 1 >= mdy.length () || + ! isdigit (mdy[i + 0]) || + ! isdigit (mdy[i + 1])) + { + throw std::string ("\"") + mdy + "\" is not a valid date (V)."; + } + + i += 2; + break; + // Quadruple digit. case 'Y': if (i + 3 >= mdy.length () || @@ -168,18 +182,70 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) ! isdigit (mdy[i + 2]) || ! isdigit (mdy[i + 3])) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (Y)."; } year = atoi (mdy.substr (i, 4).c_str ()); i += 4; break; + // Short names with 3 characters + case 'a': + if (i + 2 >= mdy.length () || + isdigit (mdy[i + 0]) || + isdigit (mdy[i + 1]) || + isdigit (mdy[i + 2])) + { + throw std::string ("\"") + mdy + "\" is not a valid date (a)."; + } + + i += 3; + break; + + case 'b': + if (i + 2 >= mdy.length () || + isdigit (mdy[i + 0]) || + isdigit (mdy[i + 1]) || + isdigit (mdy[i + 2])) + { + throw std::string ("\"") + mdy + "\" is not a valid date (b)."; + } + + month = Date::monthOfYear (mdy.substr (i, 3).c_str()); + i += 3; + break; + + // Long names + case 'A': + if (i + 2 >= mdy.length () || + isdigit (mdy[i + 0]) || + isdigit (mdy[i + 1]) || + isdigit (mdy[i + 2])) + { + throw std::string ("\"") + mdy + "\" is not a valid date (A)."; + } + + i += Date::dayName( Date::dayOfWeek (mdy.substr (i, 3).c_str()) ).size(); + break; + + case 'B': + if (i + 2 >= mdy.length () || + isdigit (mdy[i + 0]) || + isdigit (mdy[i + 1]) || + isdigit (mdy[i + 2])) + { + throw std::string ("\"") + mdy + "\" is not a valid date (B)."; + } + + month = Date::monthOfYear (mdy.substr (i, 3).c_str()); + i += Date::monthName(month).size(); + break; + default: if (i >= mdy.length () || mdy[i] != format[f]) { - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (DEFAULT)."; } ++i; break; @@ -190,7 +256,7 @@ Date::Date (const std::string& mdy, const std::string& format /* = "m/d/Y" */) throw std::string ("\"") + mdy + "\" is not a valid date in " + format + " format."; if (!valid (month, day, year)) - throw std::string ("\"") + mdy + "\" is not a valid date."; + throw std::string ("\"") + mdy + "\" is not a valid date (VALID)."; // Duplicate Date::Date (const int, const int, const int); struct tm t = {0}; @@ -256,13 +322,18 @@ const std::string Date::toString (const std::string& format /*= "m/d/Y" */) cons char c = localFormat[i]; switch (c) { - case 'm': sprintf (buffer, "%d", this->month ()); break; - case 'M': sprintf (buffer, "%02d", this->month ()); break; - case 'd': sprintf (buffer, "%d", this->day ()); break; - case 'D': sprintf (buffer, "%02d", this->day ()); break; - case 'y': sprintf (buffer, "%02d", this->year () % 100); break; - case 'Y': sprintf (buffer, "%d", this->year ()); break; - default: sprintf (buffer, "%c", c); break; + case 'm': sprintf (buffer, "%d", this->month ()); break; + case 'M': sprintf (buffer, "%02d", this->month ()); break; + case 'd': sprintf (buffer, "%d", this->day ()); break; + case 'D': sprintf (buffer, "%02d", this->day ()); break; + case 'y': sprintf (buffer, "%02d", this->year () % 100); break; + case 'Y': sprintf (buffer, "%d", this->year ()); break; + case 'a': sprintf (buffer, "%.3s", Date::dayName(dayOfWeek()).c_str() ); break; + case 'A': sprintf (buffer, "%s", Date::dayName(dayOfWeek()).c_str() ); break; + case 'b': sprintf (buffer, "%.3s", Date::monthName(month()).c_str() ); break; + case 'B': sprintf (buffer, "%.9s", Date::monthName(month()).c_str() ); break; + case 'V': sprintf (buffer, "%02d", Date::weekOfYear(Date::dayOfWeek(context.config.get ("weekstart", "Sunday")))); break; + default: sprintf (buffer, "%c", c); break; } formatted += buffer; @@ -437,13 +508,34 @@ int Date::dayOfWeek (const std::string& input) { std::string in = lowerCase (input); - if (in == "sunday") return 0; - if (in == "monday") return 1; - if (in == "tuesday") return 2; - if (in == "wednesday") return 3; - if (in == "thursday") return 4; - if (in == "friday") return 5; - if (in == "saturday") return 6; + if (in == "sunday" || in == "sun") return 0; + if (in == "monday" || in == "mon") return 1; + if (in == "tuesday" || in == "tue") return 2; + if (in == "wednesday" || in == "wed") return 3; + if (in == "thursday" || in == "thu") return 4; + if (in == "friday" || in == "fri") return 5; + if (in == "saturday" || in == "sat") return 6; + + return -1; +} + +//////////////////////////////////////////////////////////////////////////////// +int Date::monthOfYear (const std::string& input) +{ + std::string in = lowerCase (input); + + if (in == "january" || in == "jan") return 1; + if (in == "february" || in == "feb") return 2; + if (in == "march" || in == "mar") return 3; + if (in == "april" || in == "apr") return 4; + if (in == "may" || in == "may") return 5; + if (in == "june" || in == "jun") return 6; + if (in == "july" || in == "jul") return 7; + if (in == "august" || in == "aug") return 8; + if (in == "september" || in == "sep") return 9; + if (in == "october" || in == "oct") return 10; + if (in == "november" || in == "nov") return 11; + if (in == "december" || in == "dec") return 12; return -1; } diff --git a/src/Date.h b/src/Date.h index 68385515f..7f7f22914 100644 --- a/src/Date.h +++ b/src/Date.h @@ -58,6 +58,7 @@ public: static std::string dayName (int); static int weekOfYear (const std::string&); static int dayOfWeek (const std::string&); + static int monthOfYear (const std::string&); int month () const; int day () const; diff --git a/src/Table.cpp b/src/Table.cpp index 06061d114..fd81db490 100644 --- a/src/Table.cpp +++ b/src/Table.cpp @@ -52,6 +52,9 @@ #include "Timer.h" #include "text.h" #include "util.h" +#include "Context.h" + +extern Context context; //////////////////////////////////////////////////////////////////////////////// Table::Table () @@ -868,6 +871,46 @@ void Table::sort (std::vector & order) } break; + case ascendingDueDate: + { + if ((std::string)*left != "" && (std::string)*right == "") + break; + + else if ((std::string)*left == "" && (std::string)*right != "") + SWAP + + else + { + Date dl ((std::string)*left, context.config.get("reportdateformat", + context.config.get("dateformat","m/d/Y"))); + Date dr ((std::string)*right, context.config.get("reportdateformat", + context.config.get("dateformat","m/d/Y"))); + if (dl > dr) + SWAP + } + } + break; + + case descendingDueDate: + { + if ((std::string)*left != "" && (std::string)*right == "") + break; + + else if ((std::string)*left == "" && (std::string)*right != "") + SWAP + + else + { + Date dl ((std::string)*left, context.config.get("reportdateformat", + context.config.get("dateformat","m/d/Y"))); + Date dr ((std::string)*right, context.config.get("reportdateformat", + context.config.get("dateformat","m/d/Y"))); + if (dl < dr) + SWAP + } + } + break; + case ascendingPriority: if (((std::string)*left == "" && (std::string)*right != "") || ((std::string)*left == "M" && (std::string)*right == "L") || diff --git a/src/Table.h b/src/Table.h index a280854af..dfe18d04e 100644 --- a/src/Table.h +++ b/src/Table.h @@ -41,11 +41,13 @@ public: ascendingCharacter, ascendingPriority, ascendingDate, + ascendingDueDate, ascendingPeriod, descendingNumeric, descendingCharacter, descendingPriority, descendingDate, + descendingDueDate, descendingPeriod}; enum sizing {minimum = -1, flexible = 0}; diff --git a/src/command.cpp b/src/command.cpp index 3326f9d5a..2fb9a5822 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -555,9 +555,9 @@ int handleConfig (std::string &outs) "color.due color.overdue color.pri.H color.pri.L color.pri.M color.pri.none " "color.recurring color.tagged color.footnote color.header color.debug color.alternate " "color.calendar.today color.calendar.due color.calendar.overdue color.calendar.weekend " - "confirmation curses data.location dateformat debug default.command default.priority " - "default.project defaultwidth due locale displayweeknumber echo.command " - "locking monthsperline nag next project shadow.command shadow.file " + "confirmation curses data.location dateformat reportdateformat debug default.command " + "default.priority default.project defaultwidth due locale displayweeknumber " + "echo.command locking monthsperline nag next project shadow.command shadow.file " "shadow.notify weekstart editor import.synonym.id import.synonym.uuid " "complete.all.projects complete.all.tags " #ifdef FEATURE_SHELL diff --git a/src/custom.cpp b/src/custom.cpp index 89a0bc66b..a4da90208 100644 --- a/src/custom.cpp +++ b/src/custom.cpp @@ -356,12 +356,15 @@ int runCustomReport ( { table.addColumn (columnLabels[*col] != "" ? columnLabels[*col] : "Due"); table.setColumnWidth (columnCount, Table::minimum); - table.setColumnJustification (columnCount, Table::right); + table.setColumnJustification (columnCount, Table::left); int row = 0; std::string due; foreach (task, tasks) - table.addCell (row++, columnCount, getDueDate (*task)); + table.addCell (row++, columnCount, + getDueDate (*task, + context.config.get ("reportdateformat", + context.config.get ("dateformat", "m/d/Y")))); dueColumn = columnCount; } @@ -550,13 +553,19 @@ int runCustomReport ( Table::ascendingPriority : Table::descendingPriority)); - else if (column == "entry" || column == "start" || column == "due" || - column == "wait" || column == "until" || column == "end") + else if (column == "entry" || column == "start" || column == "wait" || + column == "until" || column == "end") table.sortOn (columnIndex[column], (direction == '+' ? Table::ascendingDate : Table::descendingDate)); + else if (column == "due") + table.sortOn (columnIndex[column], + (direction == '+' ? + Table::ascendingDueDate : + Table::descendingDueDate)); + else if (column == "recur") table.sortOn (columnIndex[column], (direction == '+' ? @@ -571,10 +580,10 @@ int runCustomReport ( } // Now auto colorize all rows. - Color color_due (context.config.get ("color.due", "yellow")); + std::string due; + Color color_due (context.config.get ("color.due", "green")); Color color_overdue (context.config.get ("color.overdue", "red")); - std::string due; bool imminent; bool overdue; for (unsigned int row = 0; row < tasks.size (); ++row) diff --git a/src/main.h b/src/main.h index 9e1f3f53a..a934d8c53 100644 --- a/src/main.h +++ b/src/main.h @@ -103,7 +103,7 @@ int handleReportCalendar (std::string &); int handleReportStats (std::string &); int handleReportTimesheet (std::string &); std::string getFullDescription (Task&); -std::string getDueDate (Task&); +std::string getDueDate (Task&, const std::string&); // custom.cpp int handleCustomReport (const std::string&, std::string &); diff --git a/src/report.cpp b/src/report.cpp index edad19730..06043674e 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -412,7 +412,7 @@ int handleInfo (std::string &outs) table.addCell (row, 0, "Due"); Date dt (atoi (task->get ("due").c_str ())); - std::string due = getDueDate (*task); + std::string due = getDueDate (*task, context.config.get("reportdateformat", context.config.get("dateformat","m/d/Y"))); table.addCell (row, 1, due); overdue = (dt < now) ? true : false; @@ -1218,7 +1218,7 @@ int handleReportTimesheet (std::string &outs) { int row = completed.addRow (); completed.addCell (row, 1, task->get ("project")); - completed.addCell (row, 2, getDueDate (*task)); + completed.addCell (row, 2, getDueDate (*task,context.config.get("dateformat","m/d/Y"))); completed.addCell (row, 3, getFullDescription (*task)); if (color) @@ -1274,7 +1274,7 @@ int handleReportTimesheet (std::string &outs) { int row = started.addRow (); started.addCell (row, 1, task->get ("project")); - started.addCell (row, 2, getDueDate (*task)); + started.addCell (row, 2, getDueDate (*task,context.config.get("dateformat","m/d/Y"))); started.addCell (row, 3, getFullDescription (*task)); if (color) @@ -2124,13 +2124,15 @@ std::string getFullDescription (Task& task) } /////////////////////////////////////////////////////////////////////////////// -std::string getDueDate (Task& task) +std::string getDueDate (Task& task, const std::string& format) { 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")); + due = d.toString (format); + //due = d.toString (context.config.get ("dateformat", "m/d/Y")); + //due = d.toString (context.config.get ("reportdateformat", context.config.get ("dateformat", "m/d/Y"))); } return due; diff --git a/src/tests/date.t.cpp b/src/tests/date.t.cpp index 5714c19cc..391dcb8c1 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 (102); + UnitTest t (111); try { @@ -180,6 +180,21 @@ int main (int argc, char** argv) t.is (fromString7.day (), 1, "ctor (std::string) -> d"); t.is (fromString7.year (), 2008, "ctor (std::string) -> y"); + Date fromString8 ("Tue 01 Jan 2008 (01)", "a D b Y (V)"); + t.is (fromString8.month (), 1, "ctor (std::string) -> m"); + t.is (fromString8.day (), 1, "ctor (std::string) -> d"); + t.is (fromString8.year (), 2008, "ctor (std::string) -> y"); + + Date fromString9 ("Tuesday, January 1, 2008", "A, B d, Y"); + t.is (fromString9.month (), 1, "ctor (std::string) -> m"); + t.is (fromString9.day (), 1, "ctor (std::string) -> d"); + t.is (fromString9.year (), 2008, "ctor (std::string) -> y"); + + Date fromString10 ("v01 Tue 2008-01-01", "vV a Y-M-D"); + t.is (fromString10.month (), 1, "ctor (std::string) -> m"); + t.is (fromString10.day (), 1, "ctor (std::string) -> d"); + t.is (fromString10.year (), 2008, "ctor (std::string) -> y"); + // Relative dates. Date r1 ("today"); t.ok (r1.sameDay (now), "today = now"); diff --git a/src/tests/dateformat.t b/src/tests/dateformat.t index 62df90a4b..e6d8d9066 100755 --- a/src/tests/dateformat.t +++ b/src/tests/dateformat.t @@ -28,7 +28,7 @@ use strict; use warnings; -use Test::More tests => 9; +use Test::More tests => 14; # Create the rc file. if (open my $fh, '>', 'date1.rc') @@ -47,6 +47,16 @@ if (open my $fh, '>', 'date2.rc') ok (-r 'date2.rc', 'Created date2.rc'); } +if (open my $fh, '>', 'date3.rc') +{ + print $fh "data.location=.\n", + "dateformat=m/d/y\n", + "weekstart=Monday\n", + "reportdateformat=A D B Y (vV)\n"; + close $fh; + ok (-r 'date3.rc', 'Created date3.rc'); +} + qx{../task rc:date1.rc add foo due:20091231}; my $output = qx{../task rc:date1.rc info 1}; like ($output, qr/\b20091231\b/, 'date format YMD parsed'); @@ -58,6 +68,15 @@ qx{../task rc:date2.rc add foo due:12/1/09}; $output = qx{../task rc:date2.rc info 1}; like ($output, qr/\b12\/1\/09\b/, 'date format m/d/y parsed'); +unlink 'pending.data'; +ok (!-r 'pending.data', 'Removed pending.data'); + +qx{../task rc:date3.rc add foo due:4/8/10}; +$output = qx{../task rc:date3.rc list}; +like ($output, qr/Thursday 08 April 2010 \(v14\)/, 'date format A D B Y (vV) parsed'); +$output = qx{../task rc:date3.rc rc.reportdateformat:"D b Y - a" list}; +like ($output, qr/08 Apr 2010 - Thu/, 'date format D b Y - a parsed'); + # Cleanup. unlink 'pending.data'; ok (!-r 'pending.data', 'Removed pending.data'); @@ -71,5 +90,8 @@ ok (!-r 'date1.rc', 'Removed date1.rc'); unlink 'date2.rc'; ok (!-r 'date2.rc', 'Removed date2.rc'); +unlink 'date3.rc'; +ok (!-r 'date3.rc', 'Removed date3.rc'); + exit 0;