diff --git a/extras/Demo/Source/Demos/WidgetsDemo.cpp b/extras/Demo/Source/Demos/WidgetsDemo.cpp index fbfd1d4606..fd6c77482f 100644 --- a/extras/Demo/Source/Demos/WidgetsDemo.cpp +++ b/extras/Demo/Source/Demos/WidgetsDemo.cpp @@ -924,11 +924,11 @@ private: int compareElements (XmlElement* first, XmlElement* second) const { int result = first->getStringAttribute (attributeToSort) - .compareLexicographically (second->getStringAttribute (attributeToSort)); + .compareNatural (second->getStringAttribute (attributeToSort)); if (result == 0) result = first->getStringAttribute ("ID") - .compareLexicographically (second->getStringAttribute ("ID")); + .compareNatural (second->getStringAttribute ("ID")); return direction * result; } diff --git a/extras/Introjucer/Source/Project/jucer_Module.cpp b/extras/Introjucer/Source/Project/jucer_Module.cpp index f67c100857..1ebf1b38df 100644 --- a/extras/Introjucer/Source/Project/jucer_Module.cpp +++ b/extras/Introjucer/Source/Project/jucer_Module.cpp @@ -481,7 +481,7 @@ struct FileSorter { static int compareElements (const File& f1, const File& f2) { - return f1.getFileName().compareIgnoreCase (f2.getFileName()); + return f1.getFileName().compareNatural (f2.getFileName()); } }; diff --git a/extras/Introjucer/Source/Project/jucer_Project.cpp b/extras/Introjucer/Source/Project/jucer_Project.cpp index ec914c6f05..fede3e59c0 100644 --- a/extras/Introjucer/Source/Project/jucer_Project.cpp +++ b/extras/Introjucer/Source/Project/jucer_Project.cpp @@ -743,7 +743,7 @@ struct ItemSorter { static int compareElements (const ValueTree& first, const ValueTree& second) { - return first [Ids::name].toString().compareIgnoreCase (second [Ids::name].toString()); + return first [Ids::name].toString().compareNatural (second [Ids::name].toString()); } }; @@ -755,7 +755,7 @@ struct ItemSorterWithGroupsAtStart const bool secondIsGroup = second.hasType (Ids::GROUP); if (firstIsGroup == secondIsGroup) - return first [Ids::name].toString().compareIgnoreCase (second [Ids::name].toString()); + return first [Ids::name].toString().compareNatural (second [Ids::name].toString()); return firstIsGroup ? -1 : 1; } diff --git a/modules/juce_audio_processors/scanning/juce_KnownPluginList.cpp b/modules/juce_audio_processors/scanning/juce_KnownPluginList.cpp index afbf23e5e8..fc3679c241 100644 --- a/modules/juce_audio_processors/scanning/juce_KnownPluginList.cpp +++ b/modules/juce_audio_processors/scanning/juce_KnownPluginList.cpp @@ -259,15 +259,15 @@ struct PluginSorter switch (method) { - case KnownPluginList::sortByCategory: diff = first->category.compareLexicographically (second->category); break; - case KnownPluginList::sortByManufacturer: diff = first->manufacturerName.compareLexicographically (second->manufacturerName); break; + case KnownPluginList::sortByCategory: diff = first->category.compareNatural (second->category); break; + case KnownPluginList::sortByManufacturer: diff = first->manufacturerName.compareNatural (second->manufacturerName); break; case KnownPluginList::sortByFormat: diff = first->pluginFormatName.compare (second->pluginFormatName); break; case KnownPluginList::sortByFileSystemLocation: diff = lastPathPart (first->fileOrIdentifier).compare (lastPathPart (second->fileOrIdentifier)); break; default: break; } if (diff == 0) - diff = first->name.compareLexicographically (second->name); + diff = first->name.compareNatural (second->name); return diff * direction; } diff --git a/modules/juce_core/text/juce_String.cpp b/modules/juce_core/text/juce_String.cpp index 81e234e63d..3ff85ccc88 100644 --- a/modules/juce_core/text/juce_String.cpp +++ b/modules/juce_core/text/juce_String.cpp @@ -221,8 +221,8 @@ private: static inline StringHolder* bufferFromText (const CharPointerType text) noexcept { // (Can't use offsetof() here because of warnings about this not being a POD) - return reinterpret_cast (reinterpret_cast (text.getAddress()) - - (reinterpret_cast (reinterpret_cast (1)->text) - 1)); + return reinterpret_cast (reinterpret_cast (text.getAddress()) + - (reinterpret_cast (reinterpret_cast (1)->text) - 1)); } void compileTimeChecks() @@ -618,19 +618,98 @@ int String::compare (const char* const other) const noexcept { return text int String::compare (const wchar_t* const other) const noexcept { return text.compare (castToCharPointer_wchar_t (other)); } int String::compareIgnoreCase (const String& other) const noexcept { return (text == other.text) ? 0 : text.compareIgnoreCase (other.text); } -int String::compareLexicographically (const String& other) const noexcept +static int stringCompareRight (String::CharPointerType s1, String::CharPointerType s2) noexcept { - CharPointerType s1 (text); + for (int bias = 0;;) + { + const juce_wchar c1 = s1.getAndAdvance(); + const bool isDigit1 = CharacterFunctions::isDigit (c1); - while (! (s1.isEmpty() || s1.isLetterOrDigit())) - ++s1; + const juce_wchar c2 = s2.getAndAdvance(); + const bool isDigit2 = CharacterFunctions::isDigit (c2); - CharPointerType s2 (other.text); + if (! (isDigit1 || isDigit2)) return bias; + if (! isDigit1) return -1; + if (! isDigit2) return 1; - while (! (s2.isEmpty() || s2.isLetterOrDigit())) - ++s2; + if (c1 != c2 && bias == 0) + bias = c1 < c2 ? -1 : 1; - return s1.compareIgnoreCase (s2); + jassert (c1 != 0 && c2 != 0); + } +} + +static int stringCompareLeft (String::CharPointerType s1, String::CharPointerType s2) noexcept +{ + for (;;) + { + const juce_wchar c1 = s1.getAndAdvance(); + const bool isDigit1 = CharacterFunctions::isDigit (c1); + + const juce_wchar c2 = s2.getAndAdvance(); + const bool isDigit2 = CharacterFunctions::isDigit (c2); + + if (! (isDigit1 || isDigit2)) return 0; + if (! isDigit1) return -1; + if (! isDigit2) return 1; + if (c1 < c2) return -1; + if (c1 > c2) return 1; + } +} + +static int naturalStringCompare (String::CharPointerType s1, String::CharPointerType s2) noexcept +{ + bool firstLoop = true; + + for (;;) + { + const bool hasSpace1 = s1.isWhitespace(); + const bool hasSpace2 = s2.isWhitespace(); + + if ((! firstLoop) && (hasSpace1 ^ hasSpace2)) + return hasSpace2 ? 1 : -1; + + firstLoop = false; + + if (hasSpace1) s1 = s1.findEndOfWhitespace(); + if (hasSpace2) s2 = s2.findEndOfWhitespace(); + + if (s1.isDigit() && s2.isDigit()) + { + const int result = (*s1 == '0' || *s2 == '0') ? stringCompareLeft (s1, s2) + : stringCompareRight (s1, s2); + + if (result != 0) + return result; + } + + const juce_wchar c1 = s1.getAndAdvance(); + const juce_wchar c2 = s2.getAndAdvance(); + + if (c1 == c2 || CharacterFunctions::toUpperCase (c1) + == CharacterFunctions::toUpperCase (c2)) + { + if (c1 == 0) + return 0; + } + else + { + const bool isAlphaNum1 = CharacterFunctions::isLetterOrDigit (c1); + const bool isAlphaNum2 = CharacterFunctions::isLetterOrDigit (c2); + + if (isAlphaNum2 && ! isAlphaNum1) return -1; + if (isAlphaNum1 && ! isAlphaNum2) return 1; + + return c1 < c2 ? -1 : 1; + } + + jassert (c1 != 0 && c2 != 0); + } +} + +int String::compareNatural (StringRef other) const noexcept +{ + return naturalStringCompare (getCharPointer(), other.text); } //============================================================================== @@ -1760,13 +1839,13 @@ String String::formatted (const String pf, ... ) va_start (args, pf); #if JUCE_WINDOWS - HeapBlock temp (bufferSize); + HeapBlock temp (bufferSize); const int num = (int) _vsnwprintf (temp.getData(), bufferSize - 1, pf.toWideCharPointer(), args); #elif JUCE_ANDROID - HeapBlock temp (bufferSize); + HeapBlock temp (bufferSize); const int num = (int) vsnprintf (temp.getData(), bufferSize - 1, pf.toUTF8(), args); #else - HeapBlock temp (bufferSize); + HeapBlock temp (bufferSize); const int num = (int) vswprintf (temp.getData(), bufferSize - 1, pf.toWideCharPointer(), args); #endif @@ -1921,12 +2000,12 @@ struct StringEncodingConverter { static CharPointerType_Dest convert (const String& s) { - String& source = const_cast (s); + String& source = const_cast (s); typedef typename CharPointerType_Dest::CharType DestChar; if (source.isEmpty()) - return CharPointerType_Dest (reinterpret_cast (&emptyChar)); + return CharPointerType_Dest (reinterpret_cast (&emptyChar)); CharPointerType_Src text (source.getCharPointer()); const size_t extraBytesNeeded = CharPointerType_Dest::getBytesRequiredFor (text) + sizeof (typename CharPointerType_Dest::CharType); @@ -1936,7 +2015,7 @@ struct StringEncodingConverter text = source.getCharPointer(); void* const newSpace = addBytesToPointer (text.getAddress(), (int) endOffset); - const CharPointerType_Dest extraSpace (static_cast (newSpace)); + const CharPointerType_Dest extraSpace (static_cast (newSpace)); #if JUCE_DEBUG // (This just avoids spurious warnings from valgrind about the uninitialised bytes at the end of the buffer..) const size_t bytesToClear = (size_t) jmin ((int) extraBytesNeeded, 4); diff --git a/modules/juce_core/text/juce_String.h b/modules/juce_core/text/juce_String.h index 1827d544d7..b3a45ae60e 100644 --- a/modules/juce_core/text/juce_String.h +++ b/modules/juce_core/text/juce_String.h @@ -346,15 +346,15 @@ public: */ int compareIgnoreCase (const String& other) const noexcept; - /** Lexicographic comparison with another string. + /** Compares two strings, taking into account textual characteristics like numbers and spaces. - The comparison used here is case-insensitive and ignores leading non-alphanumeric - characters, making it good for sorting human-readable strings. + This comparison is case-insensitive and can detect words and embedded numbers in the + strings, making it good for sorting human-readable lists of things like filenames. @returns 0 if the two strings are identical; negative if this string comes before the other one alphabetically, or positive if it comes after it. */ - int compareLexicographically (const String& other) const noexcept; + int compareNatural (StringRef other) const noexcept; /** Tests whether the string begins with another string. If the parameter is an empty string, this will always return true. diff --git a/modules/juce_core/text/juce_StringArray.cpp b/modules/juce_core/text/juce_StringArray.cpp index e26d38d118..6c49138623 100644 --- a/modules/juce_core/text/juce_StringArray.cpp +++ b/modules/juce_core/text/juce_StringArray.cpp @@ -199,6 +199,11 @@ int StringArray::indexOf (StringRef stringToLookFor, const bool ignoreCase, int return -1; } +void StringArray::move (const int currentIndex, const int newIndex) noexcept +{ + strings.move (currentIndex, newIndex); +} + //============================================================================== void StringArray::remove (const int index) { @@ -255,12 +260,17 @@ void StringArray::trim() //============================================================================== struct InternalStringArrayComparator_CaseSensitive { - static int compareElements (String& first, String& second) { return first.compare (second); } + static int compareElements (String& s1, String& s2) noexcept { return s1.compare (s2); } }; struct InternalStringArrayComparator_CaseInsensitive { - static int compareElements (String& first, String& second) { return first.compareIgnoreCase (second); } + static int compareElements (String& s1, String& s2) noexcept { return s1.compareIgnoreCase (s2); } +}; + +struct InternalStringArrayComparator_Natural +{ + static int compareElements (String& s1, String& s2) noexcept { return s1.compareNatural (s2); } }; void StringArray::sort (const bool ignoreCase) @@ -277,12 +287,12 @@ void StringArray::sort (const bool ignoreCase) } } -void StringArray::move (const int currentIndex, int newIndex) noexcept +void StringArray::sortNatural() { - strings.move (currentIndex, newIndex); + InternalStringArrayComparator_Natural comp; + strings.sort (comp); } - //============================================================================== String StringArray::joinIntoString (StringRef separator, int start, int numberToJoin) const { diff --git a/modules/juce_core/text/juce_StringArray.h b/modules/juce_core/text/juce_StringArray.h index 251a44d3df..52335e1b45 100644 --- a/modules/juce_core/text/juce_StringArray.h +++ b/modules/juce_core/text/juce_StringArray.h @@ -134,18 +134,12 @@ public: /** Returns a pointer to the first String in the array. This method is provided for compatibility with standard C++ iteration mechanisms. */ - inline String* begin() const noexcept - { - return strings.begin(); - } + inline String* begin() const noexcept { return strings.begin(); } /** Returns a pointer to the String which follows the last element in the array. This method is provided for compatibility with standard C++ iteration mechanisms. */ - inline String* end() const noexcept - { - return strings.end(); - } + inline String* end() const noexcept { return strings.end(); } /** Searches for a string in the array. @@ -387,11 +381,16 @@ public: //============================================================================== /** Sorts the array into alphabetical order. - @param ignoreCase if true, the comparisons used will be case-sensitive. */ void sort (bool ignoreCase); + /** Sorts the array using extra language-aware rules to do a better job of comparing + words containing spaces and numbers. + @see String::compareNatural() + */ + void sortNatural(); + //============================================================================== /** Increases the array's internal storage to hold a minimum number of elements. diff --git a/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.cpp b/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.cpp index 97e9482737..e15781eeb8 100644 --- a/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.cpp @@ -222,7 +222,7 @@ struct FileInfoComparator return first->isDirectory ? -1 : 1; #endif - return first->filename.compareIgnoreCase (second->filename); + return first->filename.compareNatural (second->filename); } };