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 }