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