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:
### After:
---
## 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: {