Add starts_with to filter expressions

This commit is contained in:
jcorporation 2022-09-27 19:45:15 +02:00
parent 512cd7b0de
commit 868a06eaf9
7 changed files with 92 additions and 19 deletions

View File

@ -196,6 +196,8 @@ of:
- ``(TAG contains 'VALUE')`` checks if the given value is a substring - ``(TAG contains 'VALUE')`` checks if the given value is a substring
of the tag value. of the tag value.
- ``(TAG starts_with 'VALUE')`` checks if the tag value starts with the given value.
- ``(TAG =~ 'VALUE')`` and ``(TAG !~ 'VALUE')`` use a Perl-compatible - ``(TAG =~ 'VALUE')`` and ``(TAG !~ 'VALUE')`` use a Perl-compatible
regular expression instead of doing a simple string comparison. regular expression instead of doing a simple string comparison.
(This feature is only available if :program:`MPD` was compiled with (This feature is only available if :program:`MPD` was compiled with

View File

@ -112,3 +112,32 @@ IcuCompare::IsIn(const char *haystack) const noexcept
return false; return false;
#endif #endif
} }
bool
IcuCompare::StartsWith(const char *haystack) const noexcept
{
#ifdef HAVE_ICU_CASE_FOLD
return StringIsEqual(IcuCaseFold(haystack).c_str(),
needle.c_str(), strlen(needle.c_str()));
#elif defined(_WIN32)
if (needle == nullptr)
/* the MultiByteToWideChar() call in the constructor
has failed, so let's always fail the comparison */
return false;
try {
auto w_haystack = MultiByteToWideChar(CP_UTF8, haystack);
return FindNLSStringEx(LOCALE_NAME_INVARIANT,
FIND_STARTSWITH|NORM_IGNORECASE,
w_haystack.c_str(), -1,
needle.c_str(), -1,
nullptr,
nullptr, nullptr, 0) == 0;
} catch (...) {
/* MultiByteToWideChar() has failed */
return false;
}
#else
return strncmp(haystack, needle.c_str(), strlen(needle.c_str()));
#endif
}

View File

@ -72,6 +72,9 @@ public:
[[gnu::pure]] [[gnu::pure]]
bool IsIn(const char *haystack) const noexcept; bool IsIn(const char *haystack) const noexcept;
[[gnu::pure]]
bool StartsWith(const char *haystack) const noexcept;
}; };
#endif #endif

View File

@ -91,7 +91,7 @@ SongFilter::SongFilter(TagType tag, const char *value, bool fold_case)
/* for compatibility with MPD 0.20 and older, "fold_case" also /* for compatibility with MPD 0.20 and older, "fold_case" also
switches on "substring" */ switches on "substring" */
and_filter.AddItem(std::make_unique<TagSongFilter>(tag, and_filter.AddItem(std::make_unique<TagSongFilter>(tag,
StringFilter(value, fold_case, fold_case, false))); StringFilter(value, fold_case, fold_case, false, false)));
} }
/* this destructor exists here just so it won't get inlined */ /* this destructor exists here just so it won't get inlined */
@ -209,13 +209,25 @@ ParseStringFilter(const char *&s, bool fold_case)
if (auto after_contains = StringAfterPrefixIgnoreCase(s, "contains ")) { if (auto after_contains = StringAfterPrefixIgnoreCase(s, "contains ")) {
s = StripLeft(after_contains); s = StripLeft(after_contains);
auto value = ExpectQuoted(s); auto value = ExpectQuoted(s);
return {std::move(value), fold_case, true, false}; return {std::move(value), fold_case, true, false, false};
} }
if (auto after_not_contains = StringAfterPrefixIgnoreCase(s, "!contains ")) { if (auto after_not_contains = StringAfterPrefixIgnoreCase(s, "!contains ")) {
s = StripLeft(after_not_contains); s = StripLeft(after_not_contains);
auto value = ExpectQuoted(s); auto value = ExpectQuoted(s);
return {std::move(value), fold_case, true, true}; return {std::move(value), fold_case, true, false, true};
}
if (auto after_starts_with = StringAfterPrefixIgnoreCase(s, "starts_with ")) {
s = StripLeft(after_starts_with);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, false, true, false};
}
if (auto after_not_starts_with = StringAfterPrefixIgnoreCase(s, "!starts_with ")) {
s = StripLeft(after_not_starts_with);
auto value = ExpectQuoted(s);
return {std::move(value), fold_case, false, true, true};
} }
bool negated = false; bool negated = false;
@ -225,7 +237,7 @@ ParseStringFilter(const char *&s, bool fold_case)
negated = s[0] == '!'; negated = s[0] == '!';
s = StripLeft(s + 2); s = StripLeft(s + 2);
auto value = ExpectQuoted(s); auto value = ExpectQuoted(s);
StringFilter f(std::move(value), fold_case, false, negated); StringFilter f(std::move(value), fold_case, false, false, negated);
f.SetRegex(std::make_shared<UniqueRegex>(f.GetValue().c_str(), f.SetRegex(std::make_shared<UniqueRegex>(f.GetValue().c_str(),
false, false, false, false,
fold_case)); fold_case));
@ -241,7 +253,7 @@ ParseStringFilter(const char *&s, bool fold_case)
s = StripLeft(s + 2); s = StripLeft(s + 2);
auto value = ExpectQuoted(s); auto value = ExpectQuoted(s);
return {std::move(value), fold_case, false, negated}; return {std::move(value), fold_case, false, false, negated};
} }
ISongFilterPtr ISongFilterPtr
@ -390,6 +402,7 @@ SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
and_filter.AddItem(std::make_unique<UriSongFilter>(StringFilter(value, and_filter.AddItem(std::make_unique<UriSongFilter>(StringFilter(value,
fold_case, fold_case,
fold_case, fold_case,
false,
false))); false)));
break; break;
@ -403,6 +416,7 @@ SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
StringFilter(value, StringFilter(value,
fold_case, fold_case,
fold_case, fold_case,
false,
false))); false)));
break; break;
} }

