diff --git a/3rdparty/NaturalStringCompare2/NaturalStringCompare.cpp b/3rdparty/NaturalStringCompare2/NaturalStringCompare.cpp new file mode 100755 index 0000000..67d3551 --- /dev/null +++ b/3rdparty/NaturalStringCompare2/NaturalStringCompare.cpp @@ -0,0 +1,167 @@ +/* + * This software was written by people from OnShore Consulting services LLC + * and placed in the public domain. + * + * We reserve no legal rights to any of this. You are free to do + * whatever you want with it. And we make no guarantee or accept + * any claims on damages as a result of this. + * + * If you change the software, please help us and others improve the + * code by sending your modifications to us. If you choose to do so, + * your changes will be included under this license, and we will add + * your name to the list of contributors. +*/ + +#include "NaturalStringCompare.h" +#include + +#define INCBUF() { buffer += curr; ++pos; curr = ( pos < string.length() ) ? string[ pos ] : QChar(); } + +void ExtractToken( QString & buffer, const QString & string, int & pos, bool & isNumber ) +{ + buffer.clear(); + if ( string.isNull() || pos >= string.length() ) + return; + + isNumber = false; + QChar curr = string[ pos ]; + if ( curr == '-' || curr == '+' || curr.isDigit() ) + { + if ( curr == '-' || curr == '+' ) + INCBUF(); + + if ( !curr.isNull() && curr.isDigit() ) + { + isNumber = true; + while ( curr.isDigit() ) + INCBUF(); + + if ( curr == '.' ) + { + INCBUF(); + while ( curr.isDigit() ) + INCBUF(); + } + + if ( !curr.isNull() && curr.toLower() == 'e' ) + { + INCBUF(); + if ( curr == '-' || curr == '+' ) + INCBUF(); + + if ( curr.isNull() || !curr.isDigit() ) + isNumber = false; + else + while ( curr.isDigit() ) + INCBUF(); + } + } + } + + if ( !isNumber ) + { + while ( curr != '-' && curr != '+' && !curr.isDigit() && pos < string.length() ) + INCBUF(); + } +} + +int NaturalStringCompare( const QString & lhs, const QString & rhs, Qt::CaseSensitivity caseSensitive ) +{ + int ii = 0; + int jj = 0; + + QString lhsBufferQStr; + QString rhsBufferQStr; + + int retVal = 0; + + // all status values are created on the stack outside the loop to make as fast as possible + bool lhsNumber = false; + bool rhsNumber = false; + + double lhsValue = 0.0; + double rhsValue = 0.0; + bool ok1; + bool ok2; + + while ( retVal == 0 && ii < lhs.length() && jj < rhs.length() ) + { + ExtractToken( lhsBufferQStr, lhs, ii, lhsNumber ); + ExtractToken( rhsBufferQStr, rhs, jj, rhsNumber ); + + if ( !lhsNumber && !rhsNumber ) + { + // both strings curr val is a simple strcmp + retVal = lhsBufferQStr.compare( rhsBufferQStr, caseSensitive ); + + int maxLen = qMin( lhsBufferQStr.length(), rhsBufferQStr.length() ); + QString tmpRight = rhsBufferQStr.left( maxLen ); + QString tmpLeft = lhsBufferQStr.left( maxLen ); + if ( tmpLeft.compare( tmpRight, caseSensitive ) == 0 ) + { + retVal = lhsBufferQStr.length() - rhsBufferQStr.length(); + if ( retVal ) + { + QChar nextChar; + if ( ii < lhs.length() ) // more on the lhs + nextChar = lhs[ ii ]; + else if ( jj < rhs.length() ) // more on the rhs + nextChar = rhs[ jj ]; + + bool nextIsNum = ( nextChar == '-' || nextChar == '+' || nextChar.isDigit() ); + + if ( nextIsNum ) + retVal = -1*retVal; + } + } + } + else if ( lhsNumber && rhsNumber ) + { + // both numbers, convert and compare + lhsValue = lhsBufferQStr.toDouble( &ok1 ); + rhsValue = rhsBufferQStr.toDouble( &ok2 ); + if ( !ok1 || !ok2 ) + retVal = lhsBufferQStr.compare( rhsBufferQStr, caseSensitive ); + else if ( lhsValue > rhsValue ) + retVal = 1; + else if ( lhsValue < rhsValue ) + retVal = -1; + } + else + { + // completely arebitrary that a number comes before a string + retVal = lhsNumber ? -1 : 1; + } + } + + if ( retVal != 0 ) + return retVal; + if ( ii < lhs.length() ) + return -1; + else if ( jj < rhs.length() ) + return 1; + else + return 0; +} + +bool NaturalStringCompareLessThan( const QString & lhs, const QString & rhs ) +{ + return NaturalStringCompare( lhs, rhs, Qt::CaseSensitive ) < 0; +} + +bool NaturalStringCaseInsensitiveCompareLessThan( const QString & lhs, const QString & rhs ) +{ + return NaturalStringCompare( lhs, rhs, Qt::CaseInsensitive ) < 0; +} + +QStringList NaturalStringSort( const QStringList & list, Qt::CaseSensitivity caseSensitive ) +{ + QStringList retVal = list; + if ( caseSensitive == Qt::CaseSensitive ) + qSort( retVal.begin(), retVal.end(), NaturalStringCompareLessThan ); + else + qSort( retVal.begin(), retVal.end(), NaturalStringCaseInsensitiveCompareLessThan ); + return retVal; +} + + diff --git a/3rdparty/NaturalStringCompare2/NaturalStringCompare.h b/3rdparty/NaturalStringCompare2/NaturalStringCompare.h new file mode 100755 index 0000000..e19c061 --- /dev/null +++ b/3rdparty/NaturalStringCompare2/NaturalStringCompare.h @@ -0,0 +1,26 @@ +#ifndef __NATURALSTRINGCOMPARE_H +#define __NATURALSTRINGCOMPARE_H + +/* + * This software was written by people from OnShore Consulting services LLC + * and placed in the public domain. + * + * We reserve no legal rights to any of this. You are free to do + * whatever you want with it. And we make no guarantee or accept + * any claims on damages as a result of this. + * + * If you change the software, please help us and others improve the + * code by sending your modifications to us. If you choose to do so, + * your changes will be included under this license, and we will add + * your name to the list of contributors. +*/ + +#include +#include + +int NaturalStringCompare( const QString & lhs, const QString & rhs, Qt::CaseSensitivity caseSensitive=Qt::CaseSensitive ); +QStringList NaturalStringSort( const QStringList & list, Qt::CaseSensitivity caseSensitive=Qt::CaseSensitive ); +bool NaturalStringCompareLessThan( const QString & lhs, const QString & rhs ); +bool NaturalStringCaseInsensitiveCompareLessThan( const QString & lhs, const QString & rhs ); + +#endif diff --git a/3rdparty/NaturalStringCompare2/NaturalStringCompare.pro b/3rdparty/NaturalStringCompare2/NaturalStringCompare.pro new file mode 100755 index 0000000..0fc7ab3 --- /dev/null +++ b/3rdparty/NaturalStringCompare2/NaturalStringCompare.pro @@ -0,0 +1,16 @@ +###################################################################### +# Automatically generated by qmake (2.01a) Mon Feb 25 10:13:54 2008 +###################################################################### + +TEMPLATE = app +TARGET = +DEPENDPATH += . +INCLUDEPATH += . + +# Input +HEADERS += NaturalStringCompare.h +SOURCES += main.cpp NaturalStringCompare.cpp + +CONFIG += console qtestlib +QT-=gui + diff --git a/3rdparty/NaturalStringCompare2/main.cpp b/3rdparty/NaturalStringCompare2/main.cpp new file mode 100755 index 0000000..7f4a14f --- /dev/null +++ b/3rdparty/NaturalStringCompare2/main.cpp @@ -0,0 +1,181 @@ +/* + * This software was written by people from OnShore Consulting services LLC + * and placed in the public domain. + * + * We reserve no legal rights to any of this. You are free to do + * whatever you want with it. And we make no guarantee or accept + * any claims on damages as a result of this. + * + * If you change the software, please help us and others improve the + * code by sending your modifications to us. If you choose to do so, + * your changes will be included under this license, and we will add + * your name to the list of contributors. +*/ + +#include +#include +#include +#include "NaturalStringCompare.h" + +class CTestNaturalStringCompare : public QObject +{ + Q_OBJECT +private slots: + void compareString() + { + QString str1 = "abcdef"; + QString str2 = "aabc"; + QVERIFY( NaturalStringCompare(str1,str2) > 0 ); + QVERIFY( NaturalStringCompare(str2,str1) < 0 ); + QVERIFY( NaturalStringCompare(str1,str1) == 0 ); + QVERIFY( NaturalStringCompare(str2,str2) == 0 ); + + QVERIFY( NaturalStringCompare(str1,str1.left(2)) > 0 ); + QVERIFY( NaturalStringCompare(str1.left(2),str1) < 0 ); + } + + void compareInteger() + { + for( int ii = -15; ii <= 15; ii++ ) + { + QString currVal = QString( "%1" ).arg( ii ); + QString nextVal = QString( "%1" ).arg( ii+1 ); + + QVERIFY( NaturalStringCompare(currVal,nextVal) < 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal, "prefix" + nextVal) < 0 ); + QVERIFY( NaturalStringCompare( currVal + "postfix", nextVal + "postfix") < 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal + "postfix", "prefix" + nextVal + "postfix") < 0 ); + + QVERIFY( NaturalStringCompare(nextVal,currVal) > 0 ); + QVERIFY( NaturalStringCompare( "prefix" + nextVal, "prefix" + currVal) > 0 ); + QVERIFY( NaturalStringCompare( nextVal + "postfix", currVal + "postfix") > 0 ); + QVERIFY( NaturalStringCompare( "prefix" + nextVal + "postfix", "prefix" + currVal + "postfix") > 0 ); + + QVERIFY( NaturalStringCompare(currVal,currVal) == 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal, "prefix" + currVal) == 0 ); + QVERIFY( NaturalStringCompare( currVal + "postfix", currVal + "postfix") == 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal + "postfix", "prefix" + currVal + "postfix") == 0 ); + + QVERIFY( NaturalStringCompare( "prefix" + currVal + "middle" + currVal, "prefix" + currVal + "middle" + nextVal ) < 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal + "middle" + nextVal, "prefix" + currVal + "middle" + currVal ) > 0 ); + } + } + + void compareDouble() + { + for( int ii = -15; ii <= 15; ii++ ) + { + QString currVal = QString( "%1" ).arg( 1.0*ii - 0.05 ); + QString nextVal = QString( "%1" ).arg( 1.0*ii + 0.05 ); + + QVERIFY( NaturalStringCompare(currVal,nextVal) < 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal, "prefix" + nextVal) < 0 ); + QVERIFY( NaturalStringCompare( currVal + "postfix", nextVal + "postfix") < 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal + "postfix", "prefix" + nextVal + "postfix") < 0 ); + + QVERIFY( NaturalStringCompare(nextVal,currVal) > 0 ); + QVERIFY( NaturalStringCompare( "prefix" + nextVal, "prefix" + currVal) > 0 ); + QVERIFY( NaturalStringCompare( nextVal + "postfix", currVal + "postfix") > 0 ); + QVERIFY( NaturalStringCompare( "prefix" + nextVal + "postfix", "prefix" + currVal + "postfix") > 0 ); + + QVERIFY( NaturalStringCompare(currVal,currVal) == 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal, "prefix" + currVal) == 0 ); + QVERIFY( NaturalStringCompare( currVal + "postfix", currVal + "postfix") == 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal + "postfix", "prefix" + currVal + "postfix") == 0 ); + + QVERIFY( NaturalStringCompare( "prefix" + currVal + "middle" + currVal, "prefix" + currVal + "middle" + nextVal ) < 0 ); + QVERIFY( NaturalStringCompare( "prefix" + currVal + "middle" + nextVal, "prefix" + currVal + "middle" + currVal ) > 0 ); + } + } + + void sortStringList() + { + QStringList orig; + for( int ii = 15; ii >= -15; ii-- ) + { + QString currVal = QString( "%1" ).arg( 1.0*ii - 0.05 ); + orig << "prefix" + currVal + "postfix"; + orig << "Prefix" + currVal + "posTfIx"; + currVal = QString( "%1" ).arg( ii ); + orig << "Prefix" + currVal; + orig << currVal + "PostFIX"; + orig << currVal + "PostFix"; + } + + QStringList toBeSorted = orig; + qSort( toBeSorted.begin(), toBeSorted.end(), NaturalStringCompareLessThan ); + + + int cnt = toBeSorted.count(); + QVERIFY( toBeSorted.count() == 31 * 5 ); + QVERIFY( toBeSorted.front() == "-15PostFIX" ); + QVERIFY( toBeSorted.back() == "prefix14.95postfix" ); + + QStringList tmp = NaturalStringSort( orig ); + QVERIFY( tmp.count() == toBeSorted.count() ); + for( int ii = 0; ii < tmp.count(); ++ii ) + { + QVERIFY( tmp[ ii ] == toBeSorted[ ii ] ); + } + + toBeSorted = orig; + qSort( toBeSorted.begin(), toBeSorted.end(), NaturalStringCaseInsensitiveCompareLessThan ); + cnt = toBeSorted.count(); + QVERIFY( toBeSorted.count() == 31 * 5 ); + // QVERIFY( toBeSorted.front() == "-15PostFix" ); for case insensitive, PostFix vs PostFIX is non-deterministic which comes firs + // QVERIFY( toBeSorted.back() == "Prefix15" ); + + tmp = NaturalStringSort( orig, Qt::CaseInsensitive ); + QVERIFY( tmp.count() == toBeSorted.count() ); + for( int ii = 0; ii < tmp.count(); ++ii ) + { + QVERIFY( tmp[ ii ] == toBeSorted[ ii ] ); + } + } + + void lingfaYangTest() + { + // this is actually not what the spec wants, however, the 8 causes the first compare to be + // ppt/slides/slide vs ppt/slides/slide.xml which means embedded numbers will come first. + + QVERIFY( NaturalStringCompare( "ppt/slides/slide8.xml", "ppt/slides/slide.xml2", Qt::CaseInsensitive ) > 0 ); + + QStringList orderedList = QStringList() // from Ligfa Ya email + << "[Content_Types].xml" + << "_rels/.rels" + << "ppt/media/image8.wmf" + << "ppt/media/image9.jpeg" + << "ppt/media/image10.png" + << "ppt/media/image11.gif" + << "ppt/slides/_rels/slide9.xml.rels" + << "ppt/slides/_rels/slide10.xml.rels" + << "ppt/slides/slide.xml" + << "ppt/slides/slide8.xml" + << "PPT/SLIDES/SLIDE9.XML" + << "ppt/slides/slide10.xml" + << "ppt/slides/slide11.xml" + << "slide.xml" + ; + + QStringList toBeSorted = QStringList(); + QStringList tmp = orderedList; + qsrand( QDateTime::currentDateTime().toTime_t() ); + while( !tmp.isEmpty() ) + { + double randVal = qrand(); + randVal /= RAND_MAX; + int val = (int)( tmp.count() - 1 ) * randVal; + toBeSorted << tmp[ val ]; + tmp.removeAt( val ); + } + tmp = NaturalStringSort( toBeSorted, Qt::CaseInsensitive ); + for( int ii = 0; ii < tmp.count(); ++ii ) + { + QVERIFY( tmp[ ii ] == orderedList[ ii ] ); + } + } + +}; + +QTEST_MAIN(CTestNaturalStringCompare) +#include "main.moc" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5259d0d..1dce1fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 0.3.0 - ??/??/?? ================ +* Enrolling files/folders are now sorted naturally instead of alpha numerically * YouTubeFacesDBTransform implements Dr. Wolf's experimental protocol * NEC3 refactored diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d3cdd0..8416b14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,9 @@ find_package(OpenCV REQUIRED) set(BR_THIRDPARTY_LIBS ${BR_THIRDPARTY_LIBS} ${OpenCV_LIBS}) set(OPENCV_DEPENDENCIES calib3d contrib core features2d flann gpu highgui imgproc legacy ml nonfree objdetect photo stitching video videostab) +# Find NaturalStringCompare +find_package(NaturalStringCompare REQUIRED) + # Compiler flags if(UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-strict-overflow -fvisibility=hidden -fno-omit-frame-pointer") diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index 65b44a7..a8c2574 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -7,7 +7,7 @@ aux_source_directory(. SRC) aux_source_directory(core BR_CORE) include(plugins/plugins.cmake) -add_library(openbr SHARED ${SRC} ${BR_CORE} ${BR_PLUGIN} ${BR_THIRDPARTY_SRC} ${BR_RESOURCES}) +add_library(openbr SHARED ${SRC} ${BR_CORE} ${BR_PLUGIN} ${BR_THIRDPARTY_SRC} ${BR_RESOURCES} ${NATURALSTRINGCOMPARE_SRC}) qt5_use_modules(openbr ${QT_DEPENDENCIES}) set_target_properties(openbr PROPERTIES DEFINE_SYMBOL BR_LIBRARY diff --git a/sdk/core/qtutils.cpp b/sdk/core/qtutils.cpp index be837d1..8714e25 100644 --- a/sdk/core/qtutils.cpp +++ b/sdk/core/qtutils.cpp @@ -28,6 +28,7 @@ #include #include +#include "NaturalStringCompare.h" #include "qtutils.h" using namespace br; @@ -37,12 +38,12 @@ QStringList QtUtils::getFiles(QDir dir, bool recursive) dir = QDir(dir.canonicalPath()); QStringList files; - foreach (const QString &file, dir.entryList(QDir::Files)) + foreach (const QString &file, NaturalStringSort(dir.entryList(QDir::Files))) files.append(QDir::cleanPath(dir.absoluteFilePath(file))); if (!recursive) return files; - foreach (const QString &folder, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + foreach (const QString &folder, NaturalStringSort(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))) { QDir subdir(dir); bool success = subdir.cd(folder); if (!success) qFatal("cd failure."); files.append(getFiles(subdir, true)); diff --git a/sdk/plugins/gallery.cpp b/sdk/plugins/gallery.cpp index 9e7688e..7c07e57 100644 --- a/sdk/plugins/gallery.cpp +++ b/sdk/plugins/gallery.cpp @@ -24,6 +24,7 @@ #endif // BR_EMBEDDED #include +#include "NaturalStringCompare.h" #include "core/bee.h" #include "core/opencvutils.h" #include "core/qtutils.h" @@ -102,7 +103,7 @@ class EmptyGallery : public Gallery // Add immediate subfolders QDir dir(file); - foreach (const QString &folder, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) + foreach (const QString &folder, NaturalStringSort(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))) foreach (const QString &file, QtUtils::getFiles(dir.absoluteFilePath(folder), true)) templates.append(File(file, folder)); diff --git a/share/openbr/cmake/FindNaturalStringCompare.cmake b/share/openbr/cmake/FindNaturalStringCompare.cmake new file mode 100644 index 0000000..fda7b76 --- /dev/null +++ b/share/openbr/cmake/FindNaturalStringCompare.cmake @@ -0,0 +1,4 @@ +find_path(NATURALSTRINGCOMPARE_DIR NaturalStringCompare.h ${CMAKE_SOURCE_DIR}/3rdparty/*) + +include_directories(${NATURALSTRINGCOMPARE_DIR}) +set(NATURALSTRINGCOMPARE_SRC ${NATURALSTRINGCOMPARE_DIR}/NaturalStringCompare.cpp)