[V2] feat(merge): implement natural sorting for filenames in merge tool (#4888)

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

<img width="1858" height="995" alt="image"
src="https://github.com/user-attachments/assets/b6fe117e-5f70-4ff0-b16d-6a82a1ab5c2b"
/>


### After:
<img width="1858" height="995" alt="image"
src="https://github.com/user-attachments/assets/4621950a-ce46-4f6e-b128-a0dd1d126973"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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 <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-11-13 16:57:14 +01:00 committed by GitHub
parent aa20dbb7a6
commit 350fdcf29a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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: {