View File

@ -35,11 +35,15 @@ StringFilter::MatchWithoutNegation(const char *s) const noexcept
if (fold_case) { if (fold_case) {
return substring return substring
? fold_case.IsIn(s) ? fold_case.IsIn(s)
: fold_case == s; : (starts_with
? fold_case.StartsWith(s)
: fold_case == s);
} else { } else {
return substring return substring
? StringFind(s, value.c_str()) != nullptr ? StringFind(s, value.c_str()) != nullptr
: value == s; : (starts_with
? StringIsEqual(s, value.c_str(), value.length())
: value == s);
} }
} }

View File

@ -47,16 +47,21 @@ class StringFilter {
*/ */
bool substring; bool substring;
/**
* Search for substrings instead of matching the whole string?
*/
bool starts_with;
bool negated; bool negated;
public: public:
template<typename V> template<typename V>
StringFilter(V &&_value, bool _fold_case, bool _substring, bool _negated) StringFilter(V &&_value, bool _fold_case, bool _substring, bool _starts_with, bool _negated)
:value(std::forward<V>(_value)), :value(std::forward<V>(_value)),
fold_case(_fold_case fold_case(_fold_case
? IcuCompare(value) ? IcuCompare(value)
: IcuCompare()), : IcuCompare()),
substring(_substring), negated(_negated) {} substring(_substring), starts_with(_starts_with), negated(_negated) {}
bool empty() const noexcept { bool empty() const noexcept {
return value.empty(); return value.empty();
@ -98,7 +103,9 @@ public:
? (negated ? "!~" : "=~") ? (negated ? "!~" : "=~")
: (substring : (substring
? (negated ? "!contains" : "contains") ? (negated ? "!contains" : "contains")
: (negated ? "!=" : "==")); : (starts_with
? (negated ? "!starts_with" : "starts_with")
: (negated ? "!=" : "==")));
} }
[[gnu::pure]] [[gnu::pure]]

View File

@ -33,7 +33,7 @@ InvokeFilter(const TagSongFilter &f, const Tag &tag) noexcept
TEST(TagSongFilter, Basic) TEST(TagSongFilter, Basic)
{ {
const TagSongFilter f(TAG_TITLE, const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, false)); StringFilter("needle", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo", TAG_TITLE, "needle"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo", TAG_TITLE, "needle")));
@ -54,7 +54,7 @@ TEST(TagSongFilter, Basic)
TEST(TagSongFilter, Empty) TEST(TagSongFilter, Empty)
{ {
const TagSongFilter f(TAG_TITLE, const TagSongFilter f(TAG_TITLE,
StringFilter("", false, false, false)); StringFilter("", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag())); EXPECT_TRUE(InvokeFilter(f, MakeTag()));
@ -65,7 +65,7 @@ TEST(TagSongFilter, Empty)
TEST(TagSongFilter, Substring) TEST(TagSongFilter, Substring)
{ {
const TagSongFilter f(TAG_TITLE, const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, true, false)); StringFilter("needle", false, true, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needleBAR"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needleBAR")));
@ -76,10 +76,24 @@ TEST(TagSongFilter, Substring)
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "eedle"))); EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "eedle")));
} }
TEST(TagSongFilter, Startswith)
{
const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, true, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "needleBAR")));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "FOOneedle")));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "FOOneedleBAR")));
EXPECT_FALSE(InvokeFilter(f, MakeTag()));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "eedle")));
}
TEST(TagSongFilter, Negated) TEST(TagSongFilter, Negated)
{ {
const TagSongFilter f(TAG_TITLE, const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, true)); StringFilter("needle", false, false, false, true));
EXPECT_TRUE(InvokeFilter(f, MakeTag())); EXPECT_TRUE(InvokeFilter(f, MakeTag()));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle"))); EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle")));
@ -92,7 +106,7 @@ TEST(TagSongFilter, Negated)
TEST(TagSongFilter, EmptyNegated) TEST(TagSongFilter, EmptyNegated)
{ {
const TagSongFilter f(TAG_TITLE, const TagSongFilter f(TAG_TITLE,
StringFilter("", false, false, true)); StringFilter("", false, false, false, true));
EXPECT_FALSE(InvokeFilter(f, MakeTag())); EXPECT_FALSE(InvokeFilter(f, MakeTag()));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo")));
@ -104,7 +118,7 @@ TEST(TagSongFilter, EmptyNegated)
TEST(TagSongFilter, MultiNegated) TEST(TagSongFilter, MultiNegated)
{ {
const TagSongFilter f(TAG_TITLE, const TagSongFilter f(TAG_TITLE,
StringFilter("needle", false, false, true)); StringFilter("needle", false, false, false, true));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo", TAG_TITLE, "bar"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_TITLE, "foo", TAG_TITLE, "bar")));
EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle", TAG_TITLE, "bar"))); EXPECT_FALSE(InvokeFilter(f, MakeTag(TAG_TITLE, "needle", TAG_TITLE, "bar")));
@ -118,7 +132,7 @@ TEST(TagSongFilter, MultiNegated)
TEST(TagSongFilter, Fallback) TEST(TagSongFilter, Fallback)
{ {
const TagSongFilter f(TAG_ALBUM_ARTIST, const TagSongFilter f(TAG_ALBUM_ARTIST,
StringFilter("needle", false, false, false)); StringFilter("needle", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ALBUM_ARTIST, "needle"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ALBUM_ARTIST, "needle")));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ARTIST, "needle"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ARTIST, "needle")));
@ -138,7 +152,7 @@ TEST(TagSongFilter, Fallback)
TEST(TagSongFilter, EmptyFallback) TEST(TagSongFilter, EmptyFallback)
{ {
const TagSongFilter f(TAG_ALBUM_ARTIST, const TagSongFilter f(TAG_ALBUM_ARTIST,
StringFilter("", false, false, false)); StringFilter("", false, false, false, false));
EXPECT_TRUE(InvokeFilter(f, MakeTag())); EXPECT_TRUE(InvokeFilter(f, MakeTag()));
@ -152,7 +166,7 @@ TEST(TagSongFilter, EmptyFallback)
TEST(TagSongFilter, NegatedFallback) TEST(TagSongFilter, NegatedFallback)
{ {
const TagSongFilter f(TAG_ALBUM_ARTIST, const TagSongFilter f(TAG_ALBUM_ARTIST,
StringFilter("needle", false, false, true)); StringFilter("needle", false, false, false, true));
EXPECT_TRUE(InvokeFilter(f, MakeTag())); EXPECT_TRUE(InvokeFilter(f, MakeTag()));
EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ALBUM_ARTIST, "foo"))); EXPECT_TRUE(InvokeFilter(f, MakeTag(TAG_ALBUM_ARTIST, "foo")));