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

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_


Discussion:

Patch:
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index 2eba98ccc0..d141b63030 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/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 c48b428784..3649a8c219 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/src/Query/Filter/DateField.ts b/src/Query/Filter/DateField.ts
index ae98fd9bc4..45a311a63c 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/tests/Query/Filter/CancelledDateField.test.ts b/tests/Query/Filter/CancelledDateField.test.ts
index 7442286738..c40fbca234 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 c2e12fd74b..43c37354aa 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 4f5cc10044..4967237f3d 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 ba432e89fe..d5a01ee9b5 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 484652a39c..62334246f0 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 9b3446e94f..d1695b05e6 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 4ca64488ec..6a5105fd3d 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',
         ]);
     });