mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
Enhancements - T2::legacyParse
- T2 can now parse all supported legacy formats (ff2, ff3) as well as ff4. - Added tag and attribute support to T2. - Added T2 unit tests for all formats.
This commit is contained in:
parent
72e3f76ed9
commit
9d48faa759
9 changed files with 246 additions and 56 deletions
|
@ -25,6 +25,7 @@
|
|||
112 Cannot substitute an empty string
|
||||
113 Unrecognized character(s) at end of substitution
|
||||
114 Malformed substitution
|
||||
115 Tags are not permitted to contain commas
|
||||
|
||||
# 2xx Commands - must be sequential
|
||||
200 active
|
||||
|
|
|
@ -377,6 +377,10 @@ std::cout << "# parse tag removal '" << *arg << "'" << std::endl;
|
|||
if (foundSequence)
|
||||
foundSomethingAfterSequence = true;
|
||||
|
||||
if (arg->find (',') != std::string::npos)
|
||||
throw stringtable.get (TAGS_NO_COMMA,
|
||||
"Tags are not permitted to contain commas.");
|
||||
|
||||
tagRemovals.push_back (arg->substr (1, std::string::npos));
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,8 @@ std::string Record::composeF4 ()
|
|||
//
|
||||
void Record::parse (const std::string& input)
|
||||
{
|
||||
clear ();
|
||||
|
||||
Nibbler n (input);
|
||||
std::string line;
|
||||
if (n.skip ('[') &&
|
||||
|
|
|
@ -629,7 +629,7 @@ int T::determineVersion (const std::string& line)
|
|||
|
||||
// Version 4 looks like:
|
||||
//
|
||||
// [name:"value" ...]
|
||||
// [name:"value" ...]\n
|
||||
//
|
||||
// Scan for [, ] and :".
|
||||
if (line[0] == '[' &&
|
||||
|
|
142
src/T2.cpp
142
src/T2.cpp
|
@ -26,7 +26,6 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include "Nibbler.h"
|
||||
#include "T2.h"
|
||||
|
@ -47,7 +46,7 @@ T2::T2 (const std::string& input)
|
|||
{
|
||||
try
|
||||
{
|
||||
parse (input);
|
||||
Record::parse (input);
|
||||
}
|
||||
|
||||
catch (std::string& e)
|
||||
|
@ -74,8 +73,57 @@ T2::~T2 ()
|
|||
{
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
T2::status T2::textToStatus (const std::string& input)
|
||||
{
|
||||
if (input == "pending") return pending;
|
||||
else if (input == "completed") return completed;
|
||||
else if (input == "deleted") return deleted;
|
||||
else if (input == "recurring") return recurring;
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::string T2::statusToText (T2::status s)
|
||||
{
|
||||
if (s == pending) return "pending";
|
||||
else if (s == completed) return "completed";
|
||||
else if (s == deleted) return "deleted";
|
||||
else if (s == recurring) return "recurring";
|
||||
|
||||
return "pending";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
T2::status T2::getStatus ()
|
||||
{
|
||||
return textToStatus (get ("status"));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::setSatus (T2::status status)
|
||||
{
|
||||
set ("status", statusToText (status));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::parse (const std::string& line)
|
||||
{
|
||||
try
|
||||
{
|
||||
Record::parse (line);
|
||||
}
|
||||
|
||||
catch (std::string& e)
|
||||
{
|
||||
legacyParse (line);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Support FF2, FF3.
|
||||
// Thankfully FF1 is no longer supported.
|
||||
void T2::legacyParse (const std::string& line)
|
||||
{
|
||||
switch (determineVersion (line))
|
||||
|
@ -88,17 +136,18 @@ void T2::legacyParse (const std::string& line)
|
|||
|
||||
// File format version 2, from 2008.1.1 - 2009.3.23
|
||||
case 2:
|
||||
/*
|
||||
{
|
||||
if (line.length () > 46) // ^.{36} . \[\] \[\] \n
|
||||
{
|
||||
mUUID = line.substr (0, 36);
|
||||
set ("uuid", line.substr (0, 36));
|
||||
|
||||
mStatus = line[37] == '+' ? completed
|
||||
T2::status status = line[37] == '+' ? completed
|
||||
: line[37] == 'X' ? deleted
|
||||
: line[37] == 'r' ? recurring
|
||||
: pending;
|
||||
|
||||
set ("status", statusToText (status));
|
||||
|
||||
size_t openTagBracket = line.find ("[");
|
||||
size_t closeTagBracket = line.find ("]", openTagBracket);
|
||||
if (openTagBracket != std::string::npos &&
|
||||
|
@ -111,8 +160,9 @@ void T2::legacyParse (const std::string& line)
|
|||
{
|
||||
std::string tags = line.substr (
|
||||
openTagBracket + 1, closeTagBracket - openTagBracket - 1);
|
||||
std::vector <std::string> rawTags;
|
||||
split (mTags, tags, ' ');
|
||||
std::vector <std::string> tagSet;
|
||||
split (tagSet, tags, ' ');
|
||||
addTags (tagSet);
|
||||
|
||||
std::string attributes = line.substr (
|
||||
openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
|
||||
|
@ -123,10 +173,10 @@ void T2::legacyParse (const std::string& line)
|
|||
std::vector <std::string> pair;
|
||||
split (pair, pairs[i], ':');
|
||||
if (pair.size () == 2)
|
||||
mAttributes[pair[0]] = pair[1];
|
||||
set (pair[0], pair[1]);
|
||||
}
|
||||
|
||||
mDescription = line.substr (closeAttrBracket + 2, std::string::npos);
|
||||
set ("description", line.substr (closeAttrBracket + 2, std::string::npos));
|
||||
}
|
||||
else
|
||||
throw std::string ("Missing attribute brackets");
|
||||
|
@ -137,24 +187,24 @@ void T2::legacyParse (const std::string& line)
|
|||
else
|
||||
throw std::string ("Line too short");
|
||||
|
||||
mAnnotations.clear ();
|
||||
removeAnnotations ();
|
||||
}
|
||||
*/
|
||||
break;
|
||||
|
||||
// File format version 3, from 2009.3.23
|
||||
case 3:
|
||||
/*
|
||||
{
|
||||
if (line.length () > 49) // ^.{36} . \[\] \[\] \[\] \n
|
||||
{
|
||||
mUUID = line.substr (0, 36);
|
||||
set ("uuid", line.substr (0, 36));
|
||||
|
||||
mStatus = line[37] == '+' ? completed
|
||||
T2::status status = line[37] == '+' ? completed
|
||||
: line[37] == 'X' ? deleted
|
||||
: line[37] == 'r' ? recurring
|
||||
: pending;
|
||||
|
||||
set ("status", statusToText (status));
|
||||
|
||||
size_t openTagBracket = line.find ("[");
|
||||
size_t closeTagBracket = line.find ("]", openTagBracket);
|
||||
if (openTagBracket != std::string::npos &&
|
||||
|
@ -172,8 +222,9 @@ void T2::legacyParse (const std::string& line)
|
|||
{
|
||||
std::string tags = line.substr (
|
||||
openTagBracket + 1, closeTagBracket - openTagBracket - 1);
|
||||
std::vector <std::string> rawTags;
|
||||
split (mTags, tags, ' ');
|
||||
std::vector <std::string> tagSet;
|
||||
split (tagSet, tags, ' ');
|
||||
addTags (tagSet);
|
||||
|
||||
std::string attributes = line.substr (
|
||||
openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
|
||||
|
@ -185,9 +236,10 @@ void T2::legacyParse (const std::string& line)
|
|||
std::vector <std::string> pair;
|
||||
split (pair, pairs[i], ':');
|
||||
if (pair.size () == 2)
|
||||
mAttributes[pair[0]] = pair[1];
|
||||
set (pair[0], pair[1]);
|
||||
}
|
||||
|
||||
/*
|
||||
// Extract and split the annotations, which are of the form:
|
||||
// 1234:"..." 5678:"..."
|
||||
std::string annotations = line.substr (
|
||||
|
@ -225,8 +277,9 @@ void T2::legacyParse (const std::string& line)
|
|||
mAnnotations[::atoi (name.c_str ())] = value;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
mDescription = line.substr (closeAnnoBracket + 2, std::string::npos);
|
||||
set ("description", line.substr (closeAnnoBracket + 2, std::string::npos));
|
||||
}
|
||||
else
|
||||
throw std::string ("Missing annotation brackets.");
|
||||
|
@ -240,7 +293,6 @@ void T2::legacyParse (const std::string& line)
|
|||
else
|
||||
throw std::string ("Line too short.");
|
||||
}
|
||||
*/
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -271,10 +323,7 @@ void T2::getAnnotations (std::vector <Att>& annotations) const
|
|||
void T2::setAnnotations (const std::vector <Att>& annotations)
|
||||
{
|
||||
// Erase old annotations.
|
||||
Record::iterator i;
|
||||
for (i = this->begin (); i != this->end (); ++i)
|
||||
if (i->first.substr (0, 11) == "annotation_")
|
||||
this->erase (i);
|
||||
removeAnnotations ();
|
||||
|
||||
std::vector <Att>::const_iterator ci;
|
||||
for (ci = annotations.begin (); ci != annotations.end (); ++ci)
|
||||
|
@ -293,6 +342,37 @@ void T2::addAnnotation (const std::string& description)
|
|||
(*this)[s.str ()] = Att (s.str (), description);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::removeAnnotations ()
|
||||
{
|
||||
// Erase old annotations.
|
||||
Record::iterator i;
|
||||
for (i = this->begin (); i != this->end (); ++i)
|
||||
if (i->first.substr (0, 11) == "annotation_")
|
||||
this->erase (i);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int T2::getTagCount ()
|
||||
{
|
||||
std::vector <std::string> tags;
|
||||
split (tags, get ("tags"), ',');
|
||||
|
||||
return (int) tags.size ();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool T2::hasTag (const std::string& tag)
|
||||
{
|
||||
std::vector <std::string> tags;
|
||||
split (tags, get ("tags"), ',');
|
||||
|
||||
if (std::find (tags.begin (), tags.end (), tag) != tags.end ())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::addTag (const std::string& tag)
|
||||
{
|
||||
|
@ -308,6 +388,22 @@ void T2::addTag (const std::string& tag)
|
|||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::addTags (const std::vector <std::string>& tags)
|
||||
{
|
||||
remove ("tags");
|
||||
|
||||
std::vector <std::string>::const_iterator it;
|
||||
for (it = tags.begin (); it != tags.end (); ++it)
|
||||
addTag (*it);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::getTags (std::vector<std::string>& tags)
|
||||
{
|
||||
split (tags, get ("tags"), ',');
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void T2::removeTag (const std::string& tag)
|
||||
{
|
||||
|
|
24
src/T2.h
24
src/T2.h
|
@ -40,6 +40,7 @@ public:
|
|||
T2& operator= (const T2&); // Assignment operator
|
||||
~T2 (); // Destructor
|
||||
|
||||
void parse (const std::string&);
|
||||
std::string composeCSV ();
|
||||
|
||||
// Status values.
|
||||
|
@ -53,28 +54,23 @@ public:
|
|||
int id () const { return sequence.size () ? sequence[0] : 0; }
|
||||
void id (int anotherId) { sequence.push_back (anotherId); }
|
||||
|
||||
/*
|
||||
status getStatus () const { return mStatus; }
|
||||
void setStatus (status s) { mStatus = s; }
|
||||
static status textToStatus (const std::string&);
|
||||
static std::string statusToText (status);
|
||||
|
||||
bool hasTag (const std::string&) const;
|
||||
void getRemoveTags (std::vector<std::string>&); // SPECIAL
|
||||
void addRemoveTag (const std::string&); // SPECIAL
|
||||
int getTagCount () const;
|
||||
void getTags (std::vector<std::string>&) const;
|
||||
*/
|
||||
status getStatus ();
|
||||
void setSatus (status);
|
||||
|
||||
int getTagCount ();
|
||||
bool hasTag (const std::string&);
|
||||
void addTag (const std::string&);
|
||||
/*
|
||||
void addTags (const std::vector <std::string>&);
|
||||
*/
|
||||
void getTags (std::vector<std::string>&);
|
||||
void removeTag (const std::string&);
|
||||
/*
|
||||
void removeTags ();
|
||||
*/
|
||||
|
||||
void getAnnotations (std::vector <Att>&) const;
|
||||
void setAnnotations (const std::vector <Att>&);
|
||||
void addAnnotation (const std::string&);
|
||||
void removeAnnotations ();
|
||||
|
||||
bool valid () const;
|
||||
|
||||
|
|
|
@ -188,6 +188,9 @@ int TDB2::load (std::vector <T2>& tasks, Filter& filter)
|
|||
++line_number;
|
||||
}
|
||||
|
||||
// TODO If the filter contains Status:x where x is not deleted or
|
||||
// completed, then this can be skipped.
|
||||
|
||||
line_number = 1;
|
||||
file = location->path + "/completed.data";
|
||||
while (fgets (line, T_LINE_MAX, location->completed))
|
||||
|
@ -243,6 +246,9 @@ void TDB2::update (T2& before, T2& after)
|
|||
// TODO writes all, including comments
|
||||
int TDB2::commit ()
|
||||
{
|
||||
// TODO Two passes: first the pending file.
|
||||
// then the compelted file.
|
||||
|
||||
throw std::string ("unimplemented TDB2::commit");
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,8 @@
|
|||
#define SUBST_BAD_CHARS 113
|
||||
#define SUBST_MALFORMED 114
|
||||
|
||||
#define TAGS_NO_COMMA 115
|
||||
|
||||
// 2xx Commands
|
||||
#define CMD_ACTIVE 200
|
||||
#define CMD_ADD 201
|
||||
|
|
|
@ -32,27 +32,110 @@ Context context;
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
int main (int argc, char** argv)
|
||||
{
|
||||
UnitTest test (3);
|
||||
UnitTest test (34);
|
||||
|
||||
T2 t;
|
||||
t.addTag ("tag1");
|
||||
t.addTag ("tag2");
|
||||
test.is (t.composeF4 (), "[tags:\"tag1,tag2\" uuid:\"...\"]", "T2::addTag");
|
||||
test.is ((int)T2::textToStatus ("pending"), (int)T2::pending, "textToStatus pending");
|
||||
test.is ((int)T2::textToStatus ("completed"), (int)T2::completed, "textToStatus completed");
|
||||
test.is ((int)T2::textToStatus ("deleted"), (int)T2::deleted, "textToStatus deleted");
|
||||
test.is ((int)T2::textToStatus ("recurring"), (int)T2::recurring, "textToStatus recurring");
|
||||
|
||||
T2 t2 (t.composeF4 ());
|
||||
test.is (t2.composeF4 (), "[tags:\"tag1,tag2\" uuid:\"...\"]", "T2::composeF4 -> parse round trip");
|
||||
test.is (T2::statusToText (T2::pending), "pending", "statusToText pending");
|
||||
test.is (T2::statusToText (T2::completed), "completed", "statusToText completed");
|
||||
test.is (T2::statusToText (T2::deleted), "deleted", "statusToText deleted");
|
||||
test.is (T2::statusToText (T2::recurring), "recurring", "statusToText recurring");
|
||||
|
||||
// Round-trip testing.
|
||||
T2 t3;
|
||||
std::string before = t3.composeF4 ();
|
||||
/*
|
||||
t3 (t3.composeF4 ());
|
||||
t3 (t3.composeF4 ());
|
||||
t3 (t3.composeF4 ());
|
||||
*/
|
||||
t3.parse (before);
|
||||
std::string after = t3.composeF4 ();
|
||||
t3.parse (after);
|
||||
after = t3.composeF4 ();
|
||||
t3.parse (after);
|
||||
after = t3.composeF4 ();
|
||||
test.is (before, after, "T2::composeF4 -> parse round trip 4 iterations");
|
||||
|
||||
// Legacy Format 1
|
||||
// [tags] [attributes] description\n
|
||||
// X [tags] [attributes] description\n
|
||||
std::string sample = "[tag1 tag2] [att1:value1 att2:value2] Description";
|
||||
sample = "X "
|
||||
"[one two] "
|
||||
"[att1:value1 att2:value2] "
|
||||
"Description";
|
||||
bool good = true;
|
||||
try { T2 ff1 (sample); } catch (...) { good = false; }
|
||||
test.notok (good, "Support for ff1 removed");
|
||||
|
||||
// Legacy Format 2
|
||||
// uuid status [tags] [attributes] description\n
|
||||
sample = "00000000-0000-0000-0000-000000000000 "
|
||||
"- "
|
||||
"[tag1 tag2] "
|
||||
"[att1:value1 att2:value2] "
|
||||
"Description";
|
||||
T2 ff2 (sample);
|
||||
std::string value = ff2.get ("uuid");
|
||||
test.is (value, "00000000-0000-0000-0000-000000000000", "ff2 uuid");
|
||||
value = ff2.get ("status");
|
||||
test.is (value, "pending", "ff2 status");
|
||||
test.ok (ff2.hasTag ("tag1"), "ff2 tag1");
|
||||
test.ok (ff2.hasTag ("tag2"), "ff2 tag2");
|
||||
test.is (ff2.getTagCount (), 2, "ff2 # tags");
|
||||
value = ff2.get ("att1");
|
||||
test.is (value, "value1", "ff2 att1");
|
||||
value = ff2.get ("att2");
|
||||
test.is (value, "value2", "ff2 att2");
|
||||
value = ff2.get ("description");
|
||||
test.is (value, "Description", "ff2 description");
|
||||
|
||||
// Legacy Format 3
|
||||
// uuid status [tags] [attributes] [annotations] description\n
|
||||
sample = "00000000-0000-0000-0000-000000000000 "
|
||||
"- "
|
||||
"[tag1 tag2] "
|
||||
"[att1:value1 att2:value2] "
|
||||
"[123:ann1 456:ann2] Description";
|
||||
T2 ff3 (sample);
|
||||
value = ff2.get ("uuid");
|
||||
test.is (value, "00000000-0000-0000-0000-000000000000", "ff3 uuid");
|
||||
value = ff2.get ("status");
|
||||
test.is (value, "pending", "ff3 status");
|
||||
test.ok (ff2.hasTag ("tag1"), "ff3 tag1");
|
||||
test.ok (ff2.hasTag ("tag2"), "ff3 tag2");
|
||||
test.is (ff2.getTagCount (), 2, "ff3 # tags");
|
||||
value = ff3.get ("att1");
|
||||
test.is (value, "value1", "ff3 att1");
|
||||
value = ff3.get ("att2");
|
||||
test.is (value, "value2", "ff3 att2");
|
||||
value = ff3.get ("description");
|
||||
test.is (value, "Description", "ff3 description");
|
||||
|
||||
// Current Format 4
|
||||
// [name:"value" ...]\n
|
||||
sample = "["
|
||||
"uuid:\"00000000-0000-0000-0000-000000000000\" "
|
||||
"status:\"P\" "
|
||||
"tags:\"tag1&commaltag2\" "
|
||||
"att1:\"value1\" "
|
||||
"att2:\"value2\" "
|
||||
"description:\"Description\""
|
||||
"]";
|
||||
T2 ff4 (sample);
|
||||
value = ff2.get ("uuid");
|
||||
test.is (value, "00000000-0000-0000-0000-000000000000", "ff4 uuid");
|
||||
value = ff2.get ("status");
|
||||
test.is (value, "pending", "ff4 status");
|
||||
test.ok (ff2.hasTag ("tag1"), "ff4 tag1");
|
||||
test.ok (ff2.hasTag ("tag2"), "ff4 tag2");
|
||||
test.is (ff2.getTagCount (), 2, "ff4 # tags");
|
||||
value = ff4.get ("att1");
|
||||
test.is (value, "value1", "ff4 att1");
|
||||
value = ff4.get ("att2");
|
||||
test.is (value, "value2", "ff4 att2");
|
||||
value = ff4.get ("description");
|
||||
test.is (value, "Description", "ff4 description");
|
||||
|
||||
/*
|
||||
|
||||
T2::composeCSV
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue