Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2553 (2023-12-31 19:59:04)

feat: Add all Tasks query block instructions for Cancelled Dates
<!--- Provide a general summary of your changes in the Title above -->

# Description

This follows on from https://github.com/obsidian-tasks-group/obsidian-tasks/pull/2550.

It implements and documents the following facilities for Cancelled Dates:

- show/hide cancelled
- filtering, sorting and grouping by cancelled
- using the cancelled date in custom filters and groups

Internal changes:

- `Dates.md` - the docs on each date now link to the relevant sections in Filters, Sorting, Grouping and Task Properties, for easier access.
- New source file `tests/DocumentationSamples/ValidateTasks.test.ts` generates some more files for automatically embedding in the docs and test vault, for when new date fields and new emojis are added.
    - Consequently, `created date` is now included in the searches in `Find tasks with invalid data.md`.

### Not yet supported:

- Seeing the Cancelled Date in Edit Task Modal

## Motivation and Context


Next step in implementing #1855.

<!--- 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?

- Adding new tests
- Exploratory testing

<!--- 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. -->

## 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 **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/Getting Started/Dates.md b/docs/Getting Started/Dates.md
index 5747d7fd..fc6cf3cf 100644
--- a/docs/Getting Started/Dates.md
+++ b/docs/Getting Started/Dates.md
@@ -39,6 +39,13 @@ For example: `📅 2021-04-09` means the task is due on the 9th of April, 2021.
 - [ ] take out the trash 📅 2021-04-09
 ```
 
+Related instructions for use in Tasks query blocks:
+
+- [[Filters#Due Date]]
+- [[Sorting#Due Date]]
+- [[Grouping#Due Date]]
+- Accessible as [[Task Properties#Values for Dates in Tasks|task property]] `task.due`
+
 ---
 
 ### Scheduled date
@@ -57,6 +64,13 @@ See [[Use Filename as Default Date]] for how to optionally make Tasks use any da
 > [!released]
 'Use Filename as Default Date' was introduced in Tasks 1.18.0.
 
+Related instructions for use in Tasks query blocks:
+
+- [[Filters#Scheduled Date]]
+- [[Sorting#Scheduled Date]]
+- [[Grouping#Scheduled Date]]
+- Accessible as [[Task Properties#Values for Dates in Tasks|task property]] `task.scheduled`
+
 ---
 
 ### Start date
@@ -83,6 +97,13 @@ starts before tomorrow
 ```
 ````
 
