From 1c2cf3ac4036cb6a41ed8f34a284f428af19fc18 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Mon, 17 Nov 2025 11:00:01 +0100 Subject: [PATCH] Search: use date filter using QDate to replace current text filter --- src/library/searchquery.cpp | 111 ++++++++++++++++++++++++++++++ src/library/searchquery.h | 19 +++++ src/library/searchqueryparser.cpp | 3 +- 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/library/searchquery.cpp b/src/library/searchquery.cpp index 2cf21488faa..839f5079623 100644 --- a/src/library/searchquery.cpp +++ b/src/library/searchquery.cpp @@ -1,5 +1,6 @@ #include "library/searchquery.h" +#include #include #include "library/dao/trackschema.h" @@ -809,3 +810,113 @@ QString YearFilterNode::toSql() const { return QString(); } + +// TODO Convert to DateFilterNode and allow searching for "last_played" +DateAddedFilterNode::DateAddedFilterNode(const QString& argument) + : m_operatorQuery(false), + m_operator("=") { + QDateTime date; + QRegularExpressionMatch opMatch = kNumericOperatorRegex.match(argument); + if (opMatch.hasMatch()) { + // Explicit operator + m_operator = opMatch.captured(1); + date = parseDate(opMatch.captured(2)); + } else { + // This is an implicit 'equals' filter with ':'. + // Try parsing the date and use default '=' operator. + date = parseDate(argument); + } + + if (!date.isValid()) { + return; + } + + if (m_operator == '=') { + // Note: due to literal = time comparison in Sql we need to set up + // a range from [date]T00:00:00.000 to [date]T23:59:59.999 + m_equalsQuery = true; + m_dateStart = date; + m_dateEnd = date.addDays(1); + return; + } + + m_operatorQuery = true; + if (m_operator == '>') { + // "added:>25.10.2025" would include any time from 2025-10-25T00:00:00Z001 + // Add one day for >= 25.10.2026T00:00:00Z000 to exclude the whole day + date = date.addDays(1); + m_operator = ">="; + } + + m_opDate = date; + + // TODO Add literal parser, for example: + // added:7days + // added:4weeks-2weeks etc. +} + +QDateTime DateAddedFilterNode::parseDate(const QString& dateStr) const { + // Prior to Qt 6.7 QLocale::toDate() with QLocale::ShortFormat used the + // base year 1900. With 6.7+ we can specify the century, ie. 20 for 2000. + // Mixxx was created aftre 2000 :) +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + QDate date = QLocale().toDate(dateStr, QLocale::ShortFormat); +#else + QDate date = QLocale().toDate(dateStr, QLocale::ShortFormat, 20); +#endif + if (!date.isValid()) { + return {}; + } + + if (date.year() < 2000) { + date = date.addYears(100); + } + + // Return local date/time, don't convert to UTC, yet + return QDateTime(date, QTime(0, 0)).toUTC(); +} + +QString DateAddedFilterNode::dateStringIsoNoZ(const QDateTime& date) const { + QString dateStr = date.toString(Qt::ISODateWithMs); + // sqlite can't handle 'Z' suffix, strip if present + if (dateStr.endsWith('Z')) { + dateStr.chop(1); + } + return dateStr; +} + +bool DateAddedFilterNode::match(const TrackPointer& pTrack) const { + if (!m_operatorQuery && !m_equalsQuery) { + // invalid query, don't filter + return true; + } + + QDateTime trackDate = pTrack->getDateAdded(); + if (!trackDate.isValid()) { + return true; + } + + if (m_operatorQuery && + ((m_operator == "<" && trackDate < m_opDate) || + (m_operator == "<=" && trackDate <= m_opDate) || + (m_operator == ">=" && trackDate >= m_opDate))) { + return true; + } else if (m_equalsQuery && trackDate >= m_dateStart && trackDate < m_dateEnd) { + return true; + } + + // valid query with no matches + return false; +} + +QString DateAddedFilterNode::toSql() const { + if (m_operatorQuery) { + return QStringLiteral("datetime_added %1 '%2'") + .arg(m_operator, dateStringIsoNoZ(m_opDate)); + } else if (m_equalsQuery) { + return QStringLiteral("datetime_added >= '%1' AND datetime_added < '%2'") + .arg(dateStringIsoNoZ(m_dateStart), dateStringIsoNoZ(m_dateEnd)); + } + + return QString(); +} diff --git a/src/library/searchquery.h b/src/library/searchquery.h index b6a55305572..c1019daad94 100644 --- a/src/library/searchquery.h +++ b/src/library/searchquery.h @@ -1,6 +1,7 @@ #ifndef SEARCHQUERY_H #define SEARCHQUERY_H +#include #include #include #include @@ -273,4 +274,22 @@ class YearFilterNode : public NumericFilterNode { QString toSql() const override; }; +class DateAddedFilterNode : public QueryNode { + public: + DateAddedFilterNode(const QString& argument); + bool match(const TrackPointer& pTrack) const override; + QString toSql() const override; + + private: + QDateTime parseDate(const QString& dateStr) const; + QString dateStringIsoNoZ(const QDateTime& date) const; + + bool m_operatorQuery; + bool m_equalsQuery; + QString m_operator; + QDateTime m_opDate; + QDateTime m_dateStart; + QDateTime m_dateEnd; +}; + #endif /* SEARCHQUERY_H */ diff --git a/src/library/searchqueryparser.cpp b/src/library/searchqueryparser.cpp index 519b2c4ef83..ae254747ceb 100644 --- a/src/library/searchqueryparser.cpp +++ b/src/library/searchqueryparser.cpp @@ -255,8 +255,7 @@ void SearchQueryParser::parseTokens(QStringList tokens, field == "added" || field == "dateadded") { field = "datetime_added"; - pNode = std::make_unique( - m_pTrackCollection->database(), m_fieldToSqlColumns[field], argument); + pNode = std::make_unique(argument); } else if (field == "bpm") { if (matchMode == StringMatch::Equals) { // restore = operator removed by getTextArgument()