@ -33,9 +33,9 @@
# include <QByteArray>
# include <QByteArray>
# include <QCollator>
# include <QCollator>
# include <QtGlobal>
# include <QLocale>
# include <QLocale>
# include <QRegExp>
# include <QRegExp>
# include <QtGlobal>
# ifdef Q_OS_MAC
# ifdef Q_OS_MAC
# include <QThreadStorage>
# include <QThreadStorage>
# endif
# endif
@ -45,110 +45,103 @@ namespace
class NaturalCompare
class NaturalCompare
{
{
public :
public :
explicit NaturalCompare ( const bool caseSensitive = tru e)
explicit NaturalCompare ( const Qt : : CaseSensitivity caseSensitivity = Qt : : CaseSensitiv e)
: m_caseSensitive ( caseSensitive )
: m_caseSensitivity ( caseSensitivity )
{
{
# if defined(Q_OS_WIN)
# ifdef Q_OS_WIN
// Without ICU library, QCollator uses the native API on Windows 7+. But that API
// Without ICU library, QCollator uses the native API on Windows 7+. But that API
// sorts older versions of μTorrent differently than the newer ones because the
// sorts older versions of μTorrent differently than the newer ones because the
// 'μ' character is encoded differently and the native API can't cope with that.
// 'μ' character is encoded differently and the native API can't cope with that.
// So default to using our custom natural sorting algorithm instead.
// So default to using our custom natural sorting algorithm instead.
// See #5238 and #5240
// See #5238 and #5240
// Without ICU library, QCollator doesn't support `setNumericMode(true)` on OS older than Win7
// Without ICU library, QCollator doesn't support `setNumericMode(true)` on an OS older than Win7
// if (QSysInfo::windowsVersion() < QSysInfo::WV_WINDOWS7)
# else
return ;
# endif
m_collator . setNumericMode ( true ) ;
m_collator . setNumericMode ( true ) ;
m_collator . setCaseSensitivity ( caseSensitive ? Qt : : CaseSensitive : Qt : : CaseInsensitive ) ;
m_collator . setCaseSensitivity ( caseSensitivity ) ;
# endif
}
}
bool operator ( ) ( const QString & left , const QString & right ) const
int operator ( ) ( const QString & left , const QString & right ) const
{
{
# if defined(Q_OS_WIN)
# ifdef Q_OS_WIN
// Without ICU library, QCollator uses the native API on Windows 7+. But that API
return compare ( left , right ) ;
// sorts older versions of μTorrent differently than the newer ones because the
# else
// 'μ' character is encoded differently and the native API can't cope with that.
return m_collator . compare ( left , right ) ;
// So default to using our custom natural sorting algorithm instead.
// See #5238 and #5240
// Without ICU library, QCollator doesn't support `setNumericMode(true)` on OS older than Win7
// if (QSysInfo::windowsVersion() < QSysInfo::WV_WINDOWS7)
return lessThan ( left , right ) ;
# endif
# endif
return ( m_collator . compare ( left , right ) < 0 ) ;
}
}
bool lessThan ( const QString & left , const QString & right ) const
private :
int compare ( const QString & left , const QString & right ) const
{
{
// Return value `false` indicates `right` should go before `left`, otherwise, after
// Return value <0: `left` is smaller than `right`
// Return value >0: `left` is greater than `right`
// Return value =0: both strings are equal
int posL = 0 ;
int posL = 0 ;
int posR = 0 ;
int posR = 0 ;
while ( true ) {
while ( true ) {
while ( true ) {
if ( ( posL = = left . size ( ) ) | | ( posR = = right . size ( ) ) )
if ( ( posL = = left . size ( ) ) | | ( posR = = right . size ( ) ) )
return ( left . size ( ) < right . size ( ) ) ; // when a shorter string is another string's prefix, shorter string place before longer string
return ( left . size ( ) - right . size ( ) ) ; // when a shorter string is another string's prefix, shorter string place before longer string
QChar leftChar = m_caseSensitive ? left [ posL ] : left [ posL ] . toLower ( ) ;
QChar rightChar = m_caseSensitive ? right [ posR ] : right [ posR ] . toLower ( ) ;
if ( leftChar = = rightChar )
; // compare next character
else if ( leftChar . isDigit ( ) & & rightChar . isDigit ( ) )
break ; // Both are digits, break this loop and compare numbers
else
return leftChar < rightChar ;
const QChar leftChar = ( m_caseSensitivity = = Qt : : CaseSensitive ) ? left [ posL ] : left [ posL ] . toLower ( ) ;
const QChar rightChar = ( m_caseSensitivity = = Qt : : CaseSensitive ) ? right [ posR ] : right [ posR ] . toLower ( ) ;
if ( leftChar = = rightChar ) {
// compare next character
+ + posL ;
+ + posL ;
+ + posR ;
+ + posR ;
}
}
else if ( leftChar . isDigit ( ) & & rightChar . isDigit ( ) ) {
// Both are digits, compare the numbers
const auto consumeNumber = [ ] ( const QString & str , int & pos ) - > int
{
const int start = pos ;
while ( ( pos < str . size ( ) ) & & str [ pos ] . isDigit ( ) )
+ + pos ;
return str . midRef ( start , ( pos - start ) ) . toInt ( ) ;
} ;
int startL = posL ;
const int numL = consumeNumber ( left , posL ) ;
while ( ( posL < left . size ( ) ) & & left [ posL ] . isDigit ( ) )
const int numR = consumeNumber ( right , posR ) ;
+ + posL ;
int numL = left . midRef ( startL , posL - startL ) . toInt ( ) ;
int startR = posR ;
while ( ( posR < right . size ( ) ) & & right [ posR ] . isDigit ( ) )
+ + posR ;
int numR = right . midRef ( startR , posR - startR ) . toInt ( ) ;
if ( numL ! = numR )
if ( numL ! = numR )
return ( numL < numR ) ;
return ( numL - numR ) ;
// Strings + digits do match and we haven't hit string end
// String + digits do match and we haven't hit the end of both strings
// Do another round
// then continue to consume the remainings
}
else {
return ( leftChar . unicode ( ) - rightChar . unicode ( ) ) ;
}
}
}
return false ;
}
}
private :
QCollator m_collator ;
QCollator m_collator ;
const bool m_caseSensitive ;
const Qt : : CaseSensitivity m_caseSensitivity ;
} ;
} ;
}
}
bool Utils : : String : : naturalCompareCaseSensitiv e ( const QString & left , const QString & right )
int Utils : : String : : naturalCompare ( const QString & left , const QString & right , const Qt : : CaseSensitivity caseSensitivity )
{
{
// provide a single `NaturalCompare` instance for easy use
// provide a single `NaturalCompare` instance for easy use
// https://doc.qt.io/qt-5/threads-reentrancy.html
// https://doc.qt.io/qt-5/threads-reentrancy.html
if ( caseSensitivity = = Qt : : CaseSensitive ) {
# ifdef Q_OS_MAC // workaround for Apple xcode: https://stackoverflow.com/a/29929949
# ifdef Q_OS_MAC // workaround for Apple xcode: https://stackoverflow.com/a/29929949
static QThreadStorage < NaturalCompare > nCmp ;
static QThreadStorage < NaturalCompare > nCmp ;
if ( ! nCmp . hasLocalData ( ) ) nCmp . setLocalData ( NaturalCompare ( true ) ) ;
if ( ! nCmp . hasLocalData ( ) )
nCmp . setLocalData ( NaturalCompare ( Qt : : CaseSensitive ) ) ;
return ( nCmp . localData ( ) ) ( left , right ) ;
return ( nCmp . localData ( ) ) ( left , right ) ;
# else
# else
thread_local NaturalCompare nCmp ( tru e) ;
thread_local NaturalCompare nCmp ( Qt : : CaseSensitiv e) ;
return nCmp ( left , right ) ;
return nCmp ( left , right ) ;
# endif
# endif
}
}
bool Utils : : String : : naturalCompareCaseInsensitive ( const QString & left , const QString & right )
# ifdef Q_OS_MAC
{
// provide a single `NaturalCompare` instance for easy use
// https://doc.qt.io/qt-5/threads-reentrancy.html
# ifdef Q_OS_MAC // workaround for Apple xcode: https://stackoverflow.com/a/29929949
static QThreadStorage < NaturalCompare > nCmp ;
static QThreadStorage < NaturalCompare > nCmp ;
if ( ! nCmp . hasLocalData ( ) ) nCmp . setLocalData ( NaturalCompare ( false ) ) ;
if ( ! nCmp . hasLocalData ( ) )
nCmp . setLocalData ( NaturalCompare ( Qt : : CaseInsensitive ) ) ;
return ( nCmp . localData ( ) ) ( left , right ) ;
return ( nCmp . localData ( ) ) ( left , right ) ;
# else
# else
thread_local NaturalCompare nCmp ( fals e) ;
thread_local NaturalCompare nCmp ( Qt : : CaseInsensitiv e) ;
return nCmp ( left , right ) ;
return nCmp ( left , right ) ;
# endif
# endif
}
}
@ -188,4 +181,3 @@ QString Utils::String::wildcardToRegex(const QString &pattern)
{
{
return qt_regexp_toCanonical ( pattern , QRegExp : : Wildcard ) ;
return qt_regexp_toCanonical ( pattern , QRegExp : : Wildcard ) ;
}
}