Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1419 (2022-12-22 19:58:48)

feat: Add 'sort by happens'
<!--- Provide a general summary of your changes in the Title above -->

# Description

- Add new instruction `sort by happens`
- Add documentation
- Add tests

It sorts by the earliest of start date, scheduled date, and due date.

## Motivation and Context

This has been requested in #616.

## How has this been tested?

By adding 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)

## 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 37289659..84f16dcb 100644
--- a/docs/queries/sorting.md
+++ b/docs/queries/sorting.md
@@ -35,14 +35,17 @@ You can sort tasks by the following properties:
 5. `scheduled` (the date when the task is scheduled)
 6. `due` (the date when the task is due)
 7. `done` (the date when the task was done)
-8. `path` (the path to the file that contains the task)
-9. `description` (the description of the task)
-10. `tag` (the description of the task)
+8. `happens` (the earliest of start date, scheduled date, and due date)
+9. `path` (the path to the file that contains the task)
+10. `description` (the description of the task)
+11. `tag` (the description of the task)
 
 You can add multiple `sort by` query options, each on an extra line.
 The first sort has the highest priority.
 Each subsequent `sort` will sort within the existing sorting.
 
+> `sort by happens` was introduced in Tasks 1.21.0.
+
 <div class="code-example" markdown="1">
 Info
 {: .label .label-blue }
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index e22d12f6..16a87465 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -22,7 +22,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `starts (before, after, on) <date>`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                                                                                              | `sort by start`                             | `group by start`      | `hide start date`      |
 | `scheduled (before, after, on) <date>`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid`                                                                               | `sort by scheduled`                         | `group by scheduled`  | `hide scheduled date`  |
 | `due (before, after, on) <date>`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                                                                                                       | `sort by due`                               | `group by due`        | `hide due date`        |
-| `happens (before, after, on) <date>`<br>`has happens date`<br>`no  happens date`                                                                                                                    |                                             | `group by happens`    |                        |
+| `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`        |
diff --git a/src/Query/Filter/HappensDateField.ts b/src/Query/Filter/HappensDateField.ts
index b598f055..86137822 100644
--- a/src/Query/Filter/HappensDateField.ts
+++ b/src/Query/Filter/HappensDateField.ts
@@ -2,6 +2,7 @@ import type { Moment } from 'moment';
 import type { Task } from '../../Task';
 import { DateParser } from '../DateParser';
 import { Explanation } from '../Explain/Explanation';
+import type { Comparator } from '../Sorting';
 import { Field } from './Field';
 import { Filter, FilterOrErrorMessage } from './Filter';
 import { FilterInstructions } from './FilterInstructions';
@@ -109,4 +110,17 @@ export class HappensDateField extends Field {
     public fieldName(): string {
         return 'happens';
     }
+
+    public supportsSorting(): boolean {
+        return true;
+    }
+
+    /**
+     * This sorts on the earliest of start, scheduled and due dates.
+     */
+    public comparator(): Comparator {
+        return (a: Task, b: Task) => {
+            return DateField.compareByDate(this.earliestDate(a), this.earliestDate(b));
+        };
+    }
 }
diff --git a/src/Query/Sorting.ts b/src/Query/Sorting.ts
index 18ce6539..5afc7212 100644
--- a/src/Query/Sorting.ts
+++ b/src/Query/Sorting.ts
@@ -25,9 +25,9 @@ export class Sorting {
      * TODO Once SortingProperty has been removed, re-order the parameters so the comparator comes first.
      *
      * @param reverse - whether the sort order should be reversed.
-     * @param property - the name of the property. If {@link comparator} is not supplied, this string must match
-     *                   one of the values in ${@link SortingProperty}.
-     * @param comparator - {@link Comparator} function.
+     * @param property - the name of the property.
+     * @param comparator - {@link Comparator} function, for sorting in the standard direction.
+     *                     If `reverse` is true, it will automatically be converted to reverse the sort direction.
      */
     constructor(reverse: boolean, property: string, comparator: Comparator) {
         this.property = property;


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index e3a06df7..af26598e 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 happens',
             'sort by path reverse',
             'sort by path',
             'sort by priority reverse',
diff --git a/tests/Query/Filter/HappensDateField.test.ts b/tests/Query/Filter/HappensDateField.test.ts
index 9376a690..67073577 100644
--- a/tests/Query/Filter/HappensDateField.test.ts
+++ b/tests/Query/Filter/HappensDateField.test.ts
@@ -6,6 +6,7 @@ import { HappensDateField } from '../../../src/Query/Filter/HappensDateField';
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
 import { testFilter } from '../../TestingTools/FilterTestHelpers';
 import { toHaveExplanation } from '../../CustomMatchers/CustomMatchersForFilters';
+import * as CustomMatchersForSorting from '../../CustomMatchers/CustomMatchersForSorting';
 
 window.moment = moment;
 
@@ -119,3 +120,21 @@ describe('explain happens date queries', () => {
         );
     });
 });
+
+describe('sorting by happens', () => {
+    it('supports Field sorting methods correctly', () => {
+        const field = new HappensDateField();
+        expect(field.supportsSorting()).toEqual(true);
+    });
+
+    const date1 = new TaskBuilder().startDate('2021-01-12').build();
+    const date2 = new TaskBuilder().scheduledDate('2022-12-23').build();
+
+    it('sort by happens', () => {
+        CustomMatchersForSorting.expectTaskComparesBefore(new HappensDateField().createNormalSorter(), date1, date2);
+    });
+
+    it('sort by happens reverse', () => {
+        CustomMatchersForSorting.expectTaskComparesAfter(new HappensDateField().createReverseSorter(), date1, date2);
+    });
+});