commit 74d8f21c18e1d4e57691c3915c833f05a09d359b
parent 1c2edf1cea780158d4da814ecc5d44479d3514e8
Author: cfillion <cfillion@users.noreply.github.com>
Date: Thu, 1 Sep 2016 02:54:21 -0400
filter: implement ^ and $ anchors to match start and end of string
Diffstat:
4 files changed, 162 insertions(+), 36 deletions(-)
diff --git a/src/browser.cpp b/src/browser.cpp
@@ -808,7 +808,7 @@ bool Browser::match(const Entry &entry) const
const string &author = getValue(AuthorColumn, entry);
const string &remote = getValue(RemoteColumn, entry);
- return m_filter.match(name + category + author + remote);
+ return m_filter.match({name, category, author, remote});
}
auto Browser::getEntry(const int listIndex) -> Entry *
diff --git a/src/filter.cpp b/src/filter.cpp
@@ -18,6 +18,7 @@
#include "filter.hpp"
#include <boost/algorithm/string.hpp>
+#include <unordered_set>
using namespace std;
@@ -25,49 +26,80 @@ void Filter::set(const string &input)
{
enum State { Default, DoubleQuote, SingleQuote };
- string buf;
+ Token token{};
State state = Default;
m_input = input;
- m_words.clear();
+ m_tokens.clear();
auto push = [&] {
state = Default;
- if(!buf.empty()) {
- m_words.push_back(buf);
- buf.clear();
+ size_t size = token.buf.size();
+
+ if(!size)
+ return;
+ if(size > 1 && token.buf[0] == '^') {
+ token.flags |= StartAnchorFlag;
+ token.buf.erase(0, 1);
+ size--;
+ }
+ if(size > 1 && token.buf[size - 1] == '$') {
+ token.flags |= EndAnchorFlag;
+ token.buf.erase(size - 1, 1);
+ size--;
}
+
+ m_tokens.emplace_back(token);
+ token = Token();
};
for(const char c : input) {
if(c == '"' && state != SingleQuote) {
if(state == DoubleQuote)
- push();
+ state = Default;
else
state = DoubleQuote;
}
else if(c == '\'' && state != DoubleQuote) {
if(state == SingleQuote)
- push();
+ state = Default;
else
state = SingleQuote;
}
else if(c == '\x20' && state == Default)
push();
else
- buf += c;
+ token.buf += c;
}
push();
}
-bool Filter::match(const string &str) const
+bool Filter::match(const vector<string> &rows) const
{
- for(const string &word : m_words) {
- if(!boost::icontains(str, word))
- return false;
+ unordered_set<const Token *> matches;
+
+ for(const string &str : rows) {
+ for(const Token &token : m_tokens) {
+ if(token.match(str))
+ matches.insert(&token);
+ }
}
- return true;
+ return matches.size() == m_tokens.size();
+}
+
+bool Filter::Token::match(const string &str) const
+{
+ bool match = true;
+
+ if(test(StartAnchorFlag))
+ match = match && boost::istarts_with(str, buf);
+ if(test(EndAnchorFlag))
+ match = match && boost::iends_with(str, buf);
+
+ match = match && boost::icontains(str, buf);
+
+ return match;
}
diff --git a/src/filter.hpp b/src/filter.hpp
@@ -29,15 +29,28 @@ public:
const std::string get() const { return m_input; }
void set(const std::string &);
- bool match(const std::string &) const;
+ bool match(const std::vector<std::string> &) const;
Filter &operator=(const std::string &f) { set(f); return *this; }
bool operator==(const std::string &f) const { return m_input == f; }
bool operator!=(const std::string &f) const { return !(*this == f); }
private:
+ enum Flag {
+ StartAnchorFlag = 1<<0,
+ EndAnchorFlag = 1<<1,
+ };
+
+ struct Token {
+ std::string buf;
+ int flags;
+
+ bool test(Flag f) const { return (flags & f) != 0; }
+ bool match(const std::string &) const;
+ };
+
std::string m_input;
- std::vector<std::string> m_words;
+ std::vector<Token> m_tokens;
};
#endif
diff --git a/test/filter.cpp b/test/filter.cpp
@@ -8,13 +8,13 @@ static const char *M = "[filter]";
TEST_CASE("basic matching", M) {
Filter f;
- REQUIRE(f.match("world"));
+ REQUIRE(f.match({"world"}));
f.set("hello");
- REQUIRE(f.match("hello"));
- REQUIRE(f.match("HELLO"));
- REQUIRE_FALSE(f.match("world"));
+ REQUIRE(f.match({"hello"}));
+ REQUIRE(f.match({"HELLO"}));
+ REQUIRE_FALSE(f.match({"world"}));
}
TEST_CASE("get/set filter", M) {
@@ -23,11 +23,11 @@ TEST_CASE("get/set filter", M) {
f.set("hello");
REQUIRE(f.get() == "hello");
- REQUIRE(f.match("hello"));
+ REQUIRE(f.match({"hello"}));
f.set("world");
REQUIRE(f.get() == "world");
- REQUIRE(f.match("world"));
+ REQUIRE(f.match({"world"}));
}
TEST_CASE("filter operators", M) {
@@ -52,28 +52,28 @@ TEST_CASE("word matching", M) {
Filter f;
f.set("hello world");
- REQUIRE_FALSE(f.match("hello"));
- REQUIRE(f.match("hello world"));
- REQUIRE(f.match("helloworld"));
- REQUIRE(f.match("hello test world"));
+ REQUIRE_FALSE(f.match({"hello"}));
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE(f.match({"helloworld"}));
+ REQUIRE(f.match({"hello test world"}));
}
TEST_CASE("double quote matching", M) {
Filter f;
f.set("\"hello world\"");
- REQUIRE(f.match("hello world"));
- REQUIRE_FALSE(f.match("helloworld"));
- REQUIRE_FALSE(f.match("hello test world"));
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE_FALSE(f.match({"helloworld"}));
+ REQUIRE_FALSE(f.match({"hello test world"}));
}
TEST_CASE("single quote matching", M) {
Filter f;
f.set("'hello world'");
- REQUIRE(f.match("hello world"));
- REQUIRE_FALSE(f.match("helloworld"));
- REQUIRE_FALSE(f.match("hello test world"));
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE_FALSE(f.match({"helloworld"}));
+ REQUIRE_FALSE(f.match({"hello test world"}));
}
TEST_CASE("mixing quotes", M) {
@@ -82,14 +82,95 @@ TEST_CASE("mixing quotes", M) {
SECTION("double in single") {
f.set("'hello \"world\"'");
- REQUIRE(f.match("hello \"world\""));
- REQUIRE_FALSE(f.match("hello world"));
+ REQUIRE(f.match({"hello \"world\""}));
+ REQUIRE_FALSE(f.match({"hello world"}));
}
SECTION("single in double") {
f.set("\"hello 'world'\"");
- REQUIRE(f.match("hello 'world'"));
- REQUIRE_FALSE(f.match("hello world"));
+ REQUIRE(f.match({"hello 'world'"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
}
}
+
+TEST_CASE("start of string", M) {
+ Filter f;
+
+ SECTION("normal") {
+ f.set("^hello");
+
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE_FALSE(f.match({"puts 'hello world'"}));
+ }
+
+ SECTION("middle") {
+ f.set("hel^lo");
+
+ REQUIRE(f.match({"hel^lo world"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
+ }
+
+ SECTION("single") {
+ f.set("^");
+ REQUIRE(f.match({"hel^lo world"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
+ }
+
+ SECTION("quote before") {
+ f.set("'^hello'");
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE_FALSE(f.match({"world hello"}));
+ }
+
+ SECTION("quote after") {
+ f.set("^'hello");
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE_FALSE(f.match({"world hello"}));
+ }
+}
+
+TEST_CASE("end of string", M) {
+ Filter f;
+
+ SECTION("normal") {
+ f.set("world$");
+
+ REQUIRE(f.match({"hello world"}));
+ REQUIRE_FALSE(f.match({"'hello world'.upcase"}));
+ }
+
+ SECTION("middle") {
+ f.set("hel$lo");
+
+ REQUIRE(f.match({"hel$lo world"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
+ }
+
+ SECTION("single") {
+ f.set("$");
+ REQUIRE(f.match({"hel$lo world"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
+ }
+
+ SECTION("quote before") {
+ f.set("'hello'$");
+ REQUIRE(f.match({"hello"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
+ }
+
+ SECTION("quote after") {
+ f.set("'hello$'");
+ REQUIRE(f.match({"hello"}));
+ REQUIRE_FALSE(f.match({"hello world"}));
+ }
+}
+
+TEST_CASE("row matching", M) {
+ Filter f;
+ f.set("hello world");
+
+ REQUIRE_FALSE(f.match({"hello"}));
+ REQUIRE(f.match({"hello", "world"}));
+ REQUIRE(f.match({"hello", "test", "world"}));
+}