From 9263d6d07db61cb7fd3e4b132a37c10a9cfa1a37 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Tue, 24 Jul 2018 23:24:42 +0200
Subject: [PATCH] SongFilter: implement operator "!="

Closes #89
---
 NEWS               |  2 +-
 doc/protocol.xml   |  4 ++++
 src/SongFilter.cxx | 14 +++++++++-----
 3 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/NEWS b/NEWS
index 839bf29d8..653fee008 100644
--- a/NEWS
+++ b/NEWS
@@ -7,7 +7,7 @@ ver 0.21 (not yet released)
   - "outputs" prints the plugin name
   - "outputset" sets runtime attributes
   - close connection when client sends HTTP request
-  - new filter syntax for "find"/"search" etc.
+  - new filter syntax for "find"/"search" etc. with negation
 * database
   - simple: scan audio formats
 * player
diff --git a/doc/protocol.xml b/doc/protocol.xml
index 3fcbae71b..23584b5f0 100644
--- a/doc/protocol.xml
+++ b/doc/protocol.xml
@@ -222,6 +222,10 @@
             "<code>(TAG == 'VALUE')</code>": match a tag value.
           </para>
 
+          <para>
+            "<code>(TAG != 'VALUE')</code>": mismatch a tag value.
+          </para>
+
           <para>
             The special tag "<parameter>any</parameter>" checks all
             tag values.
diff --git a/src/SongFilter.cxx b/src/SongFilter.cxx
index 17867a082..1d691fb67 100644
--- a/src/SongFilter.cxx
+++ b/src/SongFilter.cxx
@@ -81,7 +81,7 @@ SongFilter::Item::ToExpression() const noexcept
 {
 	switch (tag) {
 	case LOCATE_TAG_FILE_TYPE:
-		return "(" LOCATE_TAG_FILE_KEY " == \"" + value + "\")";
+		return std::string("(" LOCATE_TAG_FILE_KEY " ") + (IsNegated() ? "!=" : "==") + " \"" + value + "\")";
 
 	case LOCATE_TAG_BASE_TYPE:
 		return "(base \"" + value + "\")";
@@ -90,10 +90,10 @@ SongFilter::Item::ToExpression() const noexcept
 		return "(modified-since \"" + value + "\")";
 
 	case LOCATE_TAG_ANY_TYPE:
-		return "(" LOCATE_TAG_ANY_KEY " == \"" + value + "\")";
+		return std::string("(" LOCATE_TAG_ANY_KEY " ") + (IsNegated() ? "!=" : "==") + " \"" + value + "\")";
 
 	default:
-		return std::string("(") + tag_item_names[tag] + " == \"" + value + "\")";
+		return std::string("(") + tag_item_names[tag] + " " + (IsNegated() ? "!=" : "==") + " \"" + value + "\")";
 	}
 }
 
@@ -317,8 +317,11 @@ SongFilter::ParseExpression(const char *s, bool fold_case)
 		items.emplace_back(type, std::move(value), fold_case);
 		return StripLeft(s + 1);
 	} else {
-		if (s[0] != '=' || s[1] != '=')
-			throw std::runtime_error("'==' expected");
+		bool negated = false;
+		if (s[0] == '!' && s[1] == '=')
+			negated = true;
+		else if (s[0] != '=' || s[1] != '=')
+			throw std::runtime_error("'==' or '!=' expected");
 
 		s = StripLeft(s + 2);
 		auto value = ExpectQuoted(s);
@@ -326,6 +329,7 @@ SongFilter::ParseExpression(const char *s, bool fold_case)
 			throw std::runtime_error("')' expected");
 
 		items.emplace_back(type, std::move(value), fold_case);
+		items.back().SetNegated(negated);
 		return StripLeft(s + 1);
 	}
 }