Clementine-audio-player-Mac.../src/playlist/playlistfilterparser.cpp

544 lines
15 KiB
C++

/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "playlistfilterparser.h"
#include <QAbstractItemModel>
#include "core/logging.h"
#include "playlist.h"
class SearchTermComparator {
public:
virtual ~SearchTermComparator() {}
virtual bool Matches(const QString& element) const = 0;
};
// "compares" by checking if the field contains the search term
class DefaultComparator : public SearchTermComparator {
public:
explicit DefaultComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element.contains(search_term_);
}
private:
QString search_term_;
};
class EqComparator : public SearchTermComparator {
public:
explicit EqComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return search_term_ == element;
}
private:
QString search_term_;
};
class NeComparator : public SearchTermComparator {
public:
explicit NeComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return search_term_ != element;
}
private:
QString search_term_;
};
class LexicalGtComparator : public SearchTermComparator {
public:
explicit LexicalGtComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element > search_term_;
}
private:
QString search_term_;
};
class LexicalGeComparator : public SearchTermComparator {
public:
explicit LexicalGeComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element >= search_term_;
}
private:
QString search_term_;
};
class LexicalLtComparator : public SearchTermComparator {
public:
explicit LexicalLtComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element < search_term_;
}
private:
QString search_term_;
};
class LexicalLeComparator : public SearchTermComparator {
public:
explicit LexicalLeComparator(const QString& value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element <= search_term_;
}
private:
QString search_term_;
};
class GtComparator : public SearchTermComparator {
public:
explicit GtComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element.toInt() > search_term_;
}
private:
int search_term_;
};
class GeComparator : public SearchTermComparator {
public:
explicit GeComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element.toInt() >= search_term_;
}
private:
int search_term_;
};
class LtComparator : public SearchTermComparator {
public:
explicit LtComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element.toInt() < search_term_;
}
private:
int search_term_;
};
class LeComparator : public SearchTermComparator {
public:
explicit LeComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString& element) const {
return element.toInt() <= search_term_;
}
private:
int search_term_;
};
// The length field of the playlist (entries) contains a
// song's running time in nano seconds. However, We don't
// really care about nano seconds, just seconds. Thus, with
// this decorator we drop the last 9 digits, if that many
// are present.
class DropTailComparatorDecorator : public SearchTermComparator {
public:
explicit DropTailComparatorDecorator(SearchTermComparator* cmp) : cmp_(cmp) {}
virtual bool Matches(const QString& element) const {
if (element.length() > 9)
return cmp_->Matches(element.left(element.length() - 9));
else
return cmp_->Matches(element);
}
private:
QScopedPointer<SearchTermComparator> cmp_;
};
class RatingComparatorDecorator : public SearchTermComparator {
public:
explicit RatingComparatorDecorator(SearchTermComparator* cmp) : cmp_(cmp) {}
virtual bool Matches(const QString& element) const {
return cmp_->Matches(
QString::number(static_cast<int>(element.toDouble() * 10.0 + 0.5)));
}
private:
QScopedPointer<SearchTermComparator> cmp_;
};
// filter that applies a SearchTermComparator to all fields of a playlist entry
class FilterTerm : public FilterTree {
public:
explicit FilterTerm(SearchTermComparator* comparator,
const QList<int>& columns)
: cmp_(comparator), columns_(columns) {}
virtual bool accept(int row, const QModelIndex& parent,
const QAbstractItemModel* const model) const {
for (int i : columns_) {
QModelIndex idx(model->index(row, i, parent));
if (cmp_->Matches(idx.data().toString().toLower())) return true;
}
return false;
}
virtual FilterType type() { return Term; }
private:
QScopedPointer<SearchTermComparator> cmp_;
QList<int> columns_;
};
// filter that applies a SearchTermComparator to one specific field of a
// playlist entry
class FilterColumnTerm : public FilterTree {
public:
FilterColumnTerm(int column, SearchTermComparator* comparator)
: col(column), cmp_(comparator) {}
virtual bool accept(int row, const QModelIndex& parent,
const QAbstractItemModel* const model) const {
QModelIndex idx(model->index(row, col, parent));
return cmp_->Matches(idx.data().toString().toLower());
}
virtual FilterType type() { return Column; }
private:
int col;
QScopedPointer<SearchTermComparator> cmp_;
};
class NotFilter : public FilterTree {
public:
explicit NotFilter(const FilterTree* inv) : child_(inv) {}
virtual bool accept(int row, const QModelIndex& parent,
const QAbstractItemModel* const model) const {
return !child_->accept(row, parent, model);
}
virtual FilterType type() { return Not; }
private:
QScopedPointer<const FilterTree> child_;
};
class OrFilter : public FilterTree {
public:
~OrFilter() { qDeleteAll(children_); }
virtual void add(FilterTree* child) { children_.append(child); }
virtual bool accept(int row, const QModelIndex& parent,
const QAbstractItemModel* const model) const {
for (FilterTree* child : children_) {
if (child->accept(row, parent, model)) return true;
}
return false;
}
FilterType type() { return Or; }
private:
QList<FilterTree*> children_;
};
class AndFilter : public FilterTree {
public:
virtual ~AndFilter() { qDeleteAll(children_); }
virtual void add(FilterTree* child) { children_.append(child); }
virtual bool accept(int row, const QModelIndex& parent,
const QAbstractItemModel* const model) const {
for (FilterTree* child : children_) {
if (!child->accept(row, parent, model)) return false;
}
return true;
}
FilterType type() { return And; }
private:
QList<FilterTree*> children_;
};
FilterParser::FilterParser(const QString& filter,
const QMap<QString, int>& columns,
const QSet<int>& numerical_cols)
: filterstring_(filter),
columns_(columns),
numerical_columns_(numerical_cols) {}
FilterTree* FilterParser::parse() {
iter_ = filterstring_.constBegin();
end_ = filterstring_.constEnd();
return parseOrGroup();
}
void FilterParser::advance() {
while (iter_ != end_ && iter_->isSpace()) {
++iter_;
}
}
FilterTree* FilterParser::parseOrGroup() {
advance();
if (iter_ == end_) return new NopFilter;
OrFilter* group = new OrFilter;
group->add(parseAndGroup());
advance();
while (checkOr()) {
group->add(parseAndGroup());
advance();
}
return group;
}
FilterTree* FilterParser::parseAndGroup() {
advance();
if (iter_ == end_) return new NopFilter;
AndFilter* group = new AndFilter();
do {
group->add(parseSearchExpression());
advance();
if (iter_ != end_ && *iter_ == QChar(')')) break;
if (checkOr(false)) {
break;
}
checkAnd(); // if there's no 'AND', we'll add the term anyway...
} while (iter_ != end_);
return group;
}
bool FilterParser::checkAnd() {
if (iter_ != end_) {
if (*iter_ == QChar('A')) {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && *iter_ == QChar('N')) {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && *iter_ == QChar('D')) {
buf_ += *iter_;
iter_++;
if (iter_ != end_ &&
(iter_->isSpace() || *iter_ == QChar('-') || *iter_ == '(')) {
advance();
buf_.clear();
return true;
}
}
}
}
}
return false;
}
bool FilterParser::checkOr(bool step_over) {
if (!buf_.isEmpty()) {
if (buf_ == "OR") {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
} else {
if (iter_ != end_) {
if (*iter_ == 'O') {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && *iter_ == 'R') {
buf_ += *iter_;
iter_++;
if (iter_ != end_ &&
(iter_->isSpace() || *iter_ == '-' || *iter_ == '(')) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
}
}
}
return false;
}
FilterTree* FilterParser::parseSearchExpression() {
advance();
if (iter_ == end_) return new NopFilter;
if (*iter_ == '(') {
iter_++;
advance();
FilterTree* tree = parseOrGroup();
advance();
if (iter_ != end_) {
if (*iter_ == ')') {
++iter_;
}
}
return tree;
} else if (*iter_ == '-') {
++iter_;
FilterTree* tree = parseSearchExpression();
if (tree->type() != FilterTree::Nop) return new NotFilter(tree);
return tree;
} else {
return parseSearchTerm();
}
}
FilterTree* FilterParser::parseSearchTerm() {
QString col;
QString search;
QString prefix;
bool inQuotes = false;
for (; iter_ != end_; ++iter_) {
if (inQuotes) {
if (*iter_ == '"')
inQuotes = false;
else
buf_ += *iter_;
} else {
if (*iter_ == '"') {
inQuotes = true;
} else if (col.isEmpty() && *iter_ == ':') {
col = buf_.toLower();
buf_.clear();
prefix.clear(); // prefix isn't allowed here - let's ignore it
} else if (iter_->isSpace() || *iter_ == '(' || *iter_ == ')' ||
*iter_ == '-') {
break;
} else if (buf_.isEmpty()) {
// we don't know whether there is a column part in this search term
// thus we assume the latter and just try and read a prefix
if (prefix.isEmpty() && (*iter_ == '>' || *iter_ == '<' ||
*iter_ == '=' || *iter_ == '!')) {
prefix += *iter_;
} else if (prefix != "=" && *iter_ == '=') {
prefix += *iter_;
} else {
buf_ += *iter_;
}
} else {
buf_ += *iter_;
}
}
}
search = buf_.toLower();
buf_.clear();
return createSearchTermTreeNode(col, prefix, search);
}
FilterTree* FilterParser::createSearchTermTreeNode(
const QString& col, const QString& prefix, const QString& search) const {
if (search.isEmpty() && prefix != "=") {
return new NopFilter;
}
// here comes a mess :/
// well, not that much of a mess, but so many options -_-
SearchTermComparator* cmp = nullptr;
if (prefix == "!=" || prefix == "<>") {
cmp = new NeComparator(search);
} else if (!col.isEmpty() && columns_.contains(col) &&
numerical_columns_.contains(columns_[col])) {
// the length column contains the time in seconds (nano seconds, actually -
// the "nano" part is handled by the DropTailComparatorDecorator, though).
int search_value;
if (columns_[col] == Playlist::Column_Length) {
search_value = parseTime(search);
} else if (columns_[col] == Playlist::Column_Rating) {
search_value = static_cast<int>(search.toDouble() * 2.0 + 0.5);
} else {
search_value = search.toInt();
}
// alright, back to deciding which comparator we'll use
if (prefix == ">") {
cmp = new GtComparator(search_value);
} else if (prefix == ">=") {
cmp = new GeComparator(search_value);
} else if (prefix == "<") {
cmp = new LtComparator(search_value);
} else if (prefix == "<=") {
cmp = new LeComparator(search_value);
} else {
// convert back because for time/rating
cmp = new EqComparator(QString::number(search_value));
}
} else {
if (prefix == "=") {
cmp = new EqComparator(search);
} else if (prefix == ">") {
cmp = new LexicalGtComparator(search);
} else if (prefix == ">=") {
cmp = new LexicalGeComparator(search);
} else if (prefix == "<") {
cmp = new LexicalLtComparator(search);
} else if (prefix == "<=") {
cmp = new LexicalLeComparator(search);
} else {
cmp = new DefaultComparator(search);
}
}
if (columns_.contains(col)) {
if (columns_[col] == Playlist::Column_Length) {
cmp = new DropTailComparatorDecorator(cmp);
} else if (columns_[col] == Playlist::Column_Rating) {
cmp = new RatingComparatorDecorator(cmp);
}
return new FilterColumnTerm(columns_[col], cmp);
} else {
return new FilterTerm(cmp, columns_.values());
}
}
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
// and return the number of seconds if it parses correctly.
// If not, the original string is returned.
// The 'h', 'm' and 's' components can have any length (including 0).
//
// A few examples:
// "::" is parsed to "0"
// "1::" is parsed to "3600"
// "3:45" is parsed to "225"
// "1:165" is parsed to "225"
// "225" is parsed to "225" (srsly! ^.^)
// "2:3:4:5" is parsed to "2:3:4:5"
// "25m" is parsed to "25m"
int FilterParser::parseTime(const QString& time_str) const {
int seconds = 0;
int accum = 0;
int colon_count = 0;
for (const QChar& c : time_str) {
if (c.isDigit()) {
accum = accum * 10 + c.digitValue();
} else if (c == ':') {
seconds = seconds * 60 + accum;
accum = 0;
++colon_count;
if (colon_count > 2) {
return 0;
}
} else if (!c.isSpace()) {
return 0;
}
}
seconds = seconds * 60 + accum;
return seconds;
}