diff --git a/src/import.cpp b/src/import.cpp index 94879404e..ce36f8480 100644 --- a/src/import.cpp +++ b/src/import.cpp @@ -26,6 +26,8 @@ //////////////////////////////////////////////////////////////////////////////// #include #include +#include +#include #include "Date.h" #include "task.h" @@ -249,7 +251,7 @@ static std::string importTask_1_4_3 ( std::string tokens = fields[f].substr (1, fields[f].length () - 2); std::vector tags; split (tags, tokens, ' '); - for (unsigned int i = 0; i > tags.size (); ++i) + for (unsigned int i = 0; i < tags.size (); ++i) task.addTag (tags[i]); } break; @@ -405,7 +407,7 @@ static std::string importTask_1_5_0 ( std::string tokens = fields[f].substr (1, fields[f].length () - 2); std::vector tags; split (tags, tokens, ' '); - for (unsigned int i = 0; i > tags.size (); ++i) + for (unsigned int i = 0; i < tags.size (); ++i) task.addTag (tags[i]); } break; @@ -566,7 +568,7 @@ static std::string importTask_1_6_0 ( std::string tokens = fields[f].substr (1, fields[f].length () - 2); std::vector tags; split (tags, tokens, ' '); - for (unsigned int i = 0; i > tags.size (); ++i) + for (unsigned int i = 0; i < tags.size (); ++i) task.addTag (tags[i]); } break; @@ -888,9 +890,227 @@ static std::string importCSV ( Config& conf, const std::vector & lines) { - // TODO Allow any number of fields, but attempt to map into task fields. - // TODO Must have header line to name fields. - return "CSV\n"; + std::vector failed; + + // Set up mappings. Assume no fields match. + std::map mapping; + mapping ["id"] = -1; + mapping ["uuid"] = -1; + mapping ["status"] = -1; + mapping ["tags"] = -1; + mapping ["entry"] = -1; + mapping ["start"] = -1; + mapping ["due"] = -1; + mapping ["recur"] = -1; + mapping ["end"] = -1; + mapping ["project"] = -1; + mapping ["priority"] = -1; + mapping ["fg"] = -1; + mapping ["bg"] = -1; + mapping ["description"] = -1; + + std::vector headings; + split (headings, lines[0], ','); + + for (unsigned int h = 0; h < headings.size (); ++h) + { + std::string name = lowerCase (trim (unquoteText (trim (headings[h])))); + + // If there is a mapping for the field, use the value. + if (name == "id" || + name == "#" || + name == "sequence" || + name.find ("num") != std::string::npos) + { + mapping["id"] = (int)h; + } + + else if (name == "uuid" || + name == "guid" || + name.find ("unique") != std::string::npos) + { + mapping["uuid"] = (int)h; + } + + else if (name == "status" || + name == "condition" || + name == "state") + { + mapping["status"] = (int)h; + } + + else if (name == "tags" || + name.find ("categor") != std::string::npos || + name.find ("tag") != std::string::npos) + { + mapping["tags"] = (int)h; + } + + else if (name == "entry" || + name.find ("added") != std::string::npos || + name.find ("created") != std::string::npos || + name.find ("entered") != std::string::npos) + { + mapping["entry"] = (int)h; + } + + else if (name == "start" || + name.find ("began") != std::string::npos || + name.find ("begun") != std::string::npos || + name.find ("started") != std::string::npos || + name == "") + { + mapping["start"] = (int)h; + } + + else if (name == "due" || + name.find ("expected") != std::string::npos) + { + mapping["due"] = (int)h; + } + + else if (name == "recur" || + name == "frequency") + { + mapping["recur"] = (int)h; + } + + else if (name == "end" || + name == "done" || + name.find ("complete") != std::string::npos) + { + mapping["end"] = (int)h; + } + + else if (name == "project" || + name.find ("proj") != std::string::npos) + { + mapping["project"] = (int)h; + } + + else if (name == "priority" || + name == "pri" || + name.find ("importan") != std::string::npos) + { + mapping["priority"] = (int)h; + } + + else if (name.find ("fg") != std::string::npos || + name.find ("foreground") != std::string::npos || + name.find ("color") != std::string::npos) + { + mapping["fg"] = (int)h; + } + + else if (name == "bg" || + name.find ("background") != std::string::npos) + { + mapping["bg"] = (int)h; + } + + else if (name.find ("desc") != std::string::npos || + name.find ("detail") != std::string::npos || + name.find ("what") != std::string::npos) + { + mapping["description"] = (int)h; + } + } + + // TODO Dump mappings and ask for confirmation? + + std::vector ::const_iterator it = lines.begin (); + for (++it; it != lines.end (); ++it) + { + try + { + std::vector fields; + split (fields, *it, ','); + + T task; + + int f; + if ((f = mapping["uuid"]) != -1) + task.setUUID (lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["status"]) != -1) + { + std::string value = lowerCase (unquoteText (trim (fields[f]))); + + if (value == "recurring") task.setStatus (T::recurring); + else if (value == "deleted") task.setStatus (T::deleted); + else if (value == "completed") task.setStatus (T::completed); + else task.setStatus (T::pending); + } + + if ((f = mapping["tags"]) != -1) + { + std::string value = unquoteText (trim (fields[f])); + std::vector tags; + split (tags, value, ' '); + for (unsigned int i = 0; i < tags.size (); ++i) + task.addTag (tags[i]); + } + + if ((f = mapping["entry"]) != -1) + task.setAttribute ("entry", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["start"]) != -1) + task.setAttribute ("start", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["due"]) != -1) + task.setAttribute ("due", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["recur"]) != -1) + task.setAttribute ("recur", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["end"]) != -1) + task.setAttribute ("end", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["project"]) != -1) + task.setAttribute ("project", unquoteText (trim (fields[f]))); + + if ((f = mapping["priority"]) != -1) + { + std::string value = upperCase (unquoteText (trim (fields[f]))); + if (value == "H" || value == "M" || value == "L") + task.setAttribute ("priority", value); + } + + if ((f = mapping["fg"]) != -1) + task.setAttribute ("fg", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["bg"]) != -1) + task.setAttribute ("bg", lowerCase (unquoteText (trim (fields[f])))); + + if ((f = mapping["description"]) != -1) + task.setDescription (unquoteText (trim (fields[f]))); + + if (! tdb.addT (task)) + failed.push_back (*it); + } + + catch (...) + { + failed.push_back (*it); + } + } + + std::stringstream out; + out << "Imported " + << (lines.size () - failed.size () - 1) + << " tasks successfully, with " + << failed.size () + << " errors." + << std::endl; + + if (failed.size ()) + { + std::string bad; + join (bad, "\n", failed); + return out.str () + "\nCould not import:\n\n" + bad; + } + + return out.str (); } //////////////////////////////////////////////////////////////////////////////// @@ -926,8 +1146,25 @@ std::string handleImport (TDB& tdb, T& task, Config& conf) // Take a guess at the file type. fileType type = determineFileType (lines); + std::string identifier; + switch (type) + { + case task_1_4_3: identifier = "This looks like an older task export file."; break; + case task_1_5_0: identifier = "This looks like a recent task export file."; break; + case task_1_6_0: identifier = "This looks like a current task export file."; break; + case task_cmd_line: identifier = "This looks like task command line arguments."; break; + case todo_sh_2_0: identifier = "This looks like a todo.sh 2.x file."; break; + case csv: identifier = "This looks like a CSV file, but not a task export file."; break; + case text: identifier = "This looks like a text file with one tasks per line."; break; + case not_a_clue: + throw std::string ("Task cannot determine which type of file this is, " + "and cannot proceed."); + } - // TODO Allow an override. + // For tty users, confirm the import, as it is destructive. + if (isatty (fileno (stdout))) + if (! confirm (identifier + " Okay to proceed?")) + throw std::string ("Task will not import any data."); // Determine which type it might be, then attempt an import. switch (type) @@ -939,10 +1176,7 @@ std::string handleImport (TDB& tdb, T& task, Config& conf) case todo_sh_2_0: out << importTodoSh_2_0 (tdb, conf, lines); break; case csv: out << importCSV (tdb, conf, lines); break; case text: out << importText (tdb, conf, lines); break; - - case not_a_clue: - out << "?"; - break; + case not_a_clue: /* to stop the compiler from complaining. */ break; } } else diff --git a/src/task.h b/src/task.h index 2a0b8f59f..ab4faa514 100644 --- a/src/task.h +++ b/src/task.h @@ -108,12 +108,12 @@ std::string handleCustomReport (TDB&, T&, Config&, const std::string&); void validReportColumns (const std::vector &); void validSortColumns (const std::vector &, const std::vector &); -// util.cpp -bool confirm (const std::string&); +// text.cpp void wrapText (std::vector &, const std::string&, const int); std::string trimLeft (const std::string& in, const std::string& t = " "); std::string trimRight (const std::string& in, const std::string& t = " "); std::string trim (const std::string& in, const std::string& t = " "); +std::string unquoteText (const std::string&); void extractLine (std::string&, std::string&, int); void split (std::vector&, const std::string&, const char); void split (std::vector&, const std::string&, const std::string&); @@ -121,12 +121,15 @@ void join (std::string&, const std::string&, const std::vector&); std::string commify (const std::string&); std::string lowerCase (const std::string&); std::string upperCase (const std::string&); +const char* optionalBlankLine (Config&); + +// util.cpp +bool confirm (const std::string&); void delay (float); -int autoComplete (const std::string&, const std::vector&, std::vector&); void formatTimeDeltaDays (std::string&, time_t); std::string formatSeconds (time_t); +int autoComplete (const std::string&, const std::vector&, std::vector&); const std::string uuid (); -const char* optionalBlankLine (Config&); int convertDuration (const std::string&); std::string expandPath (const std::string&); diff --git a/src/tests/import.csv.t b/src/tests/import.csv.t new file mode 100755 index 000000000..2d1eb718e --- /dev/null +++ b/src/tests/import.csv.t @@ -0,0 +1,70 @@ +#! /usr/bin/perl +################################################################################ +## 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 +## +################################################################################ + +use strict; +use warnings; +use Test::More tests => 8; + +# Create the rc file. +if (open my $fh, '>', 'import.rc') +{ + print $fh "data.location=.\n"; + close $fh; + ok (-r 'import.rc', 'Created import.rc'); +} + +# Create import file. +if (open my $fh, '>', 'import.txt') +{ + print $fh "'id','priority','description'\n", + "1,H,'this is a test'\n", + "2,,'another task'\n", + "\n"; + close $fh; + ok (-r 'import.txt', 'Created sample import data'); +} + +my $output = qx{../task rc:import.rc import import.txt}; +is ($output, "Imported 2 tasks successfully, with 0 errors.\n", 'no errors'); + +$output = qx{../task rc:import.rc list}; +like ($output, qr/1.+H.+this is a test/, 't1'); +like ($output, qr/2.+another task/, 't2'); + +# Cleanup. +unlink 'import.txt'; +ok (!-r 'import.txt', 'Removed import.txt'); + +unlink 'pending.data'; +ok (!-r 'pending.data', 'Removed pending.data'); + +unlink 'import.rc'; +ok (!-r 'import.rc', 'Removed import.rc'); + +exit 0; + diff --git a/src/tests/import.todo.t b/src/tests/import.todo.t index e8bb578f9..dac3a6cbe 100755 --- a/src/tests/import.todo.t +++ b/src/tests/import.todo.t @@ -41,7 +41,7 @@ if (open my $fh, '>', 'import.rc') # Create import file. if (open my $fh, '>', 'import.txt') { - print $fh "x 2010-03-25 Walk the dog +project \@context\n", + print $fh "x 2009-03-25 Walk the dog +project \@context\n", "This is a test +project \@context\n", "(A) A prioritized task\n", "\n"; diff --git a/src/tests/text.t.cpp b/src/tests/text.t.cpp index 65c3d5e11..d959ea1d4 100644 --- a/src/tests/text.t.cpp +++ b/src/tests/text.t.cpp @@ -31,7 +31,7 @@ //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (78); + UnitTest t (94); // void wrapText (std::vector & lines, const std::string& text, const int width) std::string text = "This is a test of the line wrapping code."; @@ -183,6 +183,24 @@ int main (int argc, char** argv) t.is (trim (" \t xxx \t "), "\t xxx \t", "trim ' \\t xxx \\t ' -> '\\t xxx \\t'"); t.is (trim (" \t xxx \t ", " \t"), "xxx", "trim ' \\t xxx \\t ' -> 'xxx'"); + // std::string unquoteText (const std::string& text) + t.is (unquoteText (""), "", "unquoteText '' -> ''"); + t.is (unquoteText ("x"), "x", "unquoteText 'x' -> 'x'"); + t.is (unquoteText ("'x"), "'x", "unquoteText ''x' -> ''x'"); + t.is (unquoteText ("x'"), "x'", "unquoteText 'x'' -> 'x''"); + t.is (unquoteText ("\"x"), "\"x", "unquoteText '\"x' -> '\"x'"); + t.is (unquoteText ("x\""), "x\"", "unquoteText 'x\"' -> 'x\"'"); + t.is (unquoteText ("''"), "", "unquoteText '''' -> ''"); + t.is (unquoteText ("'''"), "'", "unquoteText ''''' -> '''"); + t.is (unquoteText ("\"\""), "", "unquoteText '\"\"' -> ''"); + t.is (unquoteText ("\"\"\""), "\"", "unquoteText '\"\"\"' -> '\"'"); + t.is (unquoteText ("''''"), "''", "unquoteText '''''' -> ''''"); + t.is (unquoteText ("\"\"\"\""), "\"\"", "unquoteText '\"\"\"\"' -> '\"\"'"); + t.is (unquoteText ("'\"\"'"), "\"\"", "unquoteText '''\"\"' -> '\"\"'"); + t.is (unquoteText ("\"''\""), "''", "unquoteText '\"''\"' -> ''''"); + t.is (unquoteText ("'x'"), "x", "unquoteText ''x'' -> 'x'"); + t.is (unquoteText ("\"x\""), "x", "unquoteText '\"x\"' -> 'x'"); + // std::string commify (const std::string& data) t.is (commify (""), "", "commify '' -> ''"); t.is (commify ("1"), "1", "commify '1' -> '1'"); diff --git a/src/text.cpp b/src/text.cpp index fced0b0aa..69689a5ac 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -127,12 +127,19 @@ std::string trim (const std::string& in, const std::string& t /*= " "*/) //////////////////////////////////////////////////////////////////////////////// // Remove enclosing balanced quotes. Assumes trimmed text. -void unquoteText (std::string& text) +std::string unquoteText (const std::string& input) { - char quote = text[0]; - if (quote == '\'' || quote == '"') - if (text[text.length () - 1] == quote) - text = text.substr (1, text.length () - 3); + std::string output = input; + + if (output.length () > 1) + { + char quote = output[0]; + if ((quote == '\'' || quote == '"') && + output[output.length () - 1] == quote) + return output.substr (1, output.length () - 2); + } + + return output; } ////////////////////////////////////////////////////////////////////////////////