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

Issue:
Add filtering, sorting and grouping for created date
### ⚠️ Please check that this feature request hasn't been suggested before.

- [X] I searched previous [Ideas in Discussions](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/categories/ideas-any-new-feature-requests-go-in-issues-please?discussions_q=category%3A%22Ideas%3A+Any+New+Feature+Requests+go+in+Issues+please%22+-label%3A%22status%3A+released%22+-label%3Aduplicate+sort%3Atop) didn't find any similar feature requests.
- [X] I searched previous [Issues](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+enhancement%22+sort%3Acomments-desc) didn't find any similar feature requests.

### 🔖 Feature description

This is to remind me to implement  filtering, sorting and grouping for the new created date field that was added in #1723.

This ticket is actually a duplicated of #98, but discussions cannot be added to GitHub projects.

### ✔️ Solution

- Implement filter and sort using the `DateField` implementation
    - Create a new `CreateDateField` class, inheriting `DateField`
    - Add a corresponding test file, with minimal tests
- Implement grouping using `DateField`
    - Refactor the date-grouping code to use the new interface in `Field`
    - Then grouping comes for free
- Update the docs (`filters.md`, `sorting.md`, `grouping.md` and `quick reference`)


### ❓ Alternatives

_No response_

### 📝 Additional Context

_No response_

Pull request:
feat: Add filter, group and sorting for created date
<!--- Provide a general summary of your changes in the Title above -->

# Description

Provide new query commands:
* group by created date
* sort by created date
* filter with created date
<!--- Describe your changes in detail -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
I don't move group functionality from Grouper to DateField files, because it was not straightforward approach with current code. I think refactoring needs to be done via separated task. 

<!--- If it fixes an open issue, please link to the issue here. -->
Closes #1741 for filtering, grouping and sorting with created date field.

## How has this been tested?

With unit tests, based on issue description.

<!--- 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:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [ ] **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:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## 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/filters.md b/docs/queries/filters.md
index 4302be39..390fcd3c 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -232,6 +232,23 @@ Such filter could be:
     starts before tomorrow
     ```
 
+### Created Date
+
+- `no created date`
+- `has created date`
+- `created (before|after|on) <date>`
+- `created date is invalid`
+
+When filtering queries by [start date]({{ site.baseurl }}{% link getting-started/dates.md %}#-start),
+the result will include tasks without a start date.
+This way, you can use the start date as a filter to filter out any tasks that you cannot yet work on.
+
+Such filter could be:
+
+    ```tasks
+    created before tomorrow
+    ```
+
 ### Happens
 
 - `happens (before|after|on) <date>`
diff --git a/docs/queries/grouping.md b/docs/queries/grouping.md
index d892ab97..0fbae721 100644
--- a/docs/queries/grouping.md
+++ b/docs/queries/grouping.md
@@ -50,8 +50,10 @@ You can group by the following properties.
 
 ### Task date properties
 
+1. `created`
+    - The created date of the task, including the week-day, or `No created date`.
 1. `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 `No start date`.
 1. `scheduled`
     - The scheduled date of the task, including the week-day, or `No scheduled date`.
 1. `due`
diff --git a/docs/queries/sorting.md b/docs/queries/sorting.md
index 2f3af7a7..1ea3260a 100644
--- a/docs/queries/sorting.md
+++ b/docs/queries/sorting.md
@@ -47,6 +47,7 @@ You can sort tasks by the following properties.
 
 ### Task date properties
 
