Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1228 (2022-10-12 21:20:19)

feat: Add 4 instructions: '(done|due|date|start) date is invalid'
<!--- Provide a general summary of your changes in the Title above -->

# Description

- Add 4 new instructions:

`done date is invalid`
`due date is invalid`
`scheduled date is invalid`
`start date is invalid`

- Updated `docs/queries/filters.md`, including adding a new `Validating dates` section
- Updated `docs/quick-reference/index.md`
- Added new note `resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid Dates.md` in the demo vault

## Motivation and Context

To make it easy for users to find tasks with invalid dates.

This was prompted by my recently accidentally give a task a due date of `2022-09-31` which does not exist. I noticed it by chance when seeing the task displayed in Reading view, and so wanted to search my vault and see if there were any other such errors.

## How has this been tested?

- Adding new tests
- Running existing tests
- Manual exploratory testing (which identified #1227)

## 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)
- [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://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/filters.md b/docs/queries/filters.md
index 96e6602e..8c5e1fc4 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -39,6 +39,18 @@ Use `next tuesday` instead if you mean "next tuesday".
 
 When the day changes, relative dates like `due today` are re-evaluated so that the list stays up-to-date.
 
+### Validating dates
+
+It is possible to accidentally use a non-existent date on a task signifier, such as `📅 2022-02-30`. February has at most 29 days.
+
+Such tasks look like they have a date, but that date will never be found. When viewed in Reading mode, they will be shown as:
+
+Any such mistakes can be found systematically with:
+
+    ```tasks
+    (done date is invalid) OR (due date is invalid) OR (scheduled date is invalid) OR (start date is invalid)
+    ```
+
 ---
 
 ## Matching multiple filters
@@ -72,32 +84,40 @@ For full details of combining filters with boolean operators, see [Combining Fil
 - `no done date`
 - `has done date`
 - `done (before|after|on) <date>`
+- `done date is invalid`
 
-> `no done date` and `has done date` were introduced in Tasks 1.7.0.
+> `no done date` and `has done date` were introduced in Tasks 1.7.0.<br>
+> `done date is invalid` was introduced in Tasks 1.16.0.
 
 ### Due Date
 
 - `no due date`
 - `has due date`
 - `due (before|after|on) <date>`
+- `due date is invalid`
 
-> `has due date` was introduced in Tasks 1.6.0.
+> `has due date` was introduced in Tasks 1.6.0.<br>
+> `due date is invalid` was introduced in Tasks 1.16.0.
 
 ### Scheduled Date
 
 - `no scheduled date`
 - `has scheduled date`
 - `scheduled (before|after|on) <date>`
+- `scheduled date is invalid`
 
-> `has scheduled date` was introduced in Tasks 1.6.0.
+> `has scheduled date` was introduced in Tasks 1.6.0.<br>
+> `scheduled date is invalid` was introduced in Tasks 1.16.0.
 
 ### Start Date
 
 - `no start date`
 - `has start date`
 - `starts (before|after|on) <date>`
+- `start date is invalid`
 
-> `has start date` was Introduced in Tasks 1.6.0.
+> `has start date` was Introduced in Tasks 1.6.0.<br>
+> `start date is invalid` was introduced in Tasks 1.16.0.
 
 When filtering queries by [start date]({{ site.baseurl }}{% link getting-started/dates.md %}#-start),
 the result will include tasks without a start date.
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index 62ff8059..e1225534 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -17,10 +17,10 @@ This table summarizes the filters and other options available inside a `tasks` b
 | [Filters][1]                                                                                                                                                                                        | [Sort][2]                                   | [Group][3]            | [Display][4]           |
 | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------- | ---------------------- |
 | `done`<br>`not done`                                                                                                                                                                                | `sort by status`                            | `group by status`     |                        |
-| `done (before, after, on) <date>`<br>`has done date`<br>`no  done date`                                                                                                                             | `sort by done`                              | `group by done`       | `hide done date`       |
-| `starts (before, after, on) <date>`<br>`has start date`<br>`no  start date`                                                                                                                         | `sort by start`                             | `group by start`      | `hide start date`      |
-| `scheduled (before, after, on) <date>`<br>`has scheduled date`<br>`no  scheduled date`                                                                                                              | `sort by scheduled`                         | `group by scheduled`  | `hide scheduled date`  |
-| `due (before, after, on) <date>`<br>`has due date`<br>`no  due date`                                                                                                                                | `sort by due`                               | `group by due`        | `hide due date`        |
+| `done (before, after, on) <date>`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                                                                                                   | `sort by done`                              | `group by done`       | `hide done date`       |
+| `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`    |                        |
 | `is recurring`<br>`is not recurring`                                                                                                                                                                |                                             | `group by recurring`  |                        |
 |                                                                                                                                                                                                     |                                             | `group by recurrence` | `hide recurrence rule` |
diff --git a/src/Query/Filter/DateField.ts b/src/Query/Filter/DateField.ts
index 2a418a9c..ef6e7fa6 100644
--- a/src/Query/Filter/DateField.ts
+++ b/src/Query/Filter/DateField.ts
@@ -18,6 +18,10 @@ export abstract class DateField extends Field {
         this.filterInstructions = new FilterInstructions();
         this.filterInstructions.add(`has ${this.fieldName()} date`, (task: Task) => this.date(task) !== null);
         this.filterInstructions.add(`no ${this.fieldName()} date`, (task: Task) => this.date(task) === null);
+        this.filterInstructions.add(`${this.fieldName()} date is invalid`, (task: Task) => {
+            const date = this.date(task);
+            return date !== null && !date.isValid();
+        });
     }
 
     public canCreateFilterForLine(line: string): boolean {


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid Dates.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid Dates.md
new file mode 100644
index 00000000..9e9a7783
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid Dates.md
@@ -0,0 +1,37 @@
+# Invalid Dates
+
+## Sample Date
+
+### Tasks with valid dates
+
+- [x] #task Valid done date ✅ 2022-02-28
+- [ ] #task Valid due date 📅 2022-02-28
+- [ ] #task Valid scheduled date ⏳ 2022-02-28
+- [ ] #task Valid start date 🛫 2022-02-28
+
+### Tasks with invalid dates
+
+- [x] #task Invalid done date ✅ 2022-02-29
+- [ ] #task Invalid due date 📅 2022-02-29
+- [ ] #task Invalid scheduled date ⏳ 2022-02-29
+- [ ] #task Invalid start date 🛫 2022-02-29
+
+## Sample Searches
+
+### All tasks in this file
+
+```tasks
+path includes Manual Testing/Invalid Dates
+group by heading
+sort by description
+```
+
+### Search for tasks with invalid dates
+
+Tasks that have invalid dates: expect 4 matches.
+
+```tasks
+path includes Manual Testing/Invalid Dates
+(done date is invalid) OR (due date is invalid) OR (scheduled date is invalid) OR (start date is invalid)
+sort by description
+```
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 7e3fca69..743b93e9 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -18,10 +18,12 @@ describe('Query parsing', () => {
         'description includes wibble',
         'done after 2021-12-27',
         'done before 2021-12-27',
+        'done date is invalid',
         'done on 2021-12-27',
         'done',
         'due after 2021-12-27',
         'due before 2021-12-27',
+        'due date is invalid',
         'due on 2021-12-27',
         'exclude sub-items',
         'filename includes wibble',
@@ -55,7 +57,9 @@ describe('Query parsing', () => {
         'priority is none',
         'scheduled after 2021-12-27',
         'scheduled before 2021-12-27',
+        'scheduled date is invalid',
         'scheduled on 2021-12-27',
+        'start date is invalid',
         'starts after 2021-12-27',
         'starts before 2021-12-27',
         'starts on 2021-12-27',
diff --git a/tests/Query/Filter/DueDateField.test.ts b/tests/Query/Filter/DueDateField.test.ts
index c1c6b499..09c6f72b 100644
--- a/tests/Query/Filter/DueDateField.test.ts
+++ b/tests/Query/Filter/DueDateField.test.ts
@@ -25,4 +25,16 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-20', false);
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
+
+    it('due date is invalid', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due date is invalid');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-15', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-02-30', true); // 30 February is not valid
+        testTaskFilterForTaskWithDueDate(filter, '2022-00-01', true); // month 0 not valid
+        testTaskFilterForTaskWithDueDate(filter, '2022-13-01', true); // month 13 not valid
+    });
 });