File indexing completed on 2024-05-12 05:09:52
0001 /*************************************************************************** 0002 Copyright (C) 2007-2009 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "stringcomparison.h" 0026 #include "../fieldformat.h" 0027 #include "../tellico_debug.h" 0028 0029 #include <QDateTime> 0030 0031 using namespace Tellico; 0032 0033 namespace { 0034 int compareFloat(const QString& s1, const QString& s2) { 0035 bool ok1, ok2; 0036 float n1 = s1.toFloat(&ok1); 0037 if(!ok1) { 0038 return 0; 0039 } 0040 float n2 = s2.toFloat(&ok2); 0041 if(!ok2) { 0042 return 0; 0043 } 0044 return n1 > n2 ? 1 : (n1 < n2 ? -1 : 0); 0045 } 0046 } 0047 0048 Tellico::StringComparison* Tellico::StringComparison::create(Data::FieldPtr field_) { 0049 if(!field_) { 0050 myWarning() << "No field for creating a string comparison"; 0051 return nullptr; 0052 } 0053 if(field_->type() == Data::Field::Number || field_->type() == Data::Field::Rating) { 0054 return new NumberComparison(); 0055 } else if(field_->type() == Data::Field::Bool) { 0056 return new BoolComparison(); 0057 } else if(field_->type() == Data::Field::Date || field_->formatType() == FieldFormat::FormatDate) { 0058 return new ISODateComparison(); 0059 } else if(field_->formatType() == FieldFormat::FormatTitle) { 0060 return new TitleComparison(); 0061 } else if(field_->property(QStringLiteral("lcc")) == QLatin1String("true") || 0062 field_->name() == QLatin1String("lcc")) { 0063 // allow LCC comparison if LCC property is set, or if the name is lcc 0064 return new LCCComparison(); 0065 } 0066 return new StringComparison(); 0067 } 0068 0069 Tellico::StringComparison::StringComparison() { 0070 } 0071 0072 int Tellico::StringComparison::compare(const QString& str1_, const QString& str2_) { 0073 return str1_.localeAwareCompare(str2_); 0074 } 0075 0076 Tellico::BoolComparison::BoolComparison() : StringComparison() { 0077 } 0078 0079 int Tellico::BoolComparison::compare(const QString& str1_, const QString& str2_) { 0080 const bool b1 = str1_.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0 0081 || str1_ == QLatin1String("1"); 0082 const bool b2 = str2_.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0 0083 || str2_ == QLatin1String("1"); 0084 return b1 == b2 ? 0 : (b1 ? 1 : -1); 0085 } 0086 0087 Tellico::TitleComparison::TitleComparison() : StringComparison() { 0088 } 0089 0090 int Tellico::TitleComparison::compare(const QString& str1_, const QString& str2_) { 0091 // sortKeyTitle compares against the article list (which is already in lower-case) 0092 // additionally, we want lower case for localeAwareCompare 0093 const QString title1 = FieldFormat::sortKeyTitle(str1_.toLower()); 0094 const QString title2 = FieldFormat::sortKeyTitle(str2_.toLower()); 0095 const int ret = title1.localeAwareCompare(title2); 0096 return ret > 0 ? 1 : (ret < 0 ? -1 : 0); 0097 } 0098 0099 Tellico::NumberComparison::NumberComparison() : StringComparison() { 0100 } 0101 0102 int Tellico::NumberComparison::compare(const QString& str1_, const QString& str2_) { 0103 bool ok1, ok2; 0104 float num1 = 0, num2 = 0; 0105 0106 const QStringList values1 = FieldFormat::splitValue(str1_); 0107 const QStringList values2 = FieldFormat::splitValue(str2_); 0108 int index = 0; 0109 do { 0110 if((ok1 = index < values1.count())) { 0111 num1 = values1.at(index).toFloat(&ok1); 0112 } 0113 if((ok2 = index < values2.count())) { 0114 num2 = values2.at(index).toFloat(&ok2); 0115 } 0116 if(ok1 && ok2) { 0117 if(!qFuzzyCompare(num1, num2)) { 0118 const float ret = num1 - num2; 0119 // if abs(ret) < 0.5, we want to round up/down to -1 or 1 0120 // so that comparing 0.2 to 0.4 yields 1, for example, and not 0 0121 return ret < 0 ? qMin(-1, qRound(ret)) : qMax(1, qRound(ret)); 0122 } 0123 } 0124 ++index; 0125 } while(ok1 && ok2); 0126 0127 if(ok1 && !ok2) { 0128 return 1; 0129 } else if(!ok1 && ok2) { 0130 return -1; 0131 } 0132 return 0; 0133 } 0134 0135 // for details on the LCC comparison, see 0136 // http://www.mcgees.org/2001/08/08/sort-by-library-of-congress-call-number-in-perl/ 0137 // http://library.dts.edu/Pages/RM/Helps/lc_call.shtml 0138 0139 Tellico::LCCComparison::LCCComparison() : StringComparison(), 0140 m_regexp(QLatin1String("^([A-Z]+)" 0141 "(\\d+(?:\\.\\d+)?)" 0142 "\\.?([A-Z]*)" 0143 "(\\d*)" 0144 "\\.?([A-Z]*)" 0145 "(\\d*)" 0146 "(?: (.+))?")) { 0147 } 0148 0149 int Tellico::LCCComparison::compare(const QString& str1_, const QString& str2_) { 0150 if(str1_.isEmpty()) { 0151 return str2_.isEmpty() ? 0 : -1; 0152 } 0153 if(str2_.isEmpty()) { 0154 return 1; 0155 } 0156 // myDebug() << str1_ << " to " << str2_; 0157 QRegularExpressionMatch match1 = m_regexp.match(str1_); 0158 if(!match1.hasMatch()) { 0159 myDebug() << "no regexp match:" << str1_; 0160 return StringComparison::compare(str1_, str2_); 0161 } 0162 QRegularExpressionMatch match2 = m_regexp.match(str2_); 0163 if(!match2.hasMatch()) { 0164 myDebug() << "no regexp match:" << str2_; 0165 return StringComparison::compare(str1_, str2_); 0166 } 0167 QStringList cap1 = match1.capturedTexts(); 0168 QStringList cap2 = match2.capturedTexts(); 0169 // QRegularExpression doesn't include an empty string 0170 // in optional captured groups that don't exist 0171 while(cap1.size() < 8) { 0172 cap1 += QString(); 0173 } 0174 while(cap2.size() < 8) { 0175 cap2 += QString(); 0176 } 0177 return compareLCC(cap1, cap2); 0178 } 0179 0180 int Tellico::LCCComparison::compareLCC(const QStringList& cap1, const QStringList& cap2) const { 0181 0182 Q_ASSERT(cap1.size() == 8); 0183 Q_ASSERT(cap2.size() == 8); 0184 // the first item in the list is the full match, so start array index at 1 0185 int res = 0; 0186 return (res = cap1[1].compare(cap2[1])) != 0 ? res : 0187 (res = compareFloat(cap1[2], cap2[2])) != 0 ? res : 0188 (res = cap1[3].compare(cap2[3])) != 0 ? res : 0189 (res = compareFloat(QLatin1String("0.") + cap1[4], 0190 QLatin1String("0.") + cap2[4])) != 0 ? res : 0191 (res = cap1[5].compare(cap2[5])) != 0 ? res : 0192 (res = compareFloat(QLatin1String("0.") + cap1[6], 0193 QLatin1String("0.") + cap2[6])) != 0 ? res : 0194 (res = cap1[7].compare(cap2[7])) != 0 ? res : 0; 0195 } 0196 0197 Tellico::ISODateComparison::ISODateComparison() : StringComparison() { 0198 } 0199 0200 int Tellico::ISODateComparison::compare(const QString& str1, const QString& str2) { 0201 if(str1.isEmpty()) { 0202 return str2.isEmpty() ? 0 : -1; 0203 } 0204 if(str2.isEmpty()) { // str1 is not 0205 return 1; 0206 } 0207 // modelled after Field::formatDate() 0208 // so dates would sort as expected without padding month and day with zero 0209 // and accounting for "current year - 1 - 1" default scheme 0210 const QDate now = QDate::currentDate(); 0211 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) 0212 QStringList dlist1 = str1.split(QLatin1Char('-'), QString::KeepEmptyParts); 0213 #else 0214 QStringList dlist1 = str1.split(QLatin1Char('-'), Qt::KeepEmptyParts); 0215 #endif 0216 bool ok = true; 0217 int y1 = dlist1.count() > 0 ? dlist1[0].toInt(&ok) : now.year(); 0218 if(!ok) { 0219 y1 = now.year(); 0220 } 0221 int m1 = dlist1.count() > 1 ? dlist1[1].toInt(&ok) : 1; 0222 if(!ok) { 0223 m1 = 1; 0224 } 0225 int d1 = dlist1.count() > 2 ? dlist1[2].toInt(&ok) : 1; 0226 if(!ok) { 0227 d1 = 1; 0228 } 0229 QDate date1(y1, m1, d1); 0230 0231 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) 0232 QStringList dlist2 = str2.split(QLatin1Char('-'), QString::KeepEmptyParts); 0233 #else 0234 QStringList dlist2 = str2.split(QLatin1Char('-'), Qt::KeepEmptyParts); 0235 #endif 0236 int y2 = dlist2.count() > 0 ? dlist2[0].toInt(&ok) : now.year(); 0237 if(!ok) { 0238 y2 = now.year(); 0239 } 0240 int m2 = dlist2.count() > 1 ? dlist2[1].toInt(&ok) : 1; 0241 if(!ok) { 0242 m2 = 1; 0243 } 0244 int d2 = dlist2.count() > 2 ? dlist2[2].toInt(&ok) : 1; 0245 if(!ok) { 0246 d2 = 1; 0247 } 0248 QDate date2(y2, m2, d2); 0249 0250 if(date1 < date2) { 0251 return -1; 0252 } else if(date1 > date2) { 0253 return 1; 0254 } 0255 return 0; 0256 }