Add starts_with to filter expressions
This commit is contained in:
parent
512cd7b0de
commit
868a06eaf9
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]]
|
||||||
|
|
|
@ -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")));
|
||||||
|
|
Loading…
Reference in New Issue