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

Issue:
`group by due` and similar should put `Invalid date` heading before the headings for valid dates.
### 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

_Note: This issue refers to `due` dates, but the same applies to all the date fields._

`group by due` and similar should put tasks with invalid dates should be the first heading, `Invalid date`, as action is required.

_Note: this follows on from #2589._


### Current behaviour

Current the `Invalid date` heading is after all valid dates.


### Steps to reproduce

1. Create a note, and call it `Invalid dates with Sorting and Grouping`
2. View it in an editing mode
3. Use `Paste and Match Style` in the following content:

~~~
# Invalid dates with Sorting and Grouping

## Tasks

Numbers indicate the sort order I feel is useful, as invalid dates require action to fix them, so they should be prioritised.

- [ ] #task 1 invalid due date 📅 1999-02-32
- [ ] #task 2 valid over-due date 📅 2023-01-01
- [ ] #task 3 valid future-due date 📅 2050-01-01
- [ ] #task 4 undated

## Group by due - built-in

`Invalid date` should be the first heading, as action is required.

```tasks
group by due

path includes {{query.file.path}}
hide backlinks
hide postpone button
hide task count
```
~~~

4. View the file in Live Preview or Reading mode.

**Actual output**

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/880a4016-ca17-49c0-9266-30db0e1e6a59)


**Expected output**

`Invalid date` should be the first heading.


### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.5.3

### Tasks Plugin Version

5.6.0

### Checks

- [ ] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

_No response_

Pull request description:
fix: `group by [date]` now `Invalid [date] date` as first heading
<!--- Provide a general summary of your changes in the Title above -->

# Description

Make invalid dates more prominent in `group by due` and friends.

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->



## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

Fixes #2591.

## Screenshots (if appropriate)

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/8882a42e-696e-422c-9cf0-27e8d46ae0ff)


