mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
Enhancement - related to, but not fixing bug #293
- Added new attribute modifiers 'word' and 'noword' which find the existence of whole words, or prove the non-existence of whole words. If a task has the description "Pay the bill", then "description.word:the" will match, but "description.word:th" will not. For partial word matches, there is still "description.contains:th". - Added unit tests for the text.cpp functions. - Added unit tests including the new modifiers in filters. - Added unit tests to parse the new modifiers. - Modified man page. - Modified the Context::autoFilter processing to use the new modifiers for +tag and -tag filtering. - Added a support email to an error message, while looking at the filter code. - Added new modifiers to the help report. - Modified a utf8.t unit test to include an alphanumeric tag, rather than a smiley face.
This commit is contained in:
parent
d019126086
commit
7acef0c9fd
12 changed files with 189 additions and 10 deletions
|
@ -10,6 +10,11 @@
|
||||||
+ Added feature #341 that makes explicit references to the task and taskrc
|
+ Added feature #341 that makes explicit references to the task and taskrc
|
||||||
man pages, both in the auto-generated .taskrc file and the version command
|
man pages, both in the auto-generated .taskrc file and the version command
|
||||||
output (thanks to Cory Donnelly).
|
output (thanks to Cory Donnelly).
|
||||||
|
+ Added new attribute modifiers 'word' and 'noword' which find the existence
|
||||||
|
of whole words, or prove the non-existence of whole words. If a task has
|
||||||
|
the description "Pay the bill", then "description.word:the" will match, but
|
||||||
|
"description.word:th" will not. For partial word matches, there is still
|
||||||
|
"description.contains:th".
|
||||||
+ Fixed bug that showed a calendar for the year 2037 when 'task calendar due'
|
+ Fixed bug that showed a calendar for the year 2037 when 'task calendar due'
|
||||||
was run, and there are no tasks with due dates.
|
was run, and there are no tasks with due dates.
|
||||||
+ Fixed bug #316 which caused the timesheet report to display an oddly sorted
|
+ Fixed bug #316 which caused the timesheet report to display an oddly sorted
|
||||||
|
|
|
@ -266,6 +266,10 @@ Attribute modifiers improve filters. Supported modifiers are:
|
||||||
.B startswith (synonym left)
|
.B startswith (synonym left)
|
||||||
.br
|
.br
|
||||||
.B endswith (synonym right)
|
.B endswith (synonym right)
|
||||||
|
.br
|
||||||
|
.B word
|
||||||
|
.br
|
||||||
|
.B noword
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
36
src/Att.cpp
36
src/Att.cpp
|
@ -77,6 +77,8 @@ static const char* modifierNames[] =
|
||||||
"hasnt",
|
"hasnt",
|
||||||
"startswith", "left",
|
"startswith", "left",
|
||||||
"endswith", "right",
|
"endswith", "right",
|
||||||
|
"word",
|
||||||
|
"noword"
|
||||||
};
|
};
|
||||||
|
|
||||||
#define NUM_INTERNAL_NAMES (sizeof (internalNames) / sizeof (internalNames[0]))
|
#define NUM_INTERNAL_NAMES (sizeof (internalNames) / sizeof (internalNames[0]))
|
||||||
|
@ -375,7 +377,7 @@ bool Att::validNameValue (
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// TODO Obsolete
|
// TODO Deprecated - remove.
|
||||||
bool Att::validMod (const std::string& mod)
|
bool Att::validMod (const std::string& mod)
|
||||||
{
|
{
|
||||||
for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i)
|
for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i)
|
||||||
|
@ -412,7 +414,8 @@ std::string Att::type (const std::string& name) const
|
||||||
std::string Att::modType (const std::string& name) const
|
std::string Att::modType (const std::string& name) const
|
||||||
{
|
{
|
||||||
if (name == "hasnt" ||
|
if (name == "hasnt" ||
|
||||||
name == "isnt")
|
name == "isnt" ||
|
||||||
|
name == "noword")
|
||||||
return "negative";
|
return "negative";
|
||||||
|
|
||||||
return "positive";
|
return "positive";
|
||||||
|
@ -615,6 +618,35 @@ bool Att::match (const Att& other) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// word = contains as a substring, with word boundaries.
|
||||||
|
else if (mMod == "word") // TODO i18n
|
||||||
|
{
|
||||||
|
// Fail if the substring is not found.
|
||||||
|
std::string::size_type sub = other.mValue.find (mValue);
|
||||||
|
if (sub == std::string::npos)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Also fail if there is no word boundary at beginning and end.
|
||||||
|
if (!isWordStart (other.mValue, sub))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!isWordEnd (other.mValue, sub + mValue.length () - 1))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// noword = does not contain as a substring, with word boundaries.
|
||||||
|
else if (mMod == "noword") // TODO i18n
|
||||||
|
{
|
||||||
|
// Fail if the substring is not found.
|
||||||
|
std::string::size_type sub = other.mValue.find (mValue);
|
||||||
|
if (sub != std::string::npos &&
|
||||||
|
isWordStart (other.mValue, sub) &&
|
||||||
|
isWordEnd (other.mValue, sub + mValue.length () - 1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -690,6 +690,9 @@ void Context::parse (
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Note: The reason some of these are commented out is because the ::clear
|
||||||
|
// method is not really "clear" but "clear_some". Some members do not need to
|
||||||
|
// be initialized. That makes this method something of a misnomer. So be it.
|
||||||
void Context::clear ()
|
void Context::clear ()
|
||||||
{
|
{
|
||||||
// Config config;
|
// Config config;
|
||||||
|
@ -759,6 +762,7 @@ void Context::autoFilter (Task& t, Filter& f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The mechanism for filtering on tags is +/-<tag>.
|
// The mechanism for filtering on tags is +/-<tag>.
|
||||||
|
// Do not handle here - see below.
|
||||||
else if (att->second.name () == "tags")
|
else if (att->second.name () == "tags")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -776,17 +780,22 @@ void Context::autoFilter (Task& t, Filter& f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is now a correct implementation of a filter on the presence or absence
|
||||||
|
// of a tag. The prior code provided the illusion of leftmost partial tag
|
||||||
|
// matches, but was really using the 'contains' and 'nocontains' attribute
|
||||||
|
// modifiers. See bug #293.
|
||||||
|
|
||||||
// Include tagAdditions.
|
// Include tagAdditions.
|
||||||
foreach (tag, tagAdditions)
|
foreach (tag, tagAdditions)
|
||||||
{
|
{
|
||||||
f.push_back (Att ("tags", "has", *tag));
|
f.push_back (Att ("tags", "word", *tag));
|
||||||
debug ("auto filter: +" + *tag);
|
debug ("auto filter: +" + *tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include tagRemovals.
|
// Include tagRemovals.
|
||||||
foreach (tag, tagRemovals)
|
foreach (tag, tagRemovals)
|
||||||
{
|
{
|
||||||
f.push_back (Att ("tags", "hasnt", *tag));
|
f.push_back (Att ("tags", "noword", *tag));
|
||||||
debug ("auto filter: -" + *tag);
|
debug ("auto filter: -" + *tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ void Filter::applySequence (std::vector<Task>& all, Sequence& sequence)
|
||||||
std::vector <int> right;
|
std::vector <int> right;
|
||||||
listDiff (filteredSequence, (std::vector <int>&)sequence, left, right);
|
listDiff (filteredSequence, (std::vector <int>&)sequence, left, right);
|
||||||
if (left.size ())
|
if (left.size ())
|
||||||
throw std::string ("Sequence filtering error - please report this error");
|
throw std::string ("Sequence filtering error - please report this error to support@taskwarrior.org");
|
||||||
|
|
||||||
if (right.size ())
|
if (right.size ())
|
||||||
{
|
{
|
||||||
|
|
|
@ -260,6 +260,8 @@ int longUsage (std::string &outs)
|
||||||
<< " hasnt" << "\n"
|
<< " hasnt" << "\n"
|
||||||
<< " startswith (synonym left)" << "\n"
|
<< " startswith (synonym left)" << "\n"
|
||||||
<< " endswith (synonym right)" << "\n"
|
<< " endswith (synonym right)" << "\n"
|
||||||
|
<< " word" << "\n"
|
||||||
|
<< " noword" << "\n"
|
||||||
<< "\n"
|
<< "\n"
|
||||||
<< " For example:" << "\n"
|
<< " For example:" << "\n"
|
||||||
<< " task list due.before:eom priority.not:L" << "\n"
|
<< " task list due.before:eom priority.not:L" << "\n"
|
||||||
|
|
|
@ -33,7 +33,7 @@ Context context;
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int main (int argc, char** argv)
|
int main (int argc, char** argv)
|
||||||
{
|
{
|
||||||
UnitTest t (95);
|
UnitTest t (97);
|
||||||
|
|
||||||
Att a;
|
Att a;
|
||||||
t.notok (a.valid ("name"), "Att::valid name -> fail");
|
t.notok (a.valid ("name"), "Att::valid name -> fail");
|
||||||
|
@ -146,6 +146,14 @@ int main (int argc, char** argv)
|
||||||
try {a6.mod ("endswith");} catch (...) {good = false;}
|
try {a6.mod ("endswith");} catch (...) {good = false;}
|
||||||
t.ok (good, "Att::mod (endswith)");
|
t.ok (good, "Att::mod (endswith)");
|
||||||
|
|
||||||
|
good = true;
|
||||||
|
try {a6.mod ("word");} catch (...) {good = false;}
|
||||||
|
t.ok (good, "Att::mod (word)");
|
||||||
|
|
||||||
|
good = true;
|
||||||
|
try {a6.mod ("noword");} catch (...) {good = false;}
|
||||||
|
t.ok (good, "Att::mod (noword)");
|
||||||
|
|
||||||
good = true;
|
good = true;
|
||||||
try {a6.mod ("fartwizzle");} catch (...) {good = false;}
|
try {a6.mod ("fartwizzle");} catch (...) {good = false;}
|
||||||
t.notok (good, "Att::mod (fartwizzle)");
|
t.notok (good, "Att::mod (fartwizzle)");
|
||||||
|
|
|
@ -34,7 +34,7 @@ Context context;
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int main (int argc, char** argv)
|
int main (int argc, char** argv)
|
||||||
{
|
{
|
||||||
UnitTest test (14);
|
UnitTest test (20);
|
||||||
|
|
||||||
// Create a filter consisting of two Att criteria.
|
// Create a filter consisting of two Att criteria.
|
||||||
Filter f;
|
Filter f;
|
||||||
|
@ -66,6 +66,7 @@ int main (int argc, char** argv)
|
||||||
// Modifiers.
|
// Modifiers.
|
||||||
Task mods;
|
Task mods;
|
||||||
mods.set ("name", "value");
|
mods.set ("name", "value");
|
||||||
|
mods.set ("description", "hello, world.");
|
||||||
|
|
||||||
Att a ("name", "is", "value");
|
Att a ("name", "is", "value");
|
||||||
f.clear ();
|
f.clear ();
|
||||||
|
@ -124,6 +125,42 @@ int main (int argc, char** argv)
|
||||||
"below"
|
"below"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
a = Att ("description", "word", "hello");
|
||||||
|
f.clear ();
|
||||||
|
f.push_back (a);
|
||||||
|
test.ok (f.pass (mods), "description:hello, world. -> description.word:hello = match");
|
||||||
|
// TODO test inverse.
|
||||||
|
|
||||||
|
a = Att ("description", "word", "world");
|
||||||
|
f.clear ();
|
||||||
|
f.push_back (a);
|
||||||
|
test.ok (f.pass (mods), "description:hello, world. -> description.word:world = match");
|
||||||
|
// TODO test inverse.
|
||||||
|
|
||||||
|
a = Att ("description", "word", "pig");
|
||||||
|
f.clear ();
|
||||||
|
f.push_back (a);
|
||||||
|
test.notok (f.pass (mods), "description:hello, world. -> description.word:pig = no match");
|
||||||
|
// TODO test inverse.
|
||||||
|
|
||||||
|
a = Att ("description", "noword", "hello");
|
||||||
|
f.clear ();
|
||||||
|
f.push_back (a);
|
||||||
|
test.notok (f.pass (mods), "description:hello, world. -> description.noword:hello = no match");
|
||||||
|
// TODO test inverse.
|
||||||
|
|
||||||
|
a = Att ("description", "noword", "world");
|
||||||
|
f.clear ();
|
||||||
|
f.push_back (a);
|
||||||
|
test.notok (f.pass (mods), "description:hello, world. -> description.noword:world = no match");
|
||||||
|
// TODO test inverse.
|
||||||
|
|
||||||
|
a = Att ("description", "noword", "pig");
|
||||||
|
f.clear ();
|
||||||
|
f.push_back (a);
|
||||||
|
test.ok (f.pass (mods), "description:hello, world. -> description.noword:pig = match");
|
||||||
|
// TODO test inverse.
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ Context context;
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int main (int argc, char** argv)
|
int main (int argc, char** argv)
|
||||||
{
|
{
|
||||||
UnitTest t (117);
|
UnitTest t (147);
|
||||||
|
|
||||||
// void wrapText (std::vector <std::string>& lines, const std::string& text, const int width)
|
// void wrapText (std::vector <std::string>& lines, const std::string& text, const int width)
|
||||||
std::string text = "This is a test of the line wrapping code.";
|
std::string text = "This is a test of the line wrapping code.";
|
||||||
|
@ -267,6 +267,44 @@ int main (int argc, char** argv)
|
||||||
t.notok (noVerticalSpace ("a\rb"), "noVerticalSpace 'a\\rb' -> false");
|
t.notok (noVerticalSpace ("a\rb"), "noVerticalSpace 'a\\rb' -> false");
|
||||||
t.notok (noVerticalSpace ("a\fb"), "noVerticalSpace 'a\\fb' -> false");
|
t.notok (noVerticalSpace ("a\fb"), "noVerticalSpace 'a\\fb' -> false");
|
||||||
|
|
||||||
|
text = "Hello, world.";
|
||||||
|
// 0123456789012
|
||||||
|
// s e s e
|
||||||
|
|
||||||
|
// bool isWordStart (const std::string&, std::string::size_type);
|
||||||
|
t.notok (isWordStart ("", 0), "isWordStart (\"\", 0) -> false");
|
||||||
|
t.ok (isWordStart ("foo", 0), "isWordStart (\"foo\", 0) -> true");
|
||||||
|
t.ok (isWordStart (text, 0), "isWordStart (\"Hello, world.\", 0) -> true");
|
||||||
|
t.notok (isWordStart (text, 1), "isWordStart (\"Hello, world.\", 1) -> false");
|
||||||
|
t.notok (isWordStart (text, 2), "isWordStart (\"Hello, world.\", 2) -> false");
|
||||||
|
t.notok (isWordStart (text, 3), "isWordStart (\"Hello, world.\", 3) -> false");
|
||||||
|
t.notok (isWordStart (text, 4), "isWordStart (\"Hello, world.\", 4) -> false");
|
||||||
|
t.notok (isWordStart (text, 5), "isWordStart (\"Hello, world.\", 5) -> false");
|
||||||
|
t.notok (isWordStart (text, 6), "isWordStart (\"Hello, world.\", 6) -> false");
|
||||||
|
t.ok (isWordStart (text, 7), "isWordStart (\"Hello, world.\", 7) -> true");
|
||||||
|
t.notok (isWordStart (text, 8), "isWordStart (\"Hello, world.\", 8) -> false");
|
||||||
|
t.notok (isWordStart (text, 9), "isWordStart (\"Hello, world.\", 9) -> false");
|
||||||
|
t.notok (isWordStart (text, 10), "isWordStart (\"Hello, world.\", 10) -> false");
|
||||||
|
t.notok (isWordStart (text, 11), "isWordStart (\"Hello, world.\", 11) -> false");
|
||||||
|
t.notok (isWordStart (text, 12), "isWordStart (\"Hello, world.\", 12) -> false");
|
||||||
|
|
||||||
|
// bool isWordEnd (const std::string&, std::string::size_type);
|
||||||
|
t.notok (isWordEnd ("", 0), "isWordEnd (\"\", 0) -> false");
|
||||||
|
t.ok (isWordEnd ("foo", 2), "isWordEnd (\"foo\", 2) -> true");
|
||||||
|
t.notok (isWordEnd (text, 0), "isWordEnd (\"Hello, world.\", 0) -> false");
|
||||||
|
t.notok (isWordEnd (text, 1), "isWordEnd (\"Hello, world.\", 1) -> false");
|
||||||
|
t.notok (isWordEnd (text, 2), "isWordEnd (\"Hello, world.\", 2) -> false");
|
||||||
|
t.notok (isWordEnd (text, 3), "isWordEnd (\"Hello, world.\", 3) -> false");
|
||||||
|
t.ok (isWordEnd (text, 4), "isWordEnd (\"Hello, world.\", 4) -> true");
|
||||||
|
t.notok (isWordEnd (text, 5), "isWordEnd (\"Hello, world.\", 5) -> false");
|
||||||
|
t.notok (isWordEnd (text, 6), "isWordEnd (\"Hello, world.\", 6) -> false");
|
||||||
|
t.notok (isWordEnd (text, 7), "isWordEnd (\"Hello, world.\", 7) -> false");
|
||||||
|
t.notok (isWordEnd (text, 8), "isWordEnd (\"Hello, world.\", 8) -> false");
|
||||||
|
t.notok (isWordEnd (text, 9), "isWordEnd (\"Hello, world.\", 9) -> false");
|
||||||
|
t.notok (isWordEnd (text, 10), "isWordEnd (\"Hello, world.\", 10) -> false");
|
||||||
|
t.ok (isWordEnd (text, 11), "isWordEnd (\"Hello, world.\", 11) -> true");
|
||||||
|
t.notok (isWordEnd (text, 12), "isWordEnd (\"Hello, world.\", 12) -> false");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,8 +65,8 @@ qx{../task rc:utf8.rc add project:Çirçös utf8 in project};
|
||||||
$output = qx{../task rc:utf8.rc ls project:Çirçös};
|
$output = qx{../task rc:utf8.rc ls project:Çirçös};
|
||||||
like ($output, qr/Çirçös.+utf8 in project/, 'utf8 in project works');
|
like ($output, qr/Çirçös.+utf8 in project/, 'utf8 in project works');
|
||||||
|
|
||||||
qx{../task rc:utf8.rc add utf8 in tag +☺};
|
qx{../task rc:utf8.rc add utf8 in tag +Zwölf};
|
||||||
$output = qx{../task rc:utf8.rc ls +☺};
|
$output = qx{../task rc:utf8.rc ls +Zwölf};
|
||||||
like ($output, qr/utf8 in tag/, 'utf8 in tag works');
|
like ($output, qr/utf8 in tag/, 'utf8 in tag works');
|
||||||
|
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
|
|
42
src/text.cpp
42
src/text.cpp
|
@ -420,3 +420,45 @@ bool noVerticalSpace (const std::string& input)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Input: hello, world
|
||||||
|
// Result for pos: y......y....
|
||||||
|
bool isWordStart (const std::string& input, std::string::size_type pos)
|
||||||
|
{
|
||||||
|
// Short circuit: no input means no word start.
|
||||||
|
if (input.length () == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If pos is the first alphanumeric character of the string.
|
||||||
|
if (pos == 0 && isalnum (input[pos]))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If pos is not the first alphanumeric character, but there is a preceding
|
||||||
|
// non-alphanumeric character.
|
||||||
|
if (pos > 0 && isalnum (input[pos]) && !isalnum (input[pos - 1]))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Input: hello, world
|
||||||
|
// Result for pos: ....y......y
|
||||||
|
bool isWordEnd (const std::string& input, std::string::size_type pos)
|
||||||
|
{
|
||||||
|
// Short circuit: no input means no word start.
|
||||||
|
if (input.length () == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If pos is the last alphanumeric character of the string.
|
||||||
|
if (pos == input.length () - 1 && isalnum (input[pos]))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If pos is not the last alphanumeric character, but there is a following
|
||||||
|
// non-alphanumeric character.
|
||||||
|
if (pos < input.length () - 1 && isalnum (input[pos]) && !isalnum (input[pos + 1]))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -52,6 +52,8 @@ void guess (const std::string&, std::vector<std::string>&, std::string&);
|
||||||
bool digitsOnly (const std::string&);
|
bool digitsOnly (const std::string&);
|
||||||
bool noSpaces (const std::string&);
|
bool noSpaces (const std::string&);
|
||||||
bool noVerticalSpace (const std::string&);
|
bool noVerticalSpace (const std::string&);
|
||||||
|
bool isWordStart (const std::string&, std::string::size_type);
|
||||||
|
bool isWordEnd (const std::string&, std::string::size_type);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue