File Import

- Implemented import from task 1.4.3, 1.5.0, 1.6.0, plain text,
  todo.sh and task command line files.
- Implemented unit tests to cover the above.
This commit is contained in:
Paul Beckingham
2009-03-29 15:34:35 -04:00
parent c1291dc587
commit 5f4563af2f
16 changed files with 1180 additions and 54 deletions

View File

@@ -26,6 +26,7 @@
////////////////////////////////////////////////////////////////////////////////
#include <iostream>
#include <sstream>
#include "Date.h"
#include "task.h"
////////////////////////////////////////////////////////////////////////////////
@@ -34,6 +35,7 @@ enum fileType
not_a_clue,
task_1_4_3,
task_1_5_0,
task_1_6_0,
task_cmd_line,
todo_sh_2_0,
csv,
@@ -55,6 +57,10 @@ static fileType determineFileType (const std::vector <std::string>& lines)
lines[1][38] == ',' &&
lines[1][39] == '\'')
{
if (lines[0] == "'uuid','status','tags','entry','start','due','recur',"
"'end','project','priority','fg','bg','description'")
return task_1_6_0;
if (lines[0] == "'id','uuid','status','tags','entry','start','due','recur',"
"'end','project','priority','fg','bg','description'")
return task_1_5_0;
@@ -64,7 +70,26 @@ static fileType determineFileType (const std::vector <std::string>& lines)
return task_1_4_3;
}
// TODO task_cmd_line
// A task command line might include a priority or project.
for (unsigned int i = 0; i < lines.size (); ++i)
{
std::vector <std::string> words;
split (words, lines[i], ' ');
for (unsigned int w = 0; w < words.size (); ++w)
if (words[w].substr (0, 9) == "priority:" ||
words[w].substr (0, 8) == "priorit:" ||
words[w].substr (0, 7) == "priori:" ||
words[w].substr (0, 6) == "prior:" ||
words[w].substr (0, 5) == "prio:" ||
words[w].substr (0, 4) == "pri:" ||
words[w].substr (0, 8) == "project:" ||
words[w].substr (0, 7) == "projec:" ||
words[w].substr (0, 6) == "proje:" ||
words[w].substr (0, 5) == "proj:" ||
words[w].substr (0, 4) == "pro:")
return task_cmd_line;
}
// x 2009-03-25 Walk the dog +project @context
// This is a test +project @context
@@ -111,7 +136,6 @@ static fileType determineFileType (const std::vector <std::string>& lines)
for (unsigned int i = 0; i < lines.size (); ++i)
{
if (lines[i].length () > 10 &&
lines[i][0] != '#' &&
lines[i].find (",") == std::string::npos)
{
commas_on_every_line = false;
@@ -121,32 +145,187 @@ static fileType determineFileType (const std::vector <std::string>& lines)
if (commas_on_every_line)
return csv;
// TODO text, possibly with commas.
for (unsigned int i = 0; i < lines.size (); ++i)
{
// TODO priority:{H,M,L}
// TODO priorit:{H,M,L}
// TODO priori:{H,M,L}
// TODO prior:{H,M,L}
// TODO prio:{H,M,L}
// TODO pri:{H,M,L}
// TODO project:.+
// TODO projec:.+
// TODO proje:.+
// TODO proj:.+
// TODO pro:.+
}
// Looks like 'text' is the default case, if there is any data at all.
if (lines.size () > 1)
return text;
return not_a_clue;
}
////////////////////////////////////////////////////////////////////////////////
static void decorateTask (T& task, Config& conf)
{
char entryTime[16];
sprintf (entryTime, "%u", (unsigned int) time (NULL));
task.setAttribute ("entry", entryTime);
// Override with default.project, if not specified.
std::string defaultProject = conf.get ("default.project", "");
if (task.getAttribute ("project") == "" && defaultProject != "")
task.setAttribute ("project", defaultProject);
// Override with default.priority, if not specified.
std::string defaultPriority = conf.get ("default.priority", "");
if (task.getAttribute ("priority") == "" &&
defaultPriority != "" &&
validPriority (defaultPriority))
task.setAttribute ("priority", defaultPriority);
}
////////////////////////////////////////////////////////////////////////////////
static std::string importTask_1_4_3 (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
return "task 1.4.3\n";
std::vector <std::string> failed;
std::vector <std::string>::const_iterator it;
for (it = lines.begin (); it != lines.end (); ++it)
{
try
{
// Skip the first line, if it is a columns header line.
if (it->substr (0, 5) == "'id',")
continue;
std::vector <std::string> fields;
split (fields, *it, ',');
// If there is an unexpected number of fields, something is wrong. Perhaps
// an embedded comma, in which case there are (at least) two fields that
// need to be concatenated.
if (fields.size () > 12)
{
int safety = 10; // Shouldn't be more than 10 commas in a description/project.
do
{
std::vector <std::string> modified;
for (unsigned int f = 0; f < fields.size (); ++f)
{
if (fields[f][0] != '\'' &&
fields[f][fields[f].length () - 1] == '\'')
{
modified[modified.size () - 1] += "," + fields[f];
}
else
modified.push_back (fields[f]);
}
fields = modified;
if (safety-- <= 0)
throw "unrecoverable";
}
while (fields.size () > 12);
}
if (fields.size () < 12)
throw "unrecoverable";
// Build up this task ready for insertion.
T task;
// Handle the 12 fields.
for (unsigned int f = 0; f < fields.size (); ++f)
{
switch (f)
{
case 0: // 'uuid'
task.setUUID (fields[f].substr (1, 36));
break;
case 1: // 'status'
if (fields[f] == "'pending'") task.setStatus (T::pending);
else if (fields[f] == "'recurring'") task.setStatus (T::recurring);
else if (fields[f] == "'deleted'") task.setStatus (T::deleted);
else if (fields[f] == "'completed'") task.setStatus (T::completed);
break;
case 2: // 'tags'
if (fields[f].length () > 2)
{
std::string tokens = fields[f].substr (1, fields[f].length () - 2);
std::vector <std::string> tags;
split (tags, tokens, ' ');
for (unsigned int i = 0; i > tags.size (); ++i)
task.addTag (tags[i]);
}
break;
case 3: // entry
task.setAttribute ("entry", fields[f]);
break;
case 4: // start
if (fields[f].length ())
task.setAttribute ("start", fields[f]);
break;
case 5: // due
if (fields[f].length ())
task.setAttribute ("due", fields[f]);
break;
case 6: // end
if (fields[f].length ())
task.setAttribute ("end", fields[f]);
break;
case 7: // 'project'
if (fields[f].length () > 2)
task.setAttribute ("project", fields[f].substr (1, fields[f].length () - 2));
break;
case 8: // 'priority'
if (fields[f].length () > 2)
task.setAttribute ("priority", fields[f].substr (1, fields[f].length () - 2));
break;
case 9: // 'fg'
if (fields[f].length () > 2)
task.setAttribute ("fg", fields[f].substr (1, fields[f].length () - 2));
break;
case 10: // 'bg'
if (fields[f].length () > 2)
task.setAttribute ("bg", fields[f].substr (1, fields[f].length () - 2));
break;
case 11: // 'description'
if (fields[f].length () > 2)
task.setDescription (fields[f].substr (1, fields[f].length () - 2));
break;
}
}
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 ();
}
////////////////////////////////////////////////////////////////////////////////
@@ -155,7 +334,320 @@ static std::string importTask_1_5_0 (
Config& conf,
const std::vector <std::string>& lines)
{
return "task 1.5.0\n";
std::vector <std::string> failed;
std::vector <std::string>::const_iterator it;
for (it = lines.begin (); it != lines.end (); ++it)
{
try
{
// Skip the first line, if it is a columns header line.
if (it->substr (0, 5) == "'id',")
continue;
std::vector <std::string> fields;
split (fields, *it, ',');
// If there is an unexpected number of fields, something is wrong. Perhaps
// an embedded comma, in which case there are (at least) two fields that
// need to be concatenated.
if (fields.size () > 13)
{
int safety = 10; // Shouldn't be more than 10 commas in a description/project.
do
{
std::vector <std::string> modified;
for (unsigned int f = 0; f < fields.size (); ++f)
{
if (fields[f][0] != '\'' &&
fields[f][fields[f].length () - 1] == '\'')
{
modified[modified.size () - 1] += "," + fields[f];
}
else
modified.push_back (fields[f]);
}
fields = modified;
if (safety-- <= 0)
throw "unrecoverable";
}
while (fields.size () > 13);
}
if (fields.size () < 13)
throw "unrecoverable";
// Build up this task ready for insertion.
T task;
// Handle the 13 fields.
for (unsigned int f = 0; f < fields.size (); ++f)
{
switch (f)
{
case 0: // 'uuid'
task.setUUID (fields[f].substr (1, 36));
break;
case 1: // 'status'
if (fields[f] == "'pending'") task.setStatus (T::pending);
else if (fields[f] == "'recurring'") task.setStatus (T::recurring);
else if (fields[f] == "'deleted'") task.setStatus (T::deleted);
else if (fields[f] == "'completed'") task.setStatus (T::completed);
break;
case 2: // 'tags'
if (fields[f].length () > 2)
{
std::string tokens = fields[f].substr (1, fields[f].length () - 2);
std::vector <std::string> tags;
split (tags, tokens, ' ');
for (unsigned int i = 0; i > tags.size (); ++i)
task.addTag (tags[i]);
}
break;
case 3: // entry
task.setAttribute ("entry", fields[f]);
break;
case 4: // start
if (fields[f].length ())
task.setAttribute ("start", fields[f]);
break;
case 5: // due
if (fields[f].length ())
task.setAttribute ("due", fields[f]);
break;
case 6: // recur
if (fields[f].length ())
task.setAttribute ("recur", fields[f]);
break;
case 7: // end
if (fields[f].length ())
task.setAttribute ("end", fields[f]);
break;
case 8: // 'project'
if (fields[f].length () > 2)
task.setAttribute ("project", fields[f].substr (1, fields[f].length () - 2));
break;
case 9: // 'priority'
if (fields[f].length () > 2)
task.setAttribute ("priority", fields[f].substr (1, fields[f].length () - 2));
break;
case 10: // 'fg'
if (fields[f].length () > 2)
task.setAttribute ("fg", fields[f].substr (1, fields[f].length () - 2));
break;
case 11: // 'bg'
if (fields[f].length () > 2)
task.setAttribute ("bg", fields[f].substr (1, fields[f].length () - 2));
break;
case 12: // 'description'
if (fields[f].length () > 2)
task.setDescription (fields[f].substr (1, fields[f].length () - 2));
break;
}
}
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 ();
}
////////////////////////////////////////////////////////////////////////////////
static std::string importTask_1_6_0 (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
std::vector <std::string> failed;
std::vector <std::string>::const_iterator it;
for (it = lines.begin (); it != lines.end (); ++it)
{
try
{
// Skip the first line, if it is a columns header line.
if (it->substr (0, 7) == "'uuid',")
continue;
std::vector <std::string> fields;
split (fields, *it, ',');
// If there is an unexpected number of fields, something is wrong. Perhaps
// an embedded comma, in which case there are (at least) two fields that
// need to be concatenated.
if (fields.size () > 13)
{
int safety = 10; // Shouldn't be more than 10 commas in a description/project.
do
{
std::vector <std::string> modified;
for (unsigned int f = 0; f < fields.size (); ++f)
{
if (fields[f][0] != '\'' &&
fields[f][fields[f].length () - 1] == '\'')
{
modified[modified.size () - 1] += "," + fields[f];
}
else
modified.push_back (fields[f]);
}
fields = modified;
if (safety-- <= 0)
throw "unrecoverable";
}
while (fields.size () > 13);
}
if (fields.size () < 13)
throw "unrecoverable";
// Build up this task ready for insertion.
T task;
// Handle the 13 fields.
for (unsigned int f = 0; f < fields.size (); ++f)
{
switch (f)
{
case 0: // 'uuid'
task.setUUID (fields[f].substr (1, 36));
break;
case 1: // 'status'
if (fields[f] == "'pending'") task.setStatus (T::pending);
else if (fields[f] == "'recurring'") task.setStatus (T::recurring);
else if (fields[f] == "'deleted'") task.setStatus (T::deleted);
else if (fields[f] == "'completed'") task.setStatus (T::completed);
break;
case 2: // 'tags'
if (fields[f].length () > 2)
{
std::string tokens = fields[f].substr (1, fields[f].length () - 2);
std::vector <std::string> tags;
split (tags, tokens, ' ');
for (unsigned int i = 0; i > tags.size (); ++i)
task.addTag (tags[i]);
}
break;
case 3: // entry
task.setAttribute ("entry", fields[f]);
break;
case 4: // start
if (fields[f].length ())
task.setAttribute ("start", fields[f]);
break;
case 5: // due
if (fields[f].length ())
task.setAttribute ("due", fields[f]);
break;
case 6: // recur
if (fields[f].length ())
task.setAttribute ("recur", fields[f]);
break;
case 7: // end
if (fields[f].length ())
task.setAttribute ("end", fields[f]);
break;
case 8: // 'project'
if (fields[f].length () > 2)
task.setAttribute ("project", fields[f].substr (1, fields[f].length () - 2));
break;
case 9: // 'priority'
if (fields[f].length () > 2)
task.setAttribute ("priority", fields[f].substr (1, fields[f].length () - 2));
break;
case 10: // 'fg'
if (fields[f].length () > 2)
task.setAttribute ("fg", fields[f].substr (1, fields[f].length () - 2));
break;
case 11: // 'bg'
if (fields[f].length () > 2)
task.setAttribute ("bg", fields[f].substr (1, fields[f].length () - 2));
break;
case 12: // 'description'
if (fields[f].length () > 2)
task.setDescription (fields[f].substr (1, fields[f].length () - 2));
break;
}
}
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 ();
}
////////////////////////////////////////////////////////////////////////////////
@@ -163,43 +655,28 @@ static std::string importTaskCmdLine (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
return "task command line\n";
}
////////////////////////////////////////////////////////////////////////////////
static std::string importTodoSh_2_0 (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
return "todo.sh 2.0\n";
}
////////////////////////////////////////////////////////////////////////////////
static std::string importText (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
std::vector <std::string> failed;
for (unsigned int i = 0; i < lines.size (); ++i)
std::vector <std::string>::const_iterator it;
for (it = lines.begin (); it != lines.end (); ++it)
{
std::string line = lines[i];
std::string line = *it;
// Strip comments
std::string::size_type pound = line.find ("#");
if (pound != std::string::npos)
line = line.substr (0, pound);
// Skip blank lines
if (line.length () > 0)
try
{
std::vector <std::string> args;
split (args, std::string ("add ") + line, ' ');
T task;
task.parse (std::string ("\"") + lines[i] + "\"");
if (! tdb.addT (task))
failed.push_back (lines[i]);
std::string command;
parse (args, command, task, conf);
handleAdd (tdb, task, conf);
}
catch (...)
{
failed.push_back (line);
}
}
@@ -211,9 +688,198 @@ static std::string importText (
<< " errors."
<< std::endl;
std::string bad;
join (bad, "\n", failed);
return out.str () + "\n" + bad;
if (failed.size ())
{
std::string bad;
join (bad, "\n", failed);
return out.str () + "\nCould not import:\n\n" + bad;
}
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////
static std::string importTodoSh_2_0 (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
std::vector <std::string> failed;
std::vector <std::string>::const_iterator it;
for (it = lines.begin (); it != lines.end (); ++it)
{
try
{
std::vector <std::string> args;
args.push_back ("add");
bool isPending = true;
Date endDate;
std::vector <std::string> words;
split (words, *it, ' ');
for (unsigned int w = 0; w < words.size (); ++w)
{
if (words[w].length () > 1 &&
words[w][0] == '+')
{
args.push_back (std::string ("project:") +
words[w].substr (1, std::string::npos));
}
// Convert "+aaa" to "project:aaa".
// Convert "@aaa" to "+aaa".
else if (words[w].length () > 1 &&
words[w][0] == '@')
{
args.push_back (std::string ("+") +
words[w].substr (1, std::string::npos));
}
// Convert "(A)" to "priority:H".
// Convert "(B)" to "priority:M".
// Convert "(?)" to "priority:L".
else if (words[w].length () == 3 &&
words[w][0] == '(' &&
words[w][2] == ')')
{
if (words[w][1] == 'A') args.push_back ("priority:H");
else if (words[w][1] == 'B') args.push_back ("priority:M");
else args.push_back ("priority:L");
}
// Set status, if completed.
else if (w == 0 &&
words[w] == "x")
{
isPending = false;
}
// Set status, and add an end date, if completed.
else if (! isPending &&
w == 1 &&
words[w].length () == 10 &&
words[w][4] == '-' &&
words[w][7] == '-')
{
endDate = Date (words[w], "Y-M-D");
}
// Just an ordinary word.
else
{
args.push_back (words[w]);
}
}
T task;
std::string command;
parse (args, command, task, conf);
decorateTask (task, conf);
if (isPending)
{
task.setStatus (T::pending);
}
else
{
task.setStatus (T::completed);
char end[16];
sprintf (end, "%u", (unsigned int) endDate.toEpoch ());
task.setAttribute ("end", end);
}
if (! tdb.addT (task))
failed.push_back (*it);
}
catch (...)
{
failed.push_back (*it);
}
}
std::stringstream out;
out << "Imported "
<< (lines.size () - failed.size ())
<< " 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 ();
}
////////////////////////////////////////////////////////////////////////////////
static std::string importText (
TDB& tdb,
Config& conf,
const std::vector <std::string>& lines)
{
std::vector <std::string> failed;
int count = 0;
std::vector <std::string>::const_iterator it;
for (it = lines.begin (); it != lines.end (); ++it)
{
std::string line = *it;
// Strip comments
std::string::size_type pound = line.find ("#");
if (pound != std::string::npos)
line = line.substr (0, pound);
// Skip blank lines
if (line.length () > 0)
{
try
{
++count;
std::vector <std::string> args;
split (args, std::string ("add ") + line, ' ');
T task;
std::string command;
parse (args, command, task, conf);
decorateTask (task, conf);
if (! tdb.addT (task))
failed.push_back (*it);
}
catch (...)
{
failed.push_back (line);
}
}
}
std::stringstream out;
out << "Imported "
<< count
<< " 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 ();
}
////////////////////////////////////////////////////////////////////////////////
@@ -237,8 +903,26 @@ std::string handleImport (TDB& tdb, T& task, Config& conf)
if (file.length () > 0)
{
// Load the file.
std::vector <std::string> all;
slurp (file, all, true);
std::vector <std::string> lines;
slurp (file, lines, true);
std::vector <std::string>::iterator it;
for (it = all.begin (); it != all.end (); ++it)
{
std::string line = *it;
// Strip comments
std::string::size_type pound = line.find ("#");
if (pound != std::string::npos)
line = line.substr (0, pound);
trim (line);
// Skip blank lines
if (line.length () > 0)
lines.push_back (line);
}
// Take a guess at the file type.
fileType type = determineFileType (lines);
@@ -250,6 +934,7 @@ std::string handleImport (TDB& tdb, T& task, Config& conf)
{
case task_1_4_3: out << importTask_1_4_3 (tdb, conf, lines); break;
case task_1_5_0: out << importTask_1_5_0 (tdb, conf, lines); break;
case task_1_6_0: out << importTask_1_6_0 (tdb, conf, lines); break;
case task_cmd_line: out << importTaskCmdLine (tdb, conf, lines); break;
case todo_sh_2_0: out << importTodoSh_2_0 (tdb, conf, lines); break;
case csv: out << importCSV (tdb, conf, lines); break;