## 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)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [x] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

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://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) 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://publish.obsidian.md/tasks-contributing)
- [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/Grouping.md b/docs/Queries/Grouping.md
index 2eba98cc..d141b630 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -167,7 +167,7 @@ group by function "Next status symbol: " + task.status.nextSymbol.replace(" ", "
 ### Due Date
 
 - `group by due`
-  - The due date of the task, including the week-day, or `No due date`.
+  - The due date of the task, including the week-day, or `Invalid due date` or `No due date`.
 
 > [!released]
 >
@@ -330,7 +330,7 @@ Sample image showing tasks grouped first by highlighted words `Overdue`, `Today`
 ### Done Date
 
 - `group by done`
-  - The done date of the task, including the week-day, or `No done date`.
+  - The done date of the task, including the week-day, or `Invalid done date` or `No done date`.
 
 > [!released]
 >
@@ -353,7 +353,7 @@ For more examples, see [[#Due Date]].
 ### Scheduled Date
 
 - `group by scheduled`
-  - The scheduled date of the task, including the week-day, or `No scheduled date`.
+  - The scheduled date of the task, including the week-day, or `Invalid scheduled date` or `No scheduled date`.
 
 > [!released]
 >
@@ -376,7 +376,7 @@ For more examples, see [[#Due Date]].
 ### Start Date
 
 - `group by start`
-  - The start date of the task, including the week-day, or `No start date`.
+  - The start date of the task, including the week-day, or `Invalid start date` or `No start date`.
 
 > [!released]
 >
@@ -399,7 +399,7 @@ For more examples, see [[#Due Date]].
 ### Created Date
 
 - `group by created`
-  - The created date of the task, including the week-day, or `No created date`.
+  - The created date of the task, including the week-day, or `Invalid created date` or `No created date`.
 
 > [!released]
 `created` grouping option was introduced in Tasks 2.0.0.
@@ -421,7 +421,7 @@ For more examples, see [[#Due Date]].
 ### Cancelled Date
 
 - `group by cancelled`
-  - The cancelled date of the task, including the week-day, or `No cancelled date`.
+  - The cancelled date of the task, including the week-day, or `Invalid cancelled date` or `No cancelled date`.
 
 > [!released]
 `cancelled` grouping option was introduced in Tasks 5.5.0.
diff --git a/src/Query/Filter/DateField.ts b/src/Query/Filter/DateField.ts
index ae98fd9b..45a311a6 100644
--- a/src/Query/Filter/DateField.ts
+++ b/src/Query/Filter/DateField.ts
@@ -271,6 +271,11 @@ export abstract class DateField extends Field {
             if (date === null) {
                 return ['No ' + this.fieldName() + ' date'];
             }
+            if (!date.isValid()) {
+                // Use comment-out text to force Invalid dates to be sorted before the other headings.
+                // When the heading is rendered by Obsidian, the comment will be invisible.
+                return ['%%0%% Invalid ' + this.fieldName() + ' date'];
+            }
             return [date.format('YYYY-MM-DD dddd')];
         };
     }


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid dates with Sorting and Grouping.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid dates with Sorting and Grouping.md
index c48b4287..3649a8c2 100644
--- a/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid dates with Sorting and Grouping.md
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid dates with Sorting and Grouping.md
@@ -13,9 +13,9 @@ Numbers indicate the sort order I feel is useful, as invalid dates require actio
 
 ### Group by due - built-in
 
-`Invalid date` should be the first heading, as action is required.
+`Invalid due date` is the first heading, as action is required.
 
-[Bug report](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2591): **`group by due` and similar should put `Invalid date` heading before the headings for valid dates.**
+==FIXED==: [Bug report](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2591): **`group by due` and similar should put `Invalid date` heading before the headings for valid dates.**
 
 ```tasks
 group by due
@@ -73,17 +73,6 @@ hide postpone button
 hide task count
 ```
 
-### Default sort order
-
-The invalid task should be before the dated ones.
-
-```tasks
-path includes {{query.file.path}}
-hide backlinks
-hide postpone button
-hide task count
-```
-
 ## Expected sort order
 
 ```tasks
diff --git a/tests/Query/Filter/CancelledDateField.test.ts b/tests/Query/Filter/CancelledDateField.test.ts
index 74422867..c40fbca2 100644
--- a/tests/Query/Filter/CancelledDateField.test.ts
+++ b/tests/Query/Filter/CancelledDateField.test.ts
@@ -113,10 +113,10 @@ describe('grouping by cancelled date', () => {
         const tasks = SampleTasks.withAllRepresentativeCancelledDates();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid cancelled date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No cancelled date',
         ]);
     });
diff --git a/tests/Query/Filter/CreatedDateField.test.ts b/tests/Query/Filter/CreatedDateField.test.ts
index c2e12fd7..43c37354 100644
--- a/tests/Query/Filter/CreatedDateField.test.ts
+++ b/tests/Query/Filter/CreatedDateField.test.ts
@@ -112,10 +112,10 @@ describe('grouping by created date', () => {
         const tasks = SampleTasks.withAllRepresentativeCreatedDates();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid created date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No created date',
         ]);
     });
diff --git a/tests/Query/Filter/DoneDateField.test.ts b/tests/Query/Filter/DoneDateField.test.ts
index 4f5cc100..4967237f 100644
--- a/tests/Query/Filter/DoneDateField.test.ts
+++ b/tests/Query/Filter/DoneDateField.test.ts
@@ -109,10 +109,10 @@ describe('grouping by done date', () => {
         const tasks = SampleTasks.withAllRepresentativeDoneDates();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid done date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No done date',
         ]);
     });
diff --git a/tests/Query/Filter/DueDateField.test.ts b/tests/Query/Filter/DueDateField.test.ts
index ba432e89..d5a01ee9 100644
--- a/tests/Query/Filter/DueDateField.test.ts
+++ b/tests/Query/Filter/DueDateField.test.ts
@@ -607,10 +607,10 @@ describe('grouping by due date', () => {
         const tasks = SampleTasks.withAllRepresentativeDueDates();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid due date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No due date',
         ]);
     });
diff --git a/tests/Query/Filter/HappensDateField.test.ts b/tests/Query/Filter/HappensDateField.test.ts
index 484652a3..62334246 100644
--- a/tests/Query/Filter/HappensDateField.test.ts
+++ b/tests/Query/Filter/HappensDateField.test.ts
@@ -166,10 +166,10 @@ describe('grouping by happens date', () => {
         ];
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid happens date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No happens date',
         ]);
     });
diff --git a/tests/Query/Filter/ScheduledDateField.test.ts b/tests/Query/Filter/ScheduledDateField.test.ts
index 9b3446e9..d1695b05 100644
--- a/tests/Query/Filter/ScheduledDateField.test.ts
+++ b/tests/Query/Filter/ScheduledDateField.test.ts
@@ -63,10 +63,10 @@ describe('grouping by scheduled date', () => {
         const tasks = SampleTasks.withAllRepresentativeScheduledDates();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid scheduled date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No scheduled date',
         ]);
     });
diff --git a/tests/Query/Filter/StartDateField.test.ts b/tests/Query/Filter/StartDateField.test.ts
index 4ca64488..6a5105fd 100644
--- a/tests/Query/Filter/StartDateField.test.ts
+++ b/tests/Query/Filter/StartDateField.test.ts
@@ -79,10 +79,10 @@ describe('grouping by start date', () => {
         const tasks = SampleTasks.withAllRepresentativeStartDates();
 
         expect({ grouper, tasks }).groupHeadingsToBe([
+            '%%0%% Invalid start date',
             '2023-05-30 Tuesday',
             '2023-05-31 Wednesday',
             '2023-06-01 Thursday',
-            'Invalid date',
             'No start date',
         ]);
     });