From 350fdcf29ac4fa2b63220f53e88e9c4532146500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:57:14 +0100 Subject: [PATCH] [V2] feat(merge): implement natural sorting for filenames in merge tool (#4888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes TLDR: - Added `naturalCompare` function to handle alphanumeric sorting - Updated `sortFiles` logic to use `naturalCompare` for filename sorting - Passed `naturalCompare` as dependency in sorting callback This pull request improves the file sorting logic in the `Merge` tool to provide a more natural, human-friendly ordering of filenames (e.g., "file2" now comes before "file10" instead of after). The main change is the introduction of a custom `naturalCompare` function that is used when sorting files by filename. File sorting improvements: * Added a `naturalCompare` function to sort filenames in a way that handles numeric portions naturally, ensuring files like "file2" are ordered before "file10" (`frontend/src/core/tools/Merge.tsx`). * Updated the file sorting logic to use `naturalCompare` instead of the default `localeCompare` when sorting by filename (`frontend/src/core/tools/Merge.tsx`). * Ensured the `sortFiles` callback properly depends on the new `naturalCompare` function (`frontend/src/core/tools/Merge.tsx`). Note: the sort on upload is natural sort (at least I think so I haven't checked the code), this is only relevant after upload, and you click the sort button again ### Before: image ### After: image --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [X] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: Balázs Szücs --- frontend/src/core/tools/Merge.tsx | 55 +++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/frontend/src/core/tools/Merge.tsx b/frontend/src/core/tools/Merge.tsx index e725291db..05d358f70 100644 --- a/frontend/src/core/tools/Merge.tsx +++ b/frontend/src/core/tools/Merge.tsx @@ -26,6 +26,57 @@ const Merge = (props: BaseToolProps) => { props, { minFiles: 2 } ); + const naturalCompare = useCallback((a: string, b: string): number => { + const isDigit = (char: string) => char >= '0' && char <= '9'; + + const getChunk = (s: string, length: number, marker: number): { chunk: string; newMarker: number } => { + let chunk = ''; + const c = s.charAt(marker); + chunk += c; + marker++; + + if (isDigit(c)) { + while (marker < length && isDigit(s.charAt(marker))) { + chunk += s.charAt(marker); + marker++; + } + } else { + while (marker < length && !isDigit(s.charAt(marker))) { + chunk += s.charAt(marker); + marker++; + } + } + return { chunk, newMarker: marker }; + }; + + const len1 = a.length; + const len2 = b.length; + let marker1 = 0; + let marker2 = 0; + + while (marker1 < len1 && marker2 < len2) { + const { chunk: chunk1, newMarker: newMarker1 } = getChunk(a, len1, marker1); + marker1 = newMarker1; + + const { chunk: chunk2, newMarker: newMarker2 } = getChunk(b, len2, marker2); + marker2 = newMarker2; + + let result: number; + if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) { + const num1 = parseInt(chunk1, 10); + const num2 = parseInt(chunk2, 10); + result = num1 - num2; + } else { + result = chunk1.localeCompare(chunk2); + } + + if (result !== 0) { + return result; + } + } + + return len1 - len2; + }, []); // Custom file sorting logic for merge tool const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => { @@ -33,7 +84,7 @@ const Merge = (props: BaseToolProps) => { let comparison = 0; switch (sortType) { case 'filename': - comparison = stubA.name.localeCompare(stubB.name); + comparison = naturalCompare(stubA.name, stubB.name); break; case 'dateModified': comparison = stubA.lastModified - stubB.lastModified; @@ -45,7 +96,7 @@ const Merge = (props: BaseToolProps) => { const selectedIds = sortedStubs.map(record => record.id); const deselectedIds = fileIds.filter(id => !selectedIds.includes(id)); reorderFiles([...selectedIds, ...deselectedIds]); - }, [selectedFileStubs, fileIds, reorderFiles]); + }, [selectedFileStubs, fileIds, reorderFiles, naturalCompare]); return createToolFlow({ files: {