+Related instructions for use in Tasks query blocks:
+
+- [[Filters#Start Date]]
+- [[Sorting#Start Date]]
+- [[Grouping#Start Date]]
+- Accessible as [[Task Properties#Values for Dates in Tasks|task property]] `task.start`
+
 ## Track task histories
 
 This section explains the types of dates that Tasks can add for you automatically.
@@ -110,6 +131,13 @@ Created dates use a heavy plus emoji: ➕.
 - [ ] take out the trash ➕ 2021-04-09
 ```
 
+Related instructions for use in Tasks query blocks:
+
+- [[Filters#Created Date]]
+- [[Sorting#Created Date]]
+- [[Grouping#Created Date]]
+- Accessible as [[Task Properties#Values for Dates in Tasks|task property]] `task.created`
+
 #### Adding Created date to tasks
 
 The following Tasks facilities add created dates to tasks...
@@ -136,6 +164,13 @@ Done dates use a white check-mark emoji: ✅.
 - [x] take out the trash ✅ 2021-04-09
 ```
 
+Related instructions for use in Tasks query blocks:
+
+- [[Filters#Done Date]]
+- [[Sorting#Done Date]]
+- [[Grouping#Done Date]]
+- Accessible as [[Task Properties#Values for Dates in Tasks|task property]] `task.done`
+
 ### Cancelled date
 
 > [!released]
@@ -151,6 +186,13 @@ Cancelled dates use a red cross-mark emoji: ❌.
 - [-] take out the trash ❌ 2021-04-09
 ```
 
+Related instructions for use in Tasks query blocks:
+
+- [[Filters#Cancelled Date]]
+- [[Sorting#Cancelled Date]]
+- [[Grouping#Cancelled Date]]
+- Accessible as [[Task Properties#Values for Dates in Tasks|task property]] `task.cancelled`
+
 ## Finding mistakes in dates on task lines
 
 Tasks does not automatically report any problem tasks that have invalid dates, such as on the 32nd day of a month. These task will silently not be found by date-based searches.
diff --git a/docs/How To/Find tasks with invalid data.md b/docs/How To/Find tasks with invalid data.md
index 7f32d581..cadcc23d 100644
--- a/docs/How To/Find tasks with invalid data.md
+++ b/docs/How To/Find tasks with invalid data.md
@@ -8,7 +8,7 @@ publish: true
 
 For performance reasons, Tasks is stricter in reading data from markdown lines that some users expect.
 
-As a precaution, it can be useful to check the data in your vault periodically, to see if there any tasks with seemingly valid data that is not found by Tasks.
+As a precaution, it can be useful to check the data in your vault periodically, to see if there are any tasks with seemingly valid data that is not found by Tasks.
 
 ## Find tasks with potentially missed emoji signifiers
 
@@ -28,12 +28,11 @@ Tasks sees as an un-dated task with the text description:
 
 The following tasks block lists any tasks that have emoji in the description, which usually means data that has not been interpreted by Tasks. This is typically because there is some text other than emoji signifiers and tags at the end of the line.
 
-<!-- NEW_TASK_FIELD_EDIT_REQUIRED -->
-
+<!-- include: ValidateTasks.test.validate-tasks_find-unread-emojis.approved.text -->
 ````text
 ```tasks
 # These description instructions need to be all on one line:
-(description includes ⏫ ) OR (description includes 🔼 ) OR (description includes 🔽 ) OR (description includes 📅 ) OR (description includes ⏳ ) OR (description includes 🛫 ) OR (description includes ✅ ) OR (description includes 🔁 )
+(description includes 🔺) OR (description includes ⏫) OR (description includes 🔼) OR (description includes 🔽) OR (description includes ⏬) OR (description includes 🛫) OR (description includes ➕) OR (description includes ⏳) OR (description includes 📅) OR (description includes ✅) OR (description includes ❌) OR (description includes 🔁)
 
 # Optionally, uncomment this line and exclude your templates location
 # path does not include _templates
@@ -41,6 +40,7 @@ The following tasks block lists any tasks that have emoji in the description, wh
 group by path
 ```
 ````
+<!-- endInclude -->
 
 The errors can be fixed by editing the Task and moving any text that appears after the unparsed values to earlier in the line.
 
@@ -58,11 +58,10 @@ This task has an illegal due date:
 
 The following tasks block lists any tasks with invalid dates, meaning data that has potentially not been interpreted by Tasks.
 
-<!-- NEW_QUERY_INSTRUCTION_EDIT_REQUIRED if the new query is for a new date field -->
-
+<!-- include: ValidateTasks.test.validate-tasks_find_problem_dates.approved.text -->
 ````text
 ```tasks
-(done date is invalid) OR (due date is invalid) OR (scheduled date is invalid) OR (start date is invalid)
+(cancelled date is invalid) OR (created date is invalid) OR (done date is invalid) OR (due date is invalid) OR (scheduled date is invalid) OR (start date is invalid)
 
 # Optionally, uncomment this line and exclude your templates location
 # path does not include _templates
@@ -70,6 +69,7 @@ The following tasks block lists any tasks with invalid dates, meaning data that
 group by path
 ```
 ````
+<!-- endInclude -->
 
 Errors with invalid dates are not easily fixed using the [[Create or edit Task|‘Create or edit Task’ Modal]], as it shows any invalid dates with placeholder text, instead of showing the original values.
 
diff --git a/docs/Queries/Filters.md b/docs/Queries/Filters.md
index d5c927f7..02d4d8b4 100644
--- a/docs/Queries/Filters.md
+++ b/docs/Queries/Filters.md
@@ -697,6 +697,44 @@ filter by function task.created.format('dddd') === 'Monday'
 
 For more examples, see [[#Due Date]].
 
+### Cancelled Date
+
+- `no cancelled date`
+- `has cancelled date`
+- `cancelled (on|before|after|on or before|on or after) <date>`
+- `cancelled (in|before|after|in or before|in or after) <date range>`
+  - `YYYY-MM-DD YYYY-MM-DD`
+  - `(last|this|next) (week|month|quarter|year)`
+  - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
+- `cancelled date is invalid`
+
+For more information, see [[Dates#Cancelled date|Cancelled date]].
+
+Such a filter could be:
+
+    ```tasks
+    cancelled yesterday
+    ```
+
+> [!released]
+>
+> - Cancelled date was introduced in Tasks X.Y.Z.
+
+Since Tasks X.Y.Z, **[[Custom Filters|custom filtering]] by cancelled date** is now possible, using `task.cancelled`.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomFilteringExamples.test.dates_task.cancelled_docs.approved.md -->
+
+```javascript
+filter by function task.cancelled.format('dddd') === 'Wednesday'
+```
+
+- Find tasks cancelled on Wednesdays, that is, any Wednesday.
+- On non-English systems, you may need to supply the day of the week in the local language.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+For more examples, see [[#Due Date]].
+
 ### Happens
 
 - `happens (on|before|after|on or before|on or after) <date>`
diff --git a/docs/Queries/Grouping.md b/docs/Queries/Grouping.md
index 6d1cbd70..e7686ff5 100644
--- a/docs/Queries/Grouping.md
+++ b/docs/Queries/Grouping.md
@@ -415,6 +415,28 @@ group by function task.created.format("YYYY-MM-DD dddd")
 
 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`.
+
+> [!released]
+`cancelled` grouping option was introduced in Tasks X.Y.Z.
+
+Since Tasks X.Y.Z, **[[Custom Grouping|custom grouping]] by cancelled date** is now possible, using `task.cancelled`.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomGroupingExamples.test.dates_task.cancelled_docs.approved.md -->
+
+```javascript
+group by function task.cancelled.format("YYYY-MM-DD dddd")
+```
+
+- Like "group by cancelled", except it uses an empty string instead of "No cancelled date" if there is no cancelled date.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+For more examples, see [[#Due Date]].
+
 ### Happens
 
 - `group by happens`
diff --git a/docs/Queries/Layout.md b/docs/Queries/Layout.md
index b6da99f9..80cf1a91 100644
--- a/docs/Queries/Layout.md
+++ b/docs/Queries/Layout.md
@@ -18,6 +18,7 @@ The following elements exist:
 - `backlink`
 - `urgency`
 - `priority`
+- `cancelled date`
 - `created date`
 - `start date`
 - `scheduled date`
@@ -36,9 +37,11 @@ The following elements exist:
 > 1. It is not possible to hide or show individual tags. We are tracking this in [discussion #848](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/848).
 
 > [!released]
-`urgency` was introduced in Tasks 1.14.0.<br>
-`created date` was introduced in Tasks 2.0.0.<br>
-`tags` was introduced in Tasks 4.1.0.
+>
+> - `urgency` was introduced in Tasks 1.14.0.
+> - `created date` was introduced in Tasks 2.0.0.
+> - `tags` was introduced in Tasks 4.1.0.
+> - `cancelled date` was introduced in Tasks X.Y.Z.
 
 All of these elements except `urgency` are shown by default, so you will use the command `hide`
 if you do not want to show any of them, or the command `show` to show the urgency score.
diff --git a/docs/Queries/Sorting.md b/docs/Queries/Sorting.md
index 99651f23..44687162 100644
--- a/docs/Queries/Sorting.md
+++ b/docs/Queries/Sorting.md
@@ -73,6 +73,13 @@ For more information, including adding your own customised statuses, see [[Statu
 > [!released]
 `sort by created` was introduced in Tasks 2.0.0.
 
+### Cancelled Date
+
+- `sort by cancelled` (the date when the task was cancelled)
+
+> [!released]
+`sort by cancelled` was introduced in Tasks X.Y.Z.
+
 ### Happens
 
 - `sort by happens` (the earliest of start date, scheduled date, and due date)
diff --git a/docs/Quick Reference.md b/docs/Quick Reference.md
index bf308c58..f34390cb 100644
--- a/docs/Quick Reference.md
+++ b/docs/Quick Reference.md
@@ -24,6 +24,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `starts (on, before, after, on or before, on or after) <date>`<br>`starts (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                   | `sort by start`                             | `group by start`       | `hide start date`      | `task.start`                                                                                                                   |
 | `scheduled (on, before, after, on or before, on or after) <date>`<br>`scheduled (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid` | `sort by scheduled`                         | `group by scheduled`   | `hide scheduled date`  | `task.scheduled`                                                                                                               |
 | `due (on, before, after, on or before, on or after) <date>`<br>`due (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                               | `sort by due`                               | `group by due`         | `hide due date`        | `task.due`                                                                                                                     |
+| `cancelled (on, before, after, on or before, on or after) <date>`<br>`cancelled (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has cancelled date`<br>`no  cancelled date`<br>`cancelled date is invalid` | `sort by cancelled`                         | `group by cancelled`   | `hide cancelled date`  | `task.cancelled`                                                                                                               |
 | `happens (on, before, after, on or before, on or after) <date>`<br>`happens (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has happens date`<br>`no  happens date`                                        | `sort by happens`                           | `group by happens`     |                        | `task.happens`                                                                                                                 |
 | **[[Filters#Recurrence\|Recurrence]]**                                                                                                                                                                                                                                                                                                                |                                             |                        |                        |                                                                                                                                |
 | `is recurring`<br>`is not recurring`                                                                                                                                                                                                                                                                                                                  | `sort by recurring`                         | `group by recurring`   |                        | `task.isRecurring`                                                                                                             |
diff --git a/docs/Scripting/Task Properties.md b/docs/Scripting/Task Properties.md
index d15dfa04..d3e4080c 100644
--- a/docs/Scripting/Task Properties.md
+++ b/docs/Scripting/Task Properties.md
@@ -49,6 +49,7 @@ For more information, including adding your own customised statuses, see [[Statu
 | `task.start` | `TasksDate` | `2023-07-02 00:00` | `TasksDate` | `` |
 | `task.scheduled` | `TasksDate` | `2023-07-03 00:00` | `TasksDate` | `` |
 | `task.due` | `TasksDate` | `2023-07-04 00:00` | `TasksDate` | `` |
+| `task.cancelled` | `TasksDate` | `2023-07-06 00:00` | `TasksDate` | `` |
 | `task.done` | `TasksDate` | `2023-07-05 00:00` | `TasksDate` | `` |
 | `task.happens` | `TasksDate` | `2023-07-02 00:00` | `TasksDate` | `` |
 
@@ -58,6 +59,7 @@ For more information, including adding your own customised statuses, see [[Statu
 1. Note that currently all stored dates have no time, or rather, their time is midnight at the start of the day, local time.
 1. For example uses of date properties, see [[Filters#Due Date]] and [[Grouping#Due Date]].
 1. `task.happens` is the earlier of `task.due`, `task.scheduled` and `task.start`.
+1. `task.cancelled` was added in Tasks X.Y.Z.
 
 ## Values in TasksDate Properties
 
diff --git a/src/Query/Filter/CancelledDateField.ts b/src/Query/Filter/CancelledDateField.ts
new file mode 100644
index 00000000..ebf473e8
--- /dev/null
+++ b/src/Query/Filter/CancelledDateField.ts
@@ -0,0 +1,18 @@
+import type { Moment } from 'moment';
+import type { Task } from '../../Task';
+import { DateField } from './DateField';
+
+/**
+ * Support the 'cancelled' search instruction.
+ */
+export class CancelledDateField extends DateField {
+    public fieldName(): string {
+        return 'cancelled';
+    }
+    public date(task: Task): Moment | null {
+        return task.cancelledDate;
+    }
+    protected filterResultIfFieldMissing() {
+        return false;
+    }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index ca26b0eb..617b53f1 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -27,6 +27,7 @@ import type { Grouper } from './Grouper';
 import { FolderField } from './Filter/FolderField';
 import { RootField } from './Filter/RootField';
 import { BacklinkField } from './Filter/BacklinkField';
+import { CancelledDateField } from './Filter/CancelledDateField';
 
 // When parsing a query the fields are tested one by one according to this order.
 // Since BooleanField is a meta-field, which needs to aggregate a few fields together, it is intended to
@@ -41,6 +42,7 @@ export const fieldCreators: EndsWith<BooleanField> = [
     () => new RecurringField(),
     () => new PriorityField(),
     () => new HappensDateField(),
+    () => new CancelledDateField(),
     () => new CreatedDateField(),
     () => new StartDateField(),
     () => new ScheduledDateField(),
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 0028e0d8..6227d659 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -34,7 +34,7 @@ export class Query implements IQuery {
     private _ignoreGlobalQuery: boolean = false;
 
     private readonly hideOptionsRegexp =
-        /^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|postpone button|urgency|tags)/i;
+        /^(hide|show) (task count|backlink|priority|cancelled date|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|postpone button|urgency|tags)/i;
     private readonly shortModeRegexp = /^short/i;
     private readonly fullModeRegexp = /^full/i;
     private readonly explainQueryRegexp = /^explain/i;
@@ -269,6 +269,9 @@ Problem line: "${line}"`;
                 case 'priority':
                     this._layoutOptions.hidePriority = hide;
                     break;
+                case 'cancelled date':
+                    this._layoutOptions.hideCancelledDate = hide;
+                    break;
                 case 'created date':
                     this._layoutOptions.hideCreatedDate = hide;
                     break;
diff --git a/src/Task.ts b/src/Task.ts
index 1a19c122..2b365399 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -576,6 +576,13 @@ export class Task {
         return this.taskLocation.path;
     }
 
+    /**
+     * Return {@link cancelledDate} as a {@link TasksDate}, so the field names in scripting docs are consistent with the existing search instruction names, and null values are easy to deal with.
+     */
+    public get cancelled(): TasksDate {
+        return new TasksDate(this.cancelledDate);
+    }
+
     /**
      * Return {@link createdDate} as a {@link TasksDate}, so the field names in scripting docs are consistent with the existing search instruction names, and null values are easy to deal with.
      */


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
index 84418392..13553854 100644
--- a/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid Dates.md
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Invalid Dates.md
@@ -4,6 +4,9 @@
 
 ### Tasks with valid dates
 
+<!-- NEW_TASK_FIELD_EDIT_REQUIRED -->
+
+- [-] #task Valid cancelled date ❌ 2022-02-28
 - [ ] #task Valid created date ➕ 2022-02-28
 - [x] #task Valid done date ✅ 2022-02-28
 - [ ] #task Valid due date 📅 2022-02-28
@@ -12,6 +15,9 @@
 
 ### Tasks with invalid dates
 
+<!-- NEW_TASK_FIELD_EDIT_REQUIRED -->
+
+- [-] #task Invalid cancelled date ❌ 2022-02-29
 - [ ] #task Invalid created date ➕ 2022-02-29
 - [x] #task Invalid done date ✅ 2022-02-29
 - [ ] #task Invalid due date 📅 2022-02-29
@@ -30,10 +36,13 @@ sort by description
 
 ### Search for tasks with invalid dates
 
-Tasks that have invalid dates: expect 5 matches.
+Tasks that have invalid dates: expect 6 matches.
+
+<!-- NEW_QUERY_INSTRUCTION_EDIT_REQUIRED -->
+<!-- the Boolean filter line below can be copied from 'docs/How To/Find tasks with invalid data.md' -->
 
 ```tasks
 path includes Manual Testing/Invalid Dates
-(created date is invalid) OR (done date is invalid) OR (due date is invalid) OR (scheduled date is invalid) OR (start date is invalid)
+(cancelled date is invalid) OR (created date is invalid) OR (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/DocumentationSamples/ValidateTasks.test.ts b/tests/DocumentationSamples/ValidateTasks.test.ts
new file mode 100644
index 00000000..fd44e759
--- /dev/null
+++ b/tests/DocumentationSamples/ValidateTasks.test.ts
@@ -0,0 +1,57 @@
+import { DEFAULT_SYMBOLS } from '../../src/TaskSerializer/DefaultTaskSerializer';
+import { verifyWithFileExtension } from '../TestingTools/ApprovalTestHelpers';
+import { Task } from '../../src/Task';
+
+describe('validate-tasks', () => {
+    it('find-unread-emojis', () => {
+        const allEmojis: string[] = [];
+
+        // All the priority emojis:
+        Object.values(DEFAULT_SYMBOLS.prioritySymbols).forEach((value) => {
+            if (value.length > 0) {
+                allEmojis.push(value);
+            }
+        });
+
+        // All the other field emojis:
+        Object.values(DEFAULT_SYMBOLS).forEach((value) => {
+            if (typeof value === 'string') {
+                allEmojis.push(value);
+            }
+        });
+
+        const descriptionInstructions = allEmojis.map((emoji) => `(description includes ${emoji})`);
+        const output = `
+\`\`\`\`text
+\`\`\`tasks
+# These description instructions need to be all on one line:
+${descriptionInstructions.join(' OR ')}
+
+# Optionally, uncomment this line and exclude your templates location
+# path does not include _templates
+
+group by path
+\`\`\`
+\`\`\`\`
+`;
+        verifyWithFileExtension(output, 'text');
+    });
+
+    it('find problem dates', () => {
+        const dateFields = Task.allDateFields();
+        const instructions = dateFields.sort().map((field) => `(${field.replace('Date', ' date')} is invalid)`);
+        const output = `
+\`\`\`\`text
+\`\`\`tasks
+${instructions.join(' OR ')}
+
+# Optionally, uncomment this line and exclude your templates location
+# path does not include _templates
+
+group by path
+\`\`\`
+\`\`\`\`
+`;
+        verifyWithFileExtension(output, 'text');
+    });
+});
diff --git a/tests/DocumentationSamples/ValidateTasks.test.validate-tasks_find-unread-emojis.approved.text b/tests/DocumentationSamples/ValidateTasks.test.validate-tasks_find-unread-emojis.approved.text
new file mode 100644
index 00000000..959c961f
--- /dev/null
+++ b/tests/DocumentationSamples/ValidateTasks.test.validate-tasks_find-unread-emojis.approved.text
@@ -0,0 +1,12 @@
+
+````text
+```tasks
+# These description instructions need to be all on one line:
+(description includes 🔺) OR (description includes ⏫) OR (description includes 🔼) OR (description includes 🔽) OR (description includes ⏬) OR (description includes 🛫) OR (description includes ➕) OR (description includes ⏳) OR (description includes 📅) OR (description includes ✅) OR (description includes ❌) OR (description includes 🔁)
+
+# Optionally, uncomment this line and exclude your templates location
+# path does not include _templates
+
+group by path
+```
+````
diff --git a/tests/DocumentationSamples/ValidateTasks.test.validate-tasks_find_problem_dates.approved.text b/tests/DocumentationSamples/ValidateTasks.test.validate-tasks_find_problem_dates.approved.text
new file mode 100644
index 00000000..6d9245a0
--- /dev/null
+++ b/tests/DocumentationSamples/ValidateTasks.test.validate-tasks_find_problem_dates.approved.text
@@ -0,0 +1,11 @@
+
+````text
+```tasks
+(cancelled date is invalid) OR (created date is invalid) OR (done date is invalid) OR (due date is invalid) OR (scheduled date is invalid) OR (start date is invalid)
+
+# Optionally, uncomment this line and exclude your templates location
+# path does not include _templates
+
+group by path
+```
+````
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 258e9de6..83889f6d 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -42,6 +42,12 @@ describe('Query parsing', () => {
     const filters: ReadonlyArray<string> = [
         // NEW_QUERY_INSTRUCTION_EDIT_REQUIRED
         '(due this week) AND (description includes Hello World)',
+        'cancelled after 2021-12-27',
+        'cancelled before 2021-12-27',
+        'cancelled date is invalid',
+        'cancelled in 2021-12-27 2021-12-29',
+        'cancelled on 2021-12-27',
+        'cancelled this week',
         'created after 2021-12-27',
         'created before 2021-12-27',
         'created date is invalid',
@@ -75,6 +81,7 @@ describe('Query parsing', () => {
         'happens in 2021-12-27 2021-12-29',
         'happens on 2021-12-27',
         'happens this week',
+        'has cancelled date',
         'has created date',
         'has done date',
         'has due date',
@@ -88,6 +95,7 @@ describe('Query parsing', () => {
         'heading includes wibble',
         'is not recurring',
         'is recurring',
+        'no cancelled date',
         'no created date',
         'no due date',
         'no happens date',
@@ -222,6 +230,8 @@ describe('Query parsing', () => {
         // In alphabetical order, please
         const filters: ReadonlyArray<string> = [
             // NEW_QUERY_INSTRUCTION_EDIT_REQUIRED
+            'sort by cancelled',
+            'sort by cancelled reverse',
             'sort by created',
             'sort by created reverse',
             'sort by description',
@@ -307,6 +317,8 @@ describe('Query parsing', () => {
             // NEW_QUERY_INSTRUCTION_EDIT_REQUIRED
             'group by backlink',
             'group by backlink reverse',
+            'group by cancelled',
+            'group by cancelled reverse',
             'group by created',
             'group by created reverse',
             'group by done',
@@ -399,6 +411,7 @@ describe('Query parsing', () => {
             'full',
             'full mode',
             'hide backlink',
+            'hide cancelled date',
             'hide created date',
             'hide done date',
             'hide due date',
@@ -418,6 +431,7 @@ describe('Query parsing', () => {
             'short',
             'short mode',
             'show backlink',
+            'show cancelled date',
             'show created date',
             'show done date',
             'show due date',
diff --git a/tests/Query/Filter/CancelledDateField.test.ts b/tests/Query/Filter/CancelledDateField.test.ts
new file mode 100644
index 00000000..74422867
--- /dev/null
+++ b/tests/Query/Filter/CancelledDateField.test.ts
@@ -0,0 +1,123 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { CancelledDateField } from '../../../src/Query/Filter/CancelledDateField';
+import type { FilterOrErrorMessage } from '../../../src/Query/Filter/FilterOrErrorMessage';
+import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { testFilter } from '../../TestingTools/FilterTestHelpers';
+import { expectTaskComparesAfter, expectTaskComparesBefore } from '../../CustomMatchers/CustomMatchersForSorting';
+import { SampleTasks } from '../../TestHelpers';
+
+window.moment = moment;
+
+function testTaskFilterForTaskWithCancelledDate(
+    filter: FilterOrErrorMessage,
+    cancelledDate: string | null,
+    expected: boolean,
+) {
+    const builder = new TaskBuilder();
+    testFilter(filter, builder.cancelledDate(cancelledDate), expected);
+}
+
+describe('cancelled date', () => {
+    it('by cancelled date presence', () => {
+        // Arrange
+        const filter = new CancelledDateField().createFilterOrErrorMessage('has cancelled date');
+
+        // Act, Assert
+        testTaskFilterForTaskWithCancelledDate(filter, null, false);
+        testTaskFilterForTaskWithCancelledDate(filter, '2022-04-15', true);
+    });
+
+    it('by cancelled date absence', () => {
+        // Arrange
+        const filter = new CancelledDateField().createFilterOrErrorMessage('no cancelled date');
+
+        // Act, Assert
+        testTaskFilterForTaskWithCancelledDate(filter, null, true);
+        testTaskFilterForTaskWithCancelledDate(filter, '2022-04-15', false);
+    });
+});
+
+describe('explain cancelled date queries', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2022, 0, 15)); // 2022-01-15
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it('should explain date before', () => {
+        const filterOrMessage = new CancelledDateField().createFilterOrErrorMessage('cancelled before 2023-01-02');
+        expect(filterOrMessage).toHaveExplanation('cancelled date is before 2023-01-02 (Monday 2nd January 2023)');
+    });
+
+    it('should explain date with explicit on', () => {
+        const filterOrMessage = new CancelledDateField().createFilterOrErrorMessage('cancelled on 2024-01-02');
+        expect(filterOrMessage).toHaveExplanation('cancelled date is on 2024-01-02 (Tuesday 2nd January 2024)');
+    });
+
+    it('should explain date with implicit on', () => {
+        const filterOrMessage = new CancelledDateField().createFilterOrErrorMessage('cancelled 2024-01-02');
+        expect(filterOrMessage).toHaveExplanation('cancelled date is on 2024-01-02 (Tuesday 2nd January 2024)');
+    });
+
+    it('should show value of relative dates', () => {
+        const filterOrMessage = new CancelledDateField().createFilterOrErrorMessage('cancelled after today');
+        expect(filterOrMessage).toHaveExplanation('cancelled date is after 2022-01-15 (Saturday 15th January 2022)');
+    });
+});
+
+describe('sorting by cancelled', () => {
+    it('supports Field sorting methods correctly', () => {
+        const field = new CancelledDateField();
+        expect(field.supportsSorting()).toEqual(true);
+    });
+
+    // These are minimal tests just to confirm basic behaviour is set up for this field.
+    // Thorough testing is cancelled in DueDateField.test.ts.
+
+    const date1 = new TaskBuilder().cancelledDate('2021-01-12').build();
+    const date2 = new TaskBuilder().cancelledDate('2022-12-23').build();
+
+    it('sort by cancelled', () => {
+        expectTaskComparesBefore(new CancelledDateField().createNormalSorter(), date1, date2);
+    });
+
+    it('sort by cancelled reverse', () => {
+        expectTaskComparesAfter(new CancelledDateField().createReverseSorter(), date1, date2);
+    });
+});
+
+describe('grouping by cancelled date', () => {
+    it('supports Field grouping methods correctly', () => {
+        expect(new CancelledDateField()).toSupportGroupingWithProperty('cancelled');
+    });
+
+    it('group by cancelled date', () => {
+        // Arrange
+        const grouper = new CancelledDateField().createNormalGrouper();
+        const taskWithDate = new TaskBuilder().cancelledDate('1970-01-01').build();
+        const taskWithoutDate = new TaskBuilder().build();
+
+        // Assert
+        expect({ grouper, tasks: [taskWithDate] }).groupHeadingsToBe(['1970-01-01 Thursday']);
+        expect({ grouper, tasks: [taskWithoutDate] }).groupHeadingsToBe(['No cancelled date']);
+    });
+
+    it('should sort groups for CancelledDateField', () => {
+        const grouper = new CancelledDateField().createNormalGrouper();
+        const tasks = SampleTasks.withAllRepresentativeCancelledDates();
+
+        expect({ grouper, tasks }).groupHeadingsToBe([
+            '2023-05-30 Tuesday',
+            '2023-05-31 Wednesday',
+            '2023-06-01 Thursday',
+            'Invalid date',
+            'No cancelled date',
+        ]);
+    });
+});
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.dates_task.cancelled_docs.approved.md b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.dates_task.cancelled_docs.approved.md
new file mode 100644
index 00000000..7b51ec1f
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.dates_task.cancelled_docs.approved.md
@@ -0,0 +1,12 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+filter by function task.cancelled.format('dddd') === 'Wednesday'
+```
+
+- Find tasks cancelled on Wednesdays, that is, any Wednesday.
+- On non-English systems, you may need to supply the day of the week in the local language.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.dates_task.cancelled_results.approved.txt b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.dates_task.cancelled_results.approved.txt
new file mode 100644
index 00000000..e213e792
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.dates_task.cancelled_results.approved.txt
@@ -0,0 +1,11 @@
+Results of custom filters
+
+
+
+filter by function task.cancelled.format('dddd') === 'Wednesday'
+Find tasks cancelled on Wednesdays, that is, any Wednesday.
+On non-English systems, you may need to supply the day of the week in the local language.
+=>
+- [ ] my description ❌ 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts
index 40c40407..687467a7 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.ts
@@ -32,6 +32,18 @@ describe('dates', () => {
         // DATE FIELDS
         // ---------------------------------------------------------------------------------
 
+        [
+            'task.cancelled',
+            [
+                [
+                    "filter by function task.cancelled.format('dddd') === 'Wednesday'",
+                    'Find tasks cancelled on Wednesdays, that is, any Wednesday.',
+                    'On non-English systems, you may need to supply the day of the week in the local language',
+                ],
+            ],
+            SampleTasks.withAllRepresentativeCancelledDates(),
+        ],
+
         [
             'task.created',
             [
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.dates_task.cancelled_docs.approved.md b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.dates_task.cancelled_docs.approved.md
new file mode 100644
index 00000000..f691d385
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.dates_task.cancelled_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+group by function task.cancelled.format("YYYY-MM-DD dddd")
+```
+
+- Like "group by cancelled", except it uses an empty string instead of "No cancelled date" if there is no cancelled date.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.dates_task.cancelled_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.dates_task.cancelled_results.approved.txt
new file mode 100644
index 00000000..13d90863
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.dates_task.cancelled_results.approved.txt
@@ -0,0 +1,13 @@
+Results of custom groupers
+
+
+
+group by function task.cancelled.format("YYYY-MM-DD dddd")
+Like "group by cancelled", except it uses an empty string instead of "No cancelled date" if there is no cancelled date.
+=>
+2023-05-30 Tuesday
+2023-05-31 Wednesday
+2023-06-01 Thursday
+Invalid date
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts
index a2b0c91b..8b2f03c2 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.ts
@@ -32,6 +32,17 @@ describe('dates', () => {
         // DATE FIELDS
         // ---------------------------------------------------------------------------------
 
+        [
+            'task.cancelled',
+            [
+                [
+                    'group by function task.cancelled.format("YYYY-MM-DD dddd")',
+                    'Like "group by cancelled", except it uses an empty string instead of "No cancelled date" if there is no cancelled date',
+                ],
+            ],
+            SampleTasks.withAllRepresentativeCancelledDates(),
+        ],
+
         [
             'task.created',
             [
diff --git a/tests/Scripting/TaskProperties.test.task_dates.approved.md b/tests/Scripting/TaskProperties.test.task_dates.approved.md
index be634e21..627f0636 100644
--- a/tests/Scripting/TaskProperties.test.task_dates.approved.md
+++ b/tests/Scripting/TaskProperties.test.task_dates.approved.md
@@ -6,6 +6,7 @@
 | `task.start` | `TasksDate` | `2023-07-02 00:00` | `TasksDate` | `` |
 | `task.scheduled` | `TasksDate` | `2023-07-03 00:00` | `TasksDate` | `` |
 | `task.due` | `TasksDate` | `2023-07-04 00:00` | `TasksDate` | `` |
+| `task.cancelled` | `TasksDate` | `2023-07-06 00:00` | `TasksDate` | `` |
 | `task.done` | `TasksDate` | `2023-07-05 00:00` | `TasksDate` | `` |
 | `task.happens` | `TasksDate` | `2023-07-02 00:00` | `TasksDate` | `` |
 
diff --git a/tests/Scripting/TaskProperties.test.ts b/tests/Scripting/TaskProperties.test.ts
index 2ee60589..01f42656 100644
--- a/tests/Scripting/TaskProperties.test.ts
+++ b/tests/Scripting/TaskProperties.test.ts
@@ -63,6 +63,7 @@ describe('task', () => {
             'task.start',
             'task.scheduled',
             'task.due',
+            'task.cancelled',
             'task.done',
             'task.happens',
         ]);
diff --git a/tests/TestHelpers.ts b/tests/TestHelpers.ts
index dffa96ac..f6358747 100644
--- a/tests/TestHelpers.ts
+++ b/tests/TestHelpers.ts
@@ -168,6 +168,12 @@ export class SampleTasks {
         });
     }
 
+    public static withAllRepresentativeCancelledDates(): Task[] {
+        return representativeDates.map((date) => {
+            return new TaskBuilder().cancelledDate(date).build();
+        });
+    }
+
     public static withEachDateTypeAndCorrespondingStatus(): Task[] {
         function desc(fieldName: string) {
             return `#task Has a ${fieldName} date`;