diff --git a/PowerEditor/src/MISC/Common/Common.cpp b/PowerEditor/src/MISC/Common/Common.cpp index ec16d2cbe..37a825c13 100644 --- a/PowerEditor/src/MISC/Common/Common.cpp +++ b/PowerEditor/src/MISC/Common/Common.cpp @@ -749,131 +749,16 @@ generic_string stringJoin(const std::vector& strings, const gene return joined; } -long long stollStrict(const generic_string& input) +generic_string stringTakeWhileAdmissable(const generic_string& input, const generic_string& admissable) { - if (input.empty()) + // Find first non-admissable character in "input", and remove everything after it. + size_t idx = input.find_first_not_of(admissable); + if (idx == std::string::npos) { - throw std::invalid_argument("Empty input."); + return input; } else { - // Check minus characters. - const int minuses = std::count(input.begin(), input.end(), TEXT('-')); - if (minuses > 1) - { - throw std::invalid_argument("More than one minus sign."); - } - else if (minuses == 1 && input[0] != TEXT('-')) - { - throw std::invalid_argument("Minus sign must be first."); - } - - // Check for other characters which are not allowed. - if (input.find_first_not_of(TEXT("-0123456789")) != std::string::npos) - { - throw std::invalid_argument("Invalid character found."); - } - - return std::stoll(input); + return input.substr(0, idx); } -} - -bool allLinesAreNumericOrEmpty(const std::vector& lines) -{ - for (const generic_string& line : lines) - { - try - { - if (!line.empty()) - { - stollStrict(line); - } - } - catch (std::invalid_argument&) - { - return false; - } - catch (std::out_of_range&) - { - return false; - } - } - return true; -} - -std::vector repeatString(const generic_string& text, const size_t count) -{ - std::vector output; - output.reserve(count); - for (size_t i = 0; i < count; ++i) - { - output.push_back(text); - } - assert(output.size() == count); - return output; -} - -std::vector lexicographicSort(std::vector input, bool isDescending) -{ - std::sort(input.begin(), input.end(), [isDescending](generic_string a, generic_string b) - { - if (isDescending) - { - return a.compare(b) > 0; - } - else - { - return a.compare(b) < 0; - } - }); - return input; -} - -std::vector numericSort(std::vector input, bool isDescending) -{ - // Pre-condition: all strings in "input" are either empty or convertible to int with stoiStrict. - // Note that empty lines are filtered out and added back manually to the output at the end. - std::vector nonEmptyInputAsNumbers; - size_t nofEmptyLines = 0; - nonEmptyInputAsNumbers.reserve(input.size()); - for (const generic_string& line : input) - { - if (line.empty()) - { - ++nofEmptyLines; - } - else - { - nonEmptyInputAsNumbers.push_back(stollStrict(line)); - } - } - assert(nonEmptyInputAsNumbers.size() + nofEmptyLines == input.size()); - std::sort(nonEmptyInputAsNumbers.begin(), nonEmptyInputAsNumbers.end(), [isDescending](long long a, long long b) - { - if (isDescending) - { - return a > b; - } - else - { - return a < b; - } - }); - std::vector output; - output.reserve(input.size()); - const std::vector empties = repeatString(TEXT(""), nofEmptyLines); - if (!isDescending) - { - output.insert(output.end(), empties.begin(), empties.end()); - } - for (const long long& sortedNumber : nonEmptyInputAsNumbers) - { - output.push_back(std::to_wstring(sortedNumber)); - } - if (isDescending) - { - output.insert(output.end(), empties.begin(), empties.end()); - } - assert(output.size() == input.size()); - return output; } \ No newline at end of file diff --git a/PowerEditor/src/MISC/Common/Common.h b/PowerEditor/src/MISC/Common/Common.h index 2e90fea69..78180d050 100644 --- a/PowerEditor/src/MISC/Common/Common.h +++ b/PowerEditor/src/MISC/Common/Common.h @@ -190,11 +190,6 @@ generic_string stringToUpper(generic_string strToConvert); generic_string stringReplace(generic_string subject, const generic_string& search, const generic_string& replace); std::vector stringSplit(const generic_string& input, const generic_string& delimiter); generic_string stringJoin(const std::vector& strings, const generic_string& separator); -long long stollStrict(const generic_string& input); -bool allLinesAreNumericOrEmpty(const std::vector& lines); -std::vector repeatString(const generic_string& text, const size_t count); - -std::vector numericSort(std::vector input, bool isDescending); -std::vector lexicographicSort(std::vector input, bool isDescending); +generic_string stringTakeWhileAdmissable(const generic_string& input, const generic_string& admissable); #endif //M30_IDE_COMMUN_H diff --git a/PowerEditor/src/MISC/Common/Sorters.h b/PowerEditor/src/MISC/Common/Sorters.h new file mode 100644 index 000000000..5ab4cad27 --- /dev/null +++ b/PowerEditor/src/MISC/Common/Sorters.h @@ -0,0 +1,210 @@ +// This file is part of Notepad++ project +// Copyright (C)2003 Don HO +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// Note that the GPL places important restrictions on "derived works", yet +// it does not provide a detailed definition of that term. To avoid +// misunderstandings, we consider an application to constitute a +// "derivative work" for the purpose of this license if it does any of the +// following: +// 1. Integrates source code from Notepad++. +// 2. Integrates/includes/aggregates Notepad++ into a proprietary executable +// installer, such as those produced by InstallShield. +// 3. Links to a library or executes a program that does any of the above. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + +#ifndef M30_IDE_SORTERS_H +#define M30_IDE_SORTERS_H + +// Base interface for line sorting. +class ISorter +{ +private: + bool _isDescending; + +protected: + bool isDescending() const + { + return _isDescending; + } + +public: + ISorter(bool isDescending) + { + _isDescending = isDescending; + }; + virtual ~ISorter() { }; + virtual std::vector sort(std::vector lines) = 0; +}; + +// Implementation of lexicographic sorting of lines. +class LexicographicSorter : public ISorter +{ +public: + LexicographicSorter(bool isDescending) : ISorter(isDescending) { }; + + std::vector sort(std::vector lines) override + { + const bool descending = isDescending(); + std::sort(lines.begin(), lines.end(), [descending](generic_string a, generic_string b) + { + if (descending) + { + return a.compare(b) > 0; + } + else + { + return a.compare(b) < 0; + } + }); + return lines; + } +}; + +// Convert each line to a number (somehow - see stringToNumber) and then sort. +template +class NumericSorter : public ISorter +{ +public: + NumericSorter(bool isDescending) : ISorter(isDescending) { }; + + std::vector sort(std::vector lines) override + { + // Note that empty lines are filtered out and added back manually to the output at the end. + std::vector> nonEmptyInputAsNumbers; + std::vector empties; + nonEmptyInputAsNumbers.reserve(lines.size()); + for (size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex) + { + generic_string line = prepareStringForConversion(lines[lineIndex]); + if (considerStringEmpty(line)) + { + empties.push_back(line); + } + else + { + try + { + nonEmptyInputAsNumbers.push_back(make_pair(lineIndex, convertStringToNumber(line))); + } + catch (...) + { + throw lineIndex; + } + } + } + assert(nonEmptyInputAsNumbers.size() + empties.size() == lines.size()); + const bool descending = isDescending(); + std::sort(nonEmptyInputAsNumbers.begin(), nonEmptyInputAsNumbers.end(), [descending](std::pair a, std::pair b) + { + if (descending) + { + return a.second > b.second; + } + else + { + return a.second < b.second; + } + }); + std::vector output; + output.reserve(lines.size()); + if (!isDescending()) + { + output.insert(output.end(), empties.begin(), empties.end()); + } + for (const std::pair& sortedNumber : nonEmptyInputAsNumbers) + { + output.push_back(lines[sortedNumber.first]); + } + if (isDescending()) + { + output.insert(output.end(), empties.begin(), empties.end()); + } + assert(output.size() == lines.size()); + return output; + } + +protected: + bool considerStringEmpty(const generic_string& input) + { + // String has something else than just whitespace. + return input.find_first_not_of(TEXT(" \t\r\n")) == std::string::npos; + } + + // Prepare the string for conversion to number. + virtual generic_string prepareStringForConversion(const generic_string& input) = 0; + + // Should convert the input string to a number of the correct type. + // If unable to convert, throw either std::invalid_argument or std::out_of_range. + virtual T_Num convertStringToNumber(const generic_string& input) = 0; +}; + +// Converts lines to long long before sorting. +class IntegerSorter : public NumericSorter +{ +public: + IntegerSorter(bool isDescending) : NumericSorter(isDescending) { }; + +protected: + virtual generic_string prepareStringForConversion(const generic_string& input) + { + return stringTakeWhileAdmissable(input, TEXT(" \t\r\n0123456789")); + } + + long long convertStringToNumber(const generic_string& input) override + { + return std::stoll(input); + } +}; + +// Converts lines to double before sorting (assumes decimal comma). +class DecimalCommaSorter : public NumericSorter +{ +public: + DecimalCommaSorter(bool isDescending) : NumericSorter(isDescending) { }; + +protected: + generic_string prepareStringForConversion(const generic_string& input) override + { + generic_string admissablePart = stringTakeWhileAdmissable(input, TEXT(" \t\r\n0123456789,")); + return stringReplace(admissablePart, TEXT(","), TEXT(".")); + } + + double convertStringToNumber(const generic_string& input) override + { + return std::stod(input); + } +}; + +// Converts lines to double before sorting (assumes decimal dot). +class DecimalDotSorter : public NumericSorter +{ +public: + DecimalDotSorter(bool isDescending) : NumericSorter(isDescending) { }; + +protected: + generic_string prepareStringForConversion(const generic_string& input) override + { + return stringTakeWhileAdmissable(input, TEXT(" \t\r\n0123456789.")); + } + + double convertStringToNumber(const generic_string& input) override + { + return std::stod(input); + } +}; + +#endif //M30_IDE_SORTERS_H diff --git a/PowerEditor/src/Notepad_plus.rc b/PowerEditor/src/Notepad_plus.rc index 292bf99d7..66b1b5b07 100644 --- a/PowerEditor/src/Notepad_plus.rc +++ b/PowerEditor/src/Notepad_plus.rc @@ -274,8 +274,29 @@ BEGIN MENUITEM "Duplicate Current Line", IDM_EDIT_DUP_LINE MENUITEM "Split Lines", IDM_EDIT_SPLIT_LINES MENUITEM "Join Lines", IDM_EDIT_JOIN_LINES - MENUITEM "Sort Lines in Ascending Order", IDM_EDIT_SORTLINES_ASCENDING - MENUITEM "Sort Lines in Descending Order", IDM_EDIT_SORTLINES_DESCENDING + POPUP "Sort Lines" + BEGIN + POPUP "Lexicographic" + BEGIN + MENUITEM "Ascending", IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING + MENUITEM "Descending", IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING + END + POPUP "Integer" + BEGIN + MENUITEM "Ascending", IDM_EDIT_SORTLINES_INTEGER_ASCENDING + MENUITEM "Descending", IDM_EDIT_SORTLINES_INTEGER_DESCENDING + END + POPUP "Decimal (comma)" + BEGIN + MENUITEM "Ascending", IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING + MENUITEM "Descending", IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING + END + POPUP "Decimal (dot)" + BEGIN + MENUITEM "Ascending", IDM_EDIT_SORTLINES_DECIMALDOT_ASCENDING + MENUITEM "Descending", IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING + END + END MENUITEM "Move Up Current Line", IDM_EDIT_LINE_UP MENUITEM "Move Down Current Line", IDM_EDIT_LINE_DOWN MENUITEM "Remove Empty Lines", IDM_EDIT_REMOVEEMPTYLINES diff --git a/PowerEditor/src/NppCommands.cpp b/PowerEditor/src/NppCommands.cpp index 0dbd252c1..dde770068 100644 --- a/PowerEditor/src/NppCommands.cpp +++ b/PowerEditor/src/NppCommands.cpp @@ -35,6 +35,7 @@ #include "VerticalFileSwitcher.h" #include "documentMap.h" #include "functionListPanel.h" +#include "Sorters.h" void Notepad_plus::macroPlayback(Macro macro) @@ -344,8 +345,14 @@ void Notepad_plus::command(int id) } break; - case IDM_EDIT_SORTLINES_ASCENDING: - case IDM_EDIT_SORTLINES_DESCENDING: + case IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING: + case IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING: + case IDM_EDIT_SORTLINES_INTEGER_ASCENDING: + case IDM_EDIT_SORTLINES_INTEGER_DESCENDING: + case IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING: + case IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING: + case IDM_EDIT_SORTLINES_DECIMALDOT_ASCENDING: + case IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING: { // default: no selection size_t fromLine = 0; @@ -374,8 +381,44 @@ void Notepad_plus::command(int id) toLine = lineRange.second; } + bool isDescending = id == IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING || + id == IDM_EDIT_SORTLINES_INTEGER_DESCENDING || + id == IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING || + id == IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING; + _pEditView->execute(SCI_BEGINUNDOACTION); - _pEditView->sortLines(fromLine, toLine, id == IDM_EDIT_SORTLINES_DESCENDING); + std::unique_ptr pSorter; + if (id == IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING || id == IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING) + { + pSorter = std::unique_ptr(new LexicographicSorter(isDescending)); + } + else if (id == IDM_EDIT_SORTLINES_INTEGER_DESCENDING || id == IDM_EDIT_SORTLINES_INTEGER_ASCENDING) + { + pSorter = std::unique_ptr(new IntegerSorter(isDescending)); + } + else if (id == IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING || id == IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING) + { + pSorter = std::unique_ptr(new DecimalCommaSorter(isDescending)); + } + else + { + pSorter = std::unique_ptr(new DecimalDotSorter(isDescending)); + } + try + { + _pEditView->sortLines(fromLine, toLine, pSorter.get()); + } + catch (size_t& failedLineIndex) + { + generic_string lineNo = std::to_wstring(1 + fromLine + failedLineIndex); + _nativeLangSpeaker.messageBox("SortingError", + _pPublicInterface->getHSelf(), + TEXT("Unable to perform numeric sort due to line $STR_REPLACE$."), + TEXT("Sorting Error"), + MB_OK | MB_ICONINFORMATION | MB_APPLMODAL, + 0, + lineNo.c_str()); // We don't use intInfo since it would require casting size_t -> int. + } _pEditView->execute(SCI_ENDUNDOACTION); if (hasSelection) // there was 1 selection, so we restore it @@ -2598,8 +2641,14 @@ void Notepad_plus::command(int id) case IDM_EDIT_RTL : case IDM_EDIT_LTR : case IDM_EDIT_BEGINENDSELECT: - case IDM_EDIT_SORTLINES_ASCENDING: - case IDM_EDIT_SORTLINES_DESCENDING: + case IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING: + case IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING: + case IDM_EDIT_SORTLINES_INTEGER_ASCENDING: + case IDM_EDIT_SORTLINES_INTEGER_DESCENDING: + case IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING: + case IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING: + case IDM_EDIT_SORTLINES_DECIMALDOT_ASCENDING: + case IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING: case IDM_EDIT_BLANKLINEABOVECURRENT: case IDM_EDIT_BLANKLINEBELOWCURRENT: case IDM_VIEW_FULLSCREENTOGGLE : diff --git a/PowerEditor/src/Parameters.cpp b/PowerEditor/src/Parameters.cpp index 23a07ebc1..dc5435507 100644 --- a/PowerEditor/src/Parameters.cpp +++ b/PowerEditor/src/Parameters.cpp @@ -127,8 +127,14 @@ WinMenuKeyDefinition winKeyDefs[] = { {VK_SPACE, IDM_EDIT_FUNCCALLTIP, true, false, true, NULL}, {VK_R, IDM_EDIT_RTL, true, true, false, NULL}, {VK_L, IDM_EDIT_LTR, true, true, false, NULL}, - {VK_NULL, IDM_EDIT_SORTLINES_ASCENDING, false, false, false, NULL}, - {VK_NULL, IDM_EDIT_SORTLINES_DESCENDING, false, false, false, NULL}, + {VK_NULL, IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_INTEGER_ASCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_INTEGER_DESCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_DECIMALDOT_ASCENDING, false, false, false, NULL }, + {VK_NULL, IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING, false, false, false, NULL }, {VK_RETURN, IDM_EDIT_BLANKLINEABOVECURRENT, true, true, false, NULL}, {VK_RETURN, IDM_EDIT_BLANKLINEBELOWCURRENT, true, true, true, NULL}, {VK_F, IDM_SEARCH_FIND, true, false, false, NULL}, diff --git a/PowerEditor/src/ScitillaComponent/ScintillaEditView.cpp b/PowerEditor/src/ScitillaComponent/ScintillaEditView.cpp index 91cad99a3..95defe0c9 100644 --- a/PowerEditor/src/ScitillaComponent/ScintillaEditView.cpp +++ b/PowerEditor/src/ScitillaComponent/ScintillaEditView.cpp @@ -29,6 +29,7 @@ #include "precompiledHeaders.h" #include "ScintillaEditView.h" #include "Parameters.h" +#include "Sorters.h" #include "TCHAR.h" @@ -2947,7 +2948,7 @@ void ScintillaEditView::insertNewLineBelowCurrentLine() execute(SCI_SETEMPTYSELECTION, execute(SCI_POSITIONFROMLINE, current_line + 1)); } -void ScintillaEditView::sortLines(size_t fromLine, size_t toLine, bool isDescending) +void ScintillaEditView::sortLines(size_t fromLine, size_t toLine, ISorter *pSort) { if (fromLine >= toLine) { @@ -2968,23 +2969,16 @@ void ScintillaEditView::sortLines(size_t fromLine, size_t toLine, bool isDescend } } assert(toLine - fromLine + 1 == splitText.size()); - const bool isNumericSort = allLinesAreNumericOrEmpty(splitText); - std::vector sortedText; - if (isNumericSort) - { - sortedText = numericSort(splitText, isDescending); - } - else - { - sortedText = lexicographicSort(splitText, isDescending); - } + const std::vector sortedText = pSort->sort(splitText); const generic_string joined = stringJoin(sortedText, getEOLString()); if (sortEntireDocument) { + assert(joined.length() == text.length()); replaceTarget(joined.c_str(), startPos, endPos); } else { + assert(joined.length() + getEOLString().length() == text.length()); replaceTarget((joined + getEOLString()).c_str(), startPos, endPos); } } diff --git a/PowerEditor/src/ScitillaComponent/ScintillaEditView.h b/PowerEditor/src/ScitillaComponent/ScintillaEditView.h index 0dcf90aeb..ec42f52f8 100644 --- a/PowerEditor/src/ScitillaComponent/ScintillaEditView.h +++ b/PowerEditor/src/ScitillaComponent/ScintillaEditView.h @@ -197,6 +197,8 @@ struct LanguageName { int lexerID; }; +class ISorter; + class ScintillaEditView : public Window { friend class Finder; @@ -636,7 +638,7 @@ public: }; void scrollPosToCenter(int pos); generic_string getEOLString(); - void sortLines(size_t fromLine, size_t toLine, bool isDescending); + void sortLines(size_t fromLine, size_t toLine, ISorter *pSort); void changeTextDirection(bool isRTL); bool isTextDirectionRTL() const; diff --git a/PowerEditor/src/menuCmdID.h b/PowerEditor/src/menuCmdID.h index 7ca353762..125fcb7bf 100644 --- a/PowerEditor/src/menuCmdID.h +++ b/PowerEditor/src/menuCmdID.h @@ -112,8 +112,14 @@ #define IDM_EDIT_REMOVEEMPTYLINESWITHBLANK (IDM_EDIT + 56) #define IDM_EDIT_BLANKLINEABOVECURRENT (IDM_EDIT + 57) #define IDM_EDIT_BLANKLINEBELOWCURRENT (IDM_EDIT + 58) - #define IDM_EDIT_SORTLINES_ASCENDING (IDM_EDIT + 59) - #define IDM_EDIT_SORTLINES_DESCENDING (IDM_EDIT + 60) + #define IDM_EDIT_SORTLINES_LEXICOGRAPHIC_ASCENDING (IDM_EDIT + 59) + #define IDM_EDIT_SORTLINES_LEXICOGRAPHIC_DESCENDING (IDM_EDIT + 60) + #define IDM_EDIT_SORTLINES_INTEGER_ASCENDING (IDM_EDIT + 61) + #define IDM_EDIT_SORTLINES_INTEGER_DESCENDING (IDM_EDIT + 62) + #define IDM_EDIT_SORTLINES_DECIMALCOMMA_ASCENDING (IDM_EDIT + 63) + #define IDM_EDIT_SORTLINES_DECIMALCOMMA_DESCENDING (IDM_EDIT + 64) + #define IDM_EDIT_SORTLINES_DECIMALDOT_ASCENDING (IDM_EDIT + 65) + #define IDM_EDIT_SORTLINES_DECIMALDOT_DESCENDING (IDM_EDIT + 66) // Menu macro #define IDM_MACRO_STARTRECORDINGMACRO (IDM_EDIT + 18)