Repository: obsidian-tasks-group/obsidian-tasks (ts)

Issue:
Sorting by text fields should do proper natural sorting
### Please check that this issue hasn't been reported before.

- [X] I searched previous [Bug Reports](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22+sort%3Acomments-desc) didn't find any similar reports.

### Expected Behavior

Given these tasks:

```
- [ ] 8 Eight
- [ ] 9 Nine
- [ ] 10 Ten
- [ ] 11 Eleven
```

And this tasks block:

````
```tasks
sort by description
```
````

The result should be tasks displayed in this order:

```
- [ ] 8 Eight
- [ ] 9 Nine
- [ ] 10 Ten
- [ ] 11 Eleven
```



### Current behaviour

The actual results are shown in this order:

```
- [ ] 10 Ten
- [ ] 11 Eleven
- [ ] 8 Eight
- [ ] 9 Nine
```

Because `1` sorts before `8` - and the digits are not interpreted as numbers.

### Steps to reproduce

See the tasks and query block in 'Expected Behavior' above.

### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.1.9

### Tasks Plugin Version

1.20.0

### Checks

- [ ] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

The [Stack Overflow answer](https://stackoverflow.com/a/38641281/104370) shows a solution:

```ts
myArrayObjects = myArrayObjects.sort(function(a, b) {
  return a.name.localeCompare(b.name, undefined, {
    numeric: true,
    sensitivity: 'base'
  });
});
```

See the answer for more details.





Pull request:
fix: Sorting by text fields now sorts numbers correctly
<!--- Provide a general summary of your changes in the Title above -->

# Description

- fix: Sort numbers in filename, heading and path correctly
- fix: Sort numbers in description correctly
- fix: Sort numbers in tags correctly

## Motivation and Context

Fixes #1429.

## How has this been tested?

By adding and updating tests.

## Screenshots (if appropriate)

With these tasks:

```
- [ ] #task 8 Eight
- [ ] #task 9 Nine
- [ ] #task 10 Ten
- [ ] #task 11 Eleven
```

And this tasks block:

````
```tasks
filename includes number sorting
sort by description
```
````

**Before**

![image](https://user-images.githubusercontent.com/4840096/209732814-f64852bd-08af-4bd3-9187-6f835ab393fa.png)

**After**

![image](https://user-images.githubusercontent.com/4840096/209733019-11e183f2-2b5a-4459-9cb3-a9393dacb877.png)


## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)


Patch:
diff --git a/src/Query/Filter/DescriptionField.ts b/src/Query/Filter/DescriptionField.ts
index bb6c1294..6b7475f9 100644
--- a/src/Query/Filter/DescriptionField.ts
+++ b/src/Query/Filter/DescriptionField.ts
@@ -43,9 +43,9 @@ export class DescriptionField extends TextField {
      */
     public comparator(): Comparator {
         return (a: Task, b: Task) => {
-            return DescriptionField.cleanDescription(a.description).localeCompare(
-                DescriptionField.cleanDescription(b.description),
-            );
+            const descriptionA = DescriptionField.cleanDescription(a.description);
+            const descriptionB = DescriptionField.cleanDescription(b.description);
+            return descriptionA.localeCompare(descriptionB, undefined, { numeric: true });
         };
     }
 
diff --git a/src/Query/Filter/TagsField.ts b/src/Query/Filter/TagsField.ts
index e946da73..324b0086 100644
--- a/src/Query/Filter/TagsField.ts
+++ b/src/Query/Filter/TagsField.ts
@@ -87,7 +87,9 @@ export class TagsField extends MultiTextField {
                 return 0;
             }
 
-            return a.tags[tagInstanceToSortBy].localeCompare(b.tags[tagInstanceToSortBy]);
+            const tagA = a.tags[tagInstanceToSortBy];
+            const tagB = b.tags[tagInstanceToSortBy];
+            return tagA.localeCompare(tagB, undefined, { numeric: true });
         };
     }
 }
diff --git a/src/Query/Filter/TextField.ts b/src/Query/Filter/TextField.ts
index 4bf41a74..694b16e6 100644
--- a/src/Query/Filter/TextField.ts
+++ b/src/Query/Filter/TextField.ts
@@ -100,7 +100,7 @@ export abstract class TextField extends Field {
      */
     comparator(): Comparator {
         return (a: Task, b: Task) => {
-            return this.value(a).localeCompare(this.value(b));
+            return this.value(a).localeCompare(this.value(b), undefined, { numeric: true });
         };
     }
 }


Test patch:
diff --git a/tests/Query/Filter/DescriptionField.test.ts b/tests/Query/Filter/DescriptionField.test.ts
index 4c5f3a08..4d0d7533 100644
--- a/tests/Query/Filter/DescriptionField.test.ts
+++ b/tests/Query/Filter/DescriptionField.test.ts
@@ -275,6 +275,10 @@ describe('sorting by description', () => {
         expectTaskComparesBefore(sorter, with_description('AAA'), with_description('bbb'));
         expectTaskComparesBefore(sorter, with_description('aaa'), with_description('BBB'));
         expectTaskComparesBefore(sorter, with_description('aaa'), with_description('AAA'));
+
+        // Sorts text with numbers in correctly
+        expectTaskComparesBefore(sorter, with_description('9 x'), with_description('11 x'));
+        expectTaskComparesBefore(sorter, with_description('x 9'), with_description('x 11'));
     });
 
     it('sort by description reverse', () => {
diff --git a/tests/Query/Filter/FilenameField.test.ts b/tests/Query/Filter/FilenameField.test.ts
index 454660a5..f077fe78 100644
--- a/tests/Query/Filter/FilenameField.test.ts
+++ b/tests/Query/Filter/FilenameField.test.ts
@@ -100,7 +100,7 @@ describe('sorting by filename', () => {
         );
         // Beginning with numbers
         CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_path('c/1.md'), with_path('c/9.md'));
-        CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_path('c/11.md'), with_path('c/9.md')); // TODO want 11 to compare after 9
+        CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_path('c/9.md'), with_path('c/11.md'));
     });
 
     it('sort by filename reverse', () => {
diff --git a/tests/Query/Filter/HeadingField.test.ts b/tests/Query/Filter/HeadingField.test.ts
index be92ba7d..7866f204 100644
--- a/tests/Query/Filter/HeadingField.test.ts
+++ b/tests/Query/Filter/HeadingField.test.ts
@@ -86,7 +86,7 @@ describe('sorting by heading', () => {
         CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_heading(''), with_heading('Non-empty heading')); // Empty heading comes first
         // Beginning with numbers
         CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_heading('1 Stuff'), with_heading('2 Stuff'));
-        CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_heading('11 Stuff'), with_heading('9 Stuff')); // TODO want 11 to compare after 9
+        CustomMatchersForSorting.expectTaskComparesBefore(sorter, with_heading('9 Stuff'), with_heading('11 Stuff'));
     });
 
     it('sort by heading reverse', () => {
diff --git a/tests/Query/Filter/PathField.test.ts b/tests/Query/Filter/PathField.test.ts
index d9ac8859..c3fa9f12 100644
--- a/tests/Query/Filter/PathField.test.ts
+++ b/tests/Query/Filter/PathField.test.ts
@@ -142,7 +142,7 @@ describe('sorting by path', () => {
 
         // Beginning with numbers
         expectTaskComparesBefore(sorter, with_path('c/1.md'), with_path('c/9.md'));
-        expectTaskComparesBefore(sorter, with_path('c/11.md'), with_path('c/9.md')); // TODO want 11 to compare after 9
+        expectTaskComparesBefore(sorter, with_path('c/9.md'), with_path('c/11.md'));
     });
 
     it('sort by path reverse', () => {
diff --git a/tests/Query/Filter/TagsField.test.ts b/tests/Query/Filter/TagsField.test.ts
index df4b4710..c4f15f2c 100644
--- a/tests/Query/Filter/TagsField.test.ts
+++ b/tests/Query/Filter/TagsField.test.ts
@@ -334,9 +334,15 @@ describe('Sort by tags', () => {
 
         it('should sort by tags case-insensitively', () => {
             const sorter = tagsField.createNormalSorter();
-            expectTaskComparesBefore(sorter, with_tags(['AAAA']), with_tags(['bbbb']));
-            expectTaskComparesBefore(sorter, with_tags(['aaaa']), with_tags(['BBBB']));
-            expectTaskComparesBefore(sorter, with_tags(['aaaa']), with_tags(['AAAA']));
+            expectTaskComparesBefore(sorter, with_tags(['#AAAA']), with_tags(['#bbbb']));
+            expectTaskComparesBefore(sorter, with_tags(['#aaaa']), with_tags(['#BBBB']));
+            expectTaskComparesBefore(sorter, with_tags(['#aaaa']), with_tags(['#AAAA']));
+        });
+
+        it('should sort by tags with numbers correctly', () => {
+            const sorter = tagsField.createNormalSorter();
+            expectTaskComparesBefore(sorter, with_tags(['#a-0']), with_tags(['#a-9']));
+            expectTaskComparesBefore(sorter, with_tags(['#a-9']), with_tags(['#a-11']));
         });
 
         it('should fail to parse a invalid line', () => {