File indexing completed on 2024-05-19 04:49:18

0001 /****************************************************************************************
0002  * Copyright (c) 2007 Alexandre Pereira de Oliveira <aleprj@gmail.com>                  *
0003  * Copyright (c) 2007-2009 Maximilian Kossick <maximilian.kossick@googlemail.com>       *
0004  * Copyright (c) 2007 Nikolaj Hald Nielsen <nhn@kde.org>                                *
0005  * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de>                                  *
0006  *                                                                                      *
0007  * This program is free software; you can redistribute it and/or modify it under        *
0008  * the terms of the GNU General Public License as published by the Free Software        *
0009  * Foundation; either version 2 of the License, or (at your option) any later           *
0010  * version.                                                                             *
0011  *                                                                                      *
0012  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0013  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0014  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0015  *                                                                                      *
0016  * You should have received a copy of the GNU General Public License along with         *
0017  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0018  ****************************************************************************************/
0019 
0020 #define DEBUG_PREFIX "TextualQueryFilter"
0021 
0022 #include "TextualQueryFilter.h"
0023 #include "Expression.h"
0024 
0025 #include "FileType.h"
0026 #include "core/support/Debug.h"
0027 
0028 #include <KLocalizedString>
0029 
0030 using namespace Meta;
0031 
0032 
0033 #define ADD_OR_EXCLUDE_FILTER( VALUE, FILTER, MATCHBEGIN, MATCHEND ) \
0034             { if( elem.negate ) \
0035                 qm->excludeFilter( VALUE, FILTER, MATCHBEGIN, MATCHEND ); \
0036             else \
0037                 qm->addFilter( VALUE, FILTER, MATCHBEGIN, MATCHEND ); }
0038 #define ADD_OR_EXCLUDE_NUMBER_FILTER( VALUE, FILTER, COMPARE ) \
0039             { if( elem.negate ) \
0040                 qm->excludeNumberFilter( VALUE, FILTER, COMPARE ); \
0041             else \
0042                 qm->addNumberFilter( VALUE, FILTER, COMPARE ); }
0043 
0044 void
0045 Collections::addTextualFilter( Collections::QueryMaker *qm, const QString &filter )
0046 {
0047     const int validFilters = qm->validFilterMask();
0048 
0049     ParsedExpression parsed = ExpressionParser::parse( filter );
0050     foreach( const or_list &orList, parsed )
0051     {
0052         qm->beginOr();
0053 
0054         foreach( const expression_element &elem, orList )
0055         {
0056             if( elem.negate )
0057                 qm->beginAnd();
0058             else
0059                 qm->beginOr();
0060 
0061             if ( elem.field.isEmpty() )
0062             {
0063                 qm->beginOr();
0064 
0065                 if( ( validFilters & Collections::QueryMaker::TitleFilter ) )
0066                     ADD_OR_EXCLUDE_FILTER( Meta::valTitle, elem.text, false, false );
0067                 if( ( validFilters & Collections::QueryMaker::UrlFilter ) )
0068                     ADD_OR_EXCLUDE_FILTER( Meta::valUrl, elem.text, false, false );
0069                 if( ( validFilters & Collections::QueryMaker::AlbumFilter ) )
0070                     ADD_OR_EXCLUDE_FILTER( Meta::valAlbum, elem.text, false, false );
0071                 if( ( validFilters & Collections::QueryMaker::ArtistFilter ) )
0072                     ADD_OR_EXCLUDE_FILTER( Meta::valArtist, elem.text, false, false );
0073                 if( ( validFilters & Collections::QueryMaker::AlbumArtistFilter ) )
0074                     ADD_OR_EXCLUDE_FILTER( Meta::valAlbumArtist, elem.text, false, false );
0075                 if( ( validFilters & Collections::QueryMaker::ComposerFilter ) )
0076                     ADD_OR_EXCLUDE_FILTER( Meta::valComposer, elem.text, false, false );
0077                 if( ( validFilters & Collections::QueryMaker::GenreFilter ) )
0078                     ADD_OR_EXCLUDE_FILTER( Meta::valGenre, elem.text, false, false );
0079                 if( ( validFilters & Collections::QueryMaker::YearFilter ) )
0080                     ADD_OR_EXCLUDE_FILTER( Meta::valYear, elem.text, false, false );
0081 
0082                 ADD_OR_EXCLUDE_FILTER( Meta::valLabel, elem.text, false, false );
0083 
0084                 qm->endAndOr();
0085             }
0086             else
0087             {
0088                 //get field values based on name
0089                 const qint64 field = Meta::fieldForName( elem.field );
0090                 Collections::QueryMaker::NumberComparison compare = Collections::QueryMaker::Equals;
0091                 switch( elem.match )
0092                 {
0093                     case expression_element::More:
0094                         compare = Collections::QueryMaker::GreaterThan;
0095                         break;
0096                     case expression_element::Less:
0097                         compare = Collections::QueryMaker::LessThan;
0098                         break;
0099                     case expression_element::Equals:
0100                     case expression_element::Contains:
0101                         compare = Collections::QueryMaker::Equals;
0102                         break;
0103                 }
0104 
0105                 const bool matchEqual = ( elem.match == expression_element::Equals );
0106 
0107                 switch( field )
0108                 {
0109                     case Meta::valAlbum:
0110                         if( ( validFilters & Collections::QueryMaker::AlbumFilter ) == 0 ) break;
0111                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0112                         break;
0113                     case Meta::valArtist:
0114                         if( ( validFilters & Collections::QueryMaker::ArtistFilter ) == 0 ) break;
0115                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0116                         break;
0117                     case Meta::valAlbumArtist:
0118                         if( ( validFilters & Collections::QueryMaker::AlbumArtistFilter ) == 0 ) break;
0119                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0120                         break;
0121                     case Meta::valGenre:
0122                         if( ( validFilters & Collections::QueryMaker::GenreFilter ) == 0 ) break;
0123                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0124                         break;
0125                     case Meta::valTitle:
0126                         if( ( validFilters & Collections::QueryMaker::TitleFilter ) == 0 ) break;
0127                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0128                         break;
0129                     case Meta::valComposer:
0130                         if( ( validFilters & Collections::QueryMaker::ComposerFilter ) == 0 ) break;
0131                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0132                         break;
0133                     case Meta::valYear:
0134                         if( ( validFilters & Collections::QueryMaker::YearFilter ) == 0 ) break;
0135                         ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toInt(), compare );
0136                         break;
0137                     case Meta::valLabel:
0138                     case Meta::valComment:
0139                         ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual );
0140                         break;
0141                     case Meta::valUrl:
0142                         ADD_OR_EXCLUDE_FILTER( field, elem.text, false, false );
0143                         break;
0144                     case Meta::valBpm:
0145                     case Meta::valBitrate:
0146                     case Meta::valScore:
0147                     case Meta::valPlaycount:
0148                     case Meta::valSamplerate:
0149                     case Meta::valDiscNr:
0150                     case Meta::valTrackNr:
0151                         ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toInt(), compare );
0152                         break;
0153                     case Meta::valRating:
0154                         ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toFloat() * 2, compare );
0155                         break;
0156                     case Meta::valLength:
0157                         ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toInt() * 1000, compare );
0158                         break;
0159                     case Meta::valLastPlayed:
0160                     case Meta::valFirstPlayed:
0161                     case Meta::valCreateDate:
0162                     case Meta::valModified:
0163                         addDateFilter( field, compare, elem.negate, elem.text, qm );
0164                         break;
0165                     case Meta::valFilesize:
0166                     {
0167                         bool doubleOk( false );
0168                         const double mbytes = elem.text.toDouble( &doubleOk ); // input in MBs
0169                         if( !doubleOk )
0170                         {
0171                             qm->endAndOr();
0172                             return;
0173                         }
0174                         /*
0175                         * A special case is made for Equals (e.g. filesize:100), which actually filters
0176                         * for anything between 100 and 101MBs. Megabytes are used because for audio files
0177                         * they are the most reasonable units for the user to deal with.
0178                         */
0179                         const qreal bytes = mbytes * 1024.0 * 1024.0;
0180                         const qint64 mbFloor = qint64( qAbs(mbytes) );
0181                         switch( compare )
0182                         {
0183                         case Collections::QueryMaker::Equals:
0184                             qm->endAndOr();
0185                             qm->beginAnd();
0186                             ADD_OR_EXCLUDE_NUMBER_FILTER( field, mbFloor * 1024 * 1024, Collections::QueryMaker::GreaterThan );
0187                             ADD_OR_EXCLUDE_NUMBER_FILTER( field, (mbFloor + 1) * 1024 * 1024, Collections::QueryMaker::LessThan );
0188                             break;
0189                         case Collections::QueryMaker::GreaterThan:
0190                         case Collections::QueryMaker::LessThan:
0191                             ADD_OR_EXCLUDE_NUMBER_FILTER( field, bytes, compare );
0192                             break;
0193                         }
0194                         break;
0195                     }
0196                     case Meta::valFormat:
0197                     {
0198                         const QString &ftStr = elem.text;
0199                         Amarok::FileType ft = Amarok::FileTypeSupport::fileType(ftStr);
0200                         ADD_OR_EXCLUDE_NUMBER_FILTER( field, int(ft), compare );
0201                         break;
0202                     }
0203                 }
0204             }
0205             qm->endAndOr();
0206         }
0207         qm->endAndOr();
0208     }
0209 }
0210 
0211 void
0212 Collections::addDateFilter( qint64 field, Collections::QueryMaker::NumberComparison compare,
0213                             bool negate, const QString &text, Collections::QueryMaker *qm )
0214 {
0215     bool absolute = false;
0216     const uint date = semanticDateTimeParser( text, &absolute ).toSecsSinceEpoch();
0217     if( date == 0 )
0218         return;
0219 
0220     if( compare == Collections::QueryMaker::Equals )
0221     {
0222         // equal means, on the same day
0223         uint day = 24 * 60 * 60;
0224 
0225         qm->endAndOr();
0226         qm->beginAnd();
0227 
0228         if( negate )
0229         {
0230             qm->excludeNumberFilter( field, date - day, Collections::QueryMaker::GreaterThan );
0231             qm->excludeNumberFilter( field, date + day, Collections::QueryMaker::LessThan );
0232         }
0233         else
0234         {
0235             qm->addNumberFilter( field, date - day, Collections::QueryMaker::GreaterThan );
0236             qm->addNumberFilter( field, date + day, Collections::QueryMaker::LessThan );
0237         }
0238     }
0239     // note: if the date is a relative time difference, invert the condition
0240     else if( ( compare == Collections::QueryMaker::LessThan && !absolute ) || ( compare == Collections::QueryMaker::GreaterThan && absolute ) )
0241     {
0242         if( negate )
0243             qm->excludeNumberFilter( field, date, Collections::QueryMaker::GreaterThan );
0244         else
0245             qm->addNumberFilter( field, date, Collections::QueryMaker::GreaterThan );
0246     }
0247     else if( ( compare == Collections::QueryMaker::GreaterThan && !absolute ) || ( compare == Collections::QueryMaker::LessThan && absolute ) )
0248     {
0249         if( negate )
0250             qm->excludeNumberFilter( field, date, Collections::QueryMaker::LessThan );
0251         else
0252             qm->addNumberFilter( field, date, Collections::QueryMaker::LessThan );
0253     }
0254 }
0255 
0256 QDateTime
0257 Collections::semanticDateTimeParser( const QString &text, bool *absolute )
0258 {
0259     /* TODO: semanticDateTimeParser: has potential to extend and form a class of its own */
0260     // some code duplication, see EditFilterDialog::parseTextFilter
0261 
0262     const QString lowerText = text.toLower();
0263     const QDateTime curTime = QDateTime::currentDateTime();
0264 
0265     if( absolute )
0266         *absolute = false;
0267 
0268     // parse date using local settings
0269     QDateTime result = QLocale().toDateTime( text, QLocale::ShortFormat );
0270 
0271     // parse date using a backup standard independent from local settings
0272     QRegExp shortDateReg("(\\d{1,2})[-.](\\d{1,2})");
0273     QRegExp longDateReg("(\\d{1,2})[-.](\\d{1,2})[-.](\\d{4})");
0274 
0275     if( text.at(0).isLetter() )
0276     {
0277         if( ( lowerText.compare( QLatin1String("today") ) == 0 ) || ( lowerText.compare( i18n( "today" ) ) == 0 ) )
0278             result = curTime.addDays( -1 );
0279         else if( ( lowerText.compare( QLatin1String("last week") ) == 0 ) || ( lowerText.compare( i18n( "last week" ) ) == 0 ) )
0280             result = curTime.addDays( -7 );
0281         else if( ( lowerText.compare( QLatin1String("last month") ) == 0 ) || ( lowerText.compare( i18n( "last month" ) ) == 0 ) )
0282             result = curTime.addMonths( -1 );
0283         else if( ( lowerText.compare( QLatin1String("two months ago") ) == 0 ) || ( lowerText.compare( i18n( "two months ago" ) ) == 0 ) )
0284             result = curTime.addMonths( -2 );
0285         else if( ( lowerText.compare( QLatin1String("three months ago") ) == 0 ) || ( lowerText.compare( i18n( "three months ago" ) ) == 0 ) )
0286             result = curTime.addMonths( -3 );
0287     }
0288     else if( result.isValid() )
0289     {
0290         if( absolute )
0291             *absolute = true;
0292     }
0293     else if( text.contains(shortDateReg) )
0294     {
0295         result = QDate( QDate::currentDate().year(), shortDateReg.cap(2).toInt(), shortDateReg.cap(1).toInt() ).startOfDay();
0296         if( absolute )
0297             *absolute = true;
0298     }
0299     else if( text.contains(longDateReg) )
0300     {
0301         result = QDate( longDateReg.cap(3).toInt(), longDateReg.cap(2).toInt(), longDateReg.cap(1).toInt() ).startOfDay();
0302         if( absolute )
0303             *absolute = true;
0304     }
0305     else // first character is a number
0306     {
0307         // parse a "#m#d" (discoverability == 0, but without a GUI, how to do it?)
0308         int years = 0, months = 0, days = 0, secs = 0;
0309         QString tmp;
0310         for( int i = 0; i < text.length(); i++ )
0311         {
0312             QChar c = text.at( i );
0313             if( c.isNumber() )
0314             {
0315                 tmp += c;
0316             }
0317             else if( c == 'y' )
0318             {
0319                 years += -tmp.toInt();
0320                 tmp.clear();
0321             }
0322             else if( c == 'm' )
0323             {
0324                 months += -tmp.toInt();
0325                 tmp.clear();
0326             }
0327             else if( c == 'w' )
0328             {
0329                 days += -tmp.toInt() * 7;
0330                 tmp.clear();
0331             }
0332             else if( c == 'd' )
0333             {
0334                 days += -tmp.toInt();
0335                 tmp.clear();
0336             }
0337             else if( c == 'h' )
0338             {
0339                 secs += -tmp.toInt() * 60 * 60;
0340                 tmp.clear();
0341             }
0342             else if( c == 'M' )
0343             {
0344                 secs += -tmp.toInt() * 60;
0345                 tmp.clear();
0346             }
0347             else if( c == 's' )
0348             {
0349                 secs += -tmp.toInt();
0350                 tmp.clear();
0351             }
0352         }
0353         result = QDateTime::currentDateTime().addYears( years ).addMonths( months ).addDays( days ).addSecs( secs );
0354     }
0355     return result;
0356 }
0357