Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1422 (2022-12-22 21:48:32)

feat: Add 'sort by filename'
<!--- Provide a general summary of your changes in the Title above -->

# Description

Add 'sort by filename' and document it.

## Motivation and Context

It's just a gap.

## How has this been tested?

Writing new tests.

## Screenshots (if appropriate)

## 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] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)

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`.
- [x] My change requires a change to the documentation.
- [x] 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/docs/queries/sorting.md b/docs/queries/sorting.md
index 822576d2..12fd22ee 100644
--- a/docs/queries/sorting.md
+++ b/docs/queries/sorting.md
@@ -33,6 +33,10 @@ You can sort tasks by the following properties:
 File locations:
 
 1. `path` (the path to the file that contains the task)
+1. `filename` (the filename of the file that contains the task, with its extension)
+    - Note that tasks from different notes with the same file name will be sorter.
+
+> `sort by filename` was introduced in Tasks 1.21.0.
 
 File contents:
 
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index 16a87465..57bc14cd 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -25,12 +25,12 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `happens (before, after, on) <date>`<br>`has happens date`<br>`no  happens date`                                                                                                                    | `sort by happens`                           | `group by happens`    |                        |
 | `is recurring`<br>`is not recurring`                                                                                                                                                                |                                             | `group by recurring`  |                        |
 |                                                                                                                                                                                                     |                                             | `group by recurrence` | `hide recurrence rule` |
-| `priority is (above, below, not)? (low, none, medium, high)`                                                                                                                                         | `sort by priority`                          | `group by priority`   | `hide priority`        |
+| `priority is (above, below, not)? (low, none, medium, high)`                                                                                                                                        | `sort by priority`                          | `group by priority`   | `hide priority`        |
 |                                                                                                                                                                                                     | `sort by urgency`                           |                       | `show urgency`         |
 | `path (includes, does not include) <path>`<br>`path (regex matches, regex does not match) /regex/i`                                                                                                 | `sort by path`                              | `group by path`       |                        |
 |                                                                                                                                                                                                     |                                             | `group by root`       |                        |
 |                                                                                                                                                                                                     |                                             | `group by folder`     |                        |
-| `filename (includes, does not include) <filename>`<br>`filename (regex matches, regex does not match) /regex/i`                                                                                     |                                             | `group by filename`   |                        |
+| `filename (includes, does not include) <filename>`<br>`filename (regex matches, regex does not match) /regex/i`                                                                                     | `sort by filename`                          | `group by filename`   |                        |
 | `heading (includes, does not include) <string>`<br>`heading (regex matches, regex does not match) /regex/i`                                                                                         |                                             | `group by heading`    |                        |
 |                                                                                                                                                                                                     |                                             | `group by backlink`   | `hide backlink`        |
 | `description (includes, does not include) <string>`<br>`description (regex matches, regex does not match) /regex/i`                                                                                 | `sort by description`                       |                       |                        |
diff --git a/src/Query/Filter/FilenameField.ts b/src/Query/Filter/FilenameField.ts
index c6d30423..9939692c 100644
--- a/src/Query/Filter/FilenameField.ts
+++ b/src/Query/Filter/FilenameField.ts
@@ -1,4 +1,5 @@
 import type { Task } from '../../Task';
+import type { Comparator } from '../Sorting';
 import { TextField } from './TextField';
 
 /** Support the 'filename' search instruction.
@@ -24,4 +25,14 @@ export class FilenameField extends TextField {
         }
         return filename + '.md';
     }
+
+    supportsSorting(): boolean {
+        return true;
+    }
+
+    comparator(): Comparator {
+        return (a: Task, b: Task) => {
+            return this.value(a).localeCompare(this.value(b));
+        };
+    }
 }


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index af26598e..5a70c3e0 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -131,6 +131,7 @@ describe('Query parsing', () => {
             'sort by done',
             'sort by due reverse',
             'sort by due',
+            'sort by filename',
             'sort by happens',
             'sort by path reverse',
             'sort by path',
diff --git a/tests/Query/Filter/FilenameField.test.ts b/tests/Query/Filter/FilenameField.test.ts
index 8dc8f8c8..454660a5 100644
--- a/tests/Query/Filter/FilenameField.test.ts
+++ b/tests/Query/Filter/FilenameField.test.ts
@@ -1,6 +1,7 @@
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
 import { FilenameField } from '../../../src/Query/Filter/FilenameField';
 import { toBeValid, toMatchTaskWithPath } from '../../CustomMatchers/CustomMatchersForFilters';
+import * as CustomMatchersForSorting from '../../CustomMatchers/CustomMatchersForSorting';
 
 expect.extend({
     toMatchTaskWithPath,
@@ -75,3 +76,37 @@ describe('filename', () => {
         expect(filter).not.toMatchTaskWithPath('/some/path/wibble.md');
     });
 });
+
+describe('sorting by filename', () => {
+    it('supports Field sorting methods correctly', () => {
+        const field = new FilenameField();
+        expect(field.supportsSorting()).toEqual(true);
+    });
+
+    // Helper function to create a task with a given path
+    function with_path(path: string) {
+        return new TaskBuilder().path(path).build();
+    }
+
+    it('sort by filename', () => {
+        // Arrange
+        const sorter = new FilenameField().createNormalSorter();
+
+        // Assert
+        CustomMatchersForSorting.expectTaskComparesEqual(
+            sorter,
+            with_path('some path/filename.md'), // Only sorts file name - ignores folders (which differ)
+            with_path('other path/filename.md'),
+        );
+        // 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
+    });
+
+    it('sort by filename reverse', () => {
+        // Single example just to prove reverse works.
+        // (There's no need to repeat all the examples above)
+        const sorter = new FilenameField().createReverseSorter();
+        CustomMatchersForSorting.expectTaskComparesAfter(sorter, with_path('a/b.md'), with_path('c/d.md'));
+    });
+});