+1. `created` (the date when the task was created)
 1. `start` (the date when the task starts)
 1. `scheduled` (the date when the task is scheduled)
 1. `due` (the date when the task is due)
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index acb6fb4a..b86e5d2d 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -21,7 +21,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `done (before, after, on) <date>`<br>`done (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`done (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                                                                                                   | `sort by done`                              | `group by done`        | `hide done date`       |
 | `status.name (includes, does not include) <string>`<br>`status.name (regex matches, regex does not match) /regex/i`                                                                                 | `sort by status.name`                       | `group by status.name` |                        |
 | `status.type (is, is not) (TODO, DONE, IN_PROGRESS, CANCELLED, NON_TASK)`                                                                                                                           | `sort by status.type`                       | `group by status.type` |                        |
-|                                                                                                                                                                                                     |                                             |                        | `hide created date`    |
+| `created (before, after, on) <date>`<br>`has created date`<br>`no  created date`<br>`created date is invalid`                                                                                              | `sort by created`                             | `group by created` | `hide created date`    |
 | `starts (before, after, on) <date>`<br>`starts (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`starts (before, after, in) (last, this, next) (week, month, quarter, year)`<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>`scheduled (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`scheduled (before, after, in) (last, this, next) (week, month, quarter, year)`<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>`due (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`due (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                                                                                                       | `sort by due`                               | `group by due`         | `hide due date`        |
diff --git a/src/Query/Filter/CreatedDateField.ts b/src/Query/Filter/CreatedDateField.ts
new file mode 100644
index 00000000..5d69e70e
--- /dev/null
+++ b/src/Query/Filter/CreatedDateField.ts
@@ -0,0 +1,18 @@
+import type { Moment } from 'moment';
+import type { Task } from '../../Task';
+import { DateField } from './DateField';
+
+/**
+ * Support the 'created' search instruction.
+ */
+export class CreatedDateField extends DateField {
+    public fieldName(): string {
+        return 'created';
+    }
+    public date(task: Task): Moment | null {
+        return task.createdDate;
+    }
+    protected filterResultIfFieldMissing() {
+        return false;
+    }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index 965bdda3..0c952b5a 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -1,4 +1,5 @@
 import { DescriptionField } from './Filter/DescriptionField';
+import { CreatedDateField } from './Filter/CreatedDateField';
 import { DoneDateField } from './Filter/DoneDateField';
 import { DueDateField } from './Filter/DueDateField';
 import { ExcludeSubItemsField } from './Filter/ExcludeSubItemsField';
@@ -29,6 +30,7 @@ const fieldCreators = [
     () => new RecurringField(),
     () => new PriorityField(),
     () => new HappensDateField(),
+    () => new CreatedDateField(),
     () => new StartDateField(),
     () => new ScheduledDateField(),
     () => new DueDateField(),
diff --git a/src/Query/Group.ts b/src/Query/Group.ts
index 457f8c26..377caf5a 100644
--- a/src/Query/Group.ts
+++ b/src/Query/Group.ts
@@ -42,6 +42,7 @@ export class Group {
 
     private static groupers: Record<GroupingProperty, GrouperFunction> = {
         backlink: Group.groupByBacklink,
+        created: Group.groupByCreatedDate,
         done: Group.groupByDoneDate,
         due: Group.groupByDueDate,
         filename: Group.groupByFileName,
@@ -99,6 +100,10 @@ export class Group {
         }
     }
 
+    private static groupByCreatedDate(task: Task): string[] {
+        return [Group.stringFromDate(task.createdDate, 'created')];
+    }
+
     private static groupByStartDate(task: Task): string[] {
         return [Group.stringFromDate(task.startDate, 'start')];
     }
diff --git a/src/Query/Grouper.ts b/src/Query/Grouper.ts
index e83ac100..204b7875 100644
--- a/src/Query/Grouper.ts
+++ b/src/Query/Grouper.ts
@@ -7,6 +7,7 @@ export type GrouperFunction = (task: Task) => string[];
 
 export type GroupingProperty =
     | 'backlink'
+    | 'created'
     | 'done'
     | 'due'
     | 'filename'
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 10e0c17d..35b756af 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -22,7 +22,7 @@ export class Query implements IQuery {
     private _grouping: Grouper[] = [];
 
     private readonly groupByRegexp =
-        /^group by (backlink|done|due|filename|folder|happens|heading|path|priority|recurrence|recurring|root|scheduled|start|status|tags)/;
+        /^group by (backlink|created|done|due|filename|folder|happens|heading|path|priority|recurrence|recurring|root|scheduled|start|status|tags)/;
 
     private readonly hideOptionsRegexp =
         /^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|urgency)/;


Test patch:
diff --git a/tests/Group.test.ts b/tests/Group.test.ts
index f53c8e3a..23c0fc22 100644
--- a/tests/Group.test.ts
+++ b/tests/Group.test.ts
@@ -535,6 +535,19 @@ describe('Group names', () => {
             expectedGroupNames: ['No start date'],
         },
 
+        // -----------------------------------------------------------
+        // group by created
+        {
+            groupBy: 'created',
+            taskLine: '- [ ] a ➕ 1970-01-01',
+            expectedGroupNames: ['1970-01-01 Thursday'],
+        },
+        {
+            groupBy: 'created',
+            taskLine: '- [ ] a',
+            expectedGroupNames: ['No created date'],
+        },
+
         // -----------------------------------------------------------
         // group by status
         {
diff --git a/tests/Query/Filter/CreatedDateField.test.ts b/tests/Query/Filter/CreatedDateField.test.ts
new file mode 100644
index 00000000..cba68b5f
--- /dev/null
+++ b/tests/Query/Filter/CreatedDateField.test.ts
@@ -0,0 +1,91 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { CreatedDateField } from '../../../src/Query/Filter/CreatedDateField';
+import type { FilterOrErrorMessage } from '../../../src/Query/Filter/Filter';
+import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { testFilter } from '../../TestingTools/FilterTestHelpers';
+import {
+    expectTaskComparesAfter,
+    expectTaskComparesBefore,
+    expectTaskComparesEqual,
+} from '../../CustomMatchers/CustomMatchersForSorting';
+
+window.moment = moment;
+
+function testTaskFilterForTaskWithCreatedDate(
+    filter: FilterOrErrorMessage,
+    createdDate: string | null,
+    expected: boolean,
+) {
+    const builder = new TaskBuilder();
+    testFilter(filter, builder.createdDate(createdDate), expected);
+}
+
+describe('created date', () => {
+    it('by created date (before)', () => {
+        // Arrange
+        const filter = new CreatedDateField().createFilterOrErrorMessage('created before 2022-04-20');
+
+        // Act, Assert
+        testTaskFilterForTaskWithCreatedDate(filter, null, false);
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-04-15', true);
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-04-20', false);
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-04-25', false);
+    });
+
+    it('created date is invalid', () => {
+        // Arrange
+        const filter = new CreatedDateField().createFilterOrErrorMessage('created date is invalid');
+
+        // Act, Assert
+        testTaskFilterForTaskWithCreatedDate(filter, null, false);
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-04-15', false);
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-02-30', true); // 30 February is not valid
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-00-01', true); // month 0 not valid
+        testTaskFilterForTaskWithCreatedDate(filter, '2022-13-01', true); // month 13 not valid
+    });
+});
+
+describe('explain created date queries', () => {
+    it('should explain explicit date', () => {
+        const filterOrMessage = new CreatedDateField().createFilterOrErrorMessage('created before 2023-01-02');
+        expect(filterOrMessage).toHaveExplanation('created date is before 2023-01-02 (Monday 2nd January 2023)');
+    });
+
+    it('implicit "on" gets added to explanation', () => {
+        const filterOrMessage = new CreatedDateField().createFilterOrErrorMessage('created 2023-01-02');
+        expect(filterOrMessage).toHaveExplanation('created date is on 2023-01-02 (Monday 2nd January 2023)');
+    });
+});
+
+describe('sorting by created', () => {
+    const date1 = new TaskBuilder().createdDate('2021-01-12').build();
+    const date2 = new TaskBuilder().createdDate('2022-12-23').build();
+
+    it('supports Field sorting methods correctly', () => {
+        const field = new CreatedDateField();
+        expect(field.supportsSorting()).toEqual(true);
+    });
+
+    it('sort by created', () => {
+        // Arrange
+        const sorter = new CreatedDateField().createNormalSorter();
+
+        // Assert
+        expectTaskComparesBefore(sorter, date1, date2);
+        expectTaskComparesAfter(sorter, date2, date1);
+        expectTaskComparesEqual(sorter, date2, date2);
+    });
+
+    it('sort by created reverse', () => {
+        // Arrange
+        const sorter = new CreatedDateField().createReverseSorter();
+
+        // Assert
+        expectTaskComparesAfter(sorter, date1, date2);
+        expectTaskComparesBefore(sorter, date2, date1);
+        expectTaskComparesEqual(sorter, date2, date2);
+    });
+});