From 9d48faa7599c2cc9693a3965e4e9971ed351822a Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sun, 7 Jun 2009 22:17:11 -0400 Subject: [PATCH] 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. --- i18n/strings.en-US | 1 + src/Context.cpp | 4 ++ src/Record.cpp | 2 + src/T.cpp | 2 +- src/T2.cpp | 154 ++++++++++++++++++++++++++++++++++++--------- src/T2.h | 24 +++---- src/TDB2.cpp | 6 ++ src/i18n.h | 2 + src/tests/t2.t.cpp | 107 +++++++++++++++++++++++++++---- 9 files changed, 246 insertions(+), 56 deletions(-) diff --git a/i18n/strings.en-US b/i18n/strings.en-US index 6493b21b0..33f959d91 100644 --- a/i18n/strings.en-US +++ b/i18n/strings.en-US @@ -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 diff --git a/src/Context.cpp b/src/Context.cpp index d255c2a03..f405245b9 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -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)); } diff --git a/src/Record.cpp b/src/Record.cpp index 97f657601..76a03a9eb 100644 --- a/src/Record.cpp +++ b/src/Record.cpp @@ -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 ('[') && diff --git a/src/T.cpp b/src/T.cpp index 213de7815..f468b3ac5 100644 --- a/src/T.cpp +++ b/src/T.cpp @@ -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] == '[' && diff --git a/src/T2.cpp b/src/T2.cpp index 02790c0ab..56b0d2801 100644 --- a/src/T2.cpp +++ b/src/T2.cpp @@ -26,7 +26,6 @@ //////////////////////////////////////////////////////////////////////////////// #include -#include #include #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,16 +136,17 @@ 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 - : line[37] == 'X' ? deleted - : line[37] == 'r' ? recurring - : pending; + 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); @@ -111,8 +160,9 @@ void T2::legacyParse (const std::string& line) { std::string tags = line.substr ( openTagBracket + 1, closeTagBracket - openTagBracket - 1); - std::vector rawTags; - split (mTags, tags, ' '); + std::vector 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 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,23 +187,23 @@ 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 - : line[37] == 'X' ? deleted - : line[37] == 'r' ? recurring - : pending; + 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); @@ -172,8 +222,9 @@ void T2::legacyParse (const std::string& line) { std::string tags = line.substr ( openTagBracket + 1, closeTagBracket - openTagBracket - 1); - std::vector rawTags; - split (mTags, tags, ' '); + std::vector 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 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 & annotations) const void T2::setAnnotations (const std::vector & 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 ::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 tags; + split (tags, get ("tags"), ','); + + return (int) tags.size (); +} + +//////////////////////////////////////////////////////////////////////////////// +bool T2::hasTag (const std::string& tag) +{ + std::vector 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 & tags) +{ + remove ("tags"); + + std::vector ::const_iterator it; + for (it = tags.begin (); it != tags.end (); ++it) + addTag (*it); +} + +//////////////////////////////////////////////////////////////////////////////// +void T2::getTags (std::vector& tags) +{ + split (tags, get ("tags"), ','); +} + //////////////////////////////////////////////////////////////////////////////// void T2::removeTag (const std::string& tag) { diff --git a/src/T2.h b/src/T2.h index d4e6b4100..3897a4e2b 100644 --- a/src/T2.h +++ b/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&); // SPECIAL - void addRemoveTag (const std::string&); // SPECIAL - int getTagCount () const; - void getTags (std::vector&) const; -*/ + status getStatus (); + void setSatus (status); + + int getTagCount (); + bool hasTag (const std::string&); void addTag (const std::string&); -/* void addTags (const std::vector &); -*/ + void getTags (std::vector&); void removeTag (const std::string&); -/* - void removeTags (); -*/ void getAnnotations (std::vector &) const; void setAnnotations (const std::vector &); void addAnnotation (const std::string&); + void removeAnnotations (); bool valid () const; diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 87e438e6d..0e7e78dd6 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -188,6 +188,9 @@ int TDB2::load (std::vector & 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"); } diff --git a/src/i18n.h b/src/i18n.h index bba7b05c2..6e1687aa7 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -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 diff --git a/src/tests/t2.t.cpp b/src/tests/t2.t.cpp index 28490483b..eb21d6a49 100644 --- a/src/tests/t2.t.cpp +++ b/src/tests/t2.t.cpp @@ -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