Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1870 (2023-04-11 18:50:52)

refactor: Move code for grouping by dates to DateField
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

## Motivation and Context

Following the new grouping abstraction the DateField classes are migrating on it.

Done by pairing with @claremacrae 

## How has this been tested?

Unit tests inspired from Group.ts and moved to each class tests.

## 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)
- [ ] **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)
- [ ] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [ ] **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))
- [ ] **Contributing Guidelines** (prefix: `contrib` - any improvements to documentation content **for contributors** - see [Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] **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`.
- [ ] My change requires a change to the documentation.
- [ ] 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/src/Query/Filter/DateField.ts b/src/Query/Filter/DateField.ts
index 85c104ad..12ce412a 100644
--- a/src/Query/Filter/DateField.ts
+++ b/src/Query/Filter/DateField.ts
@@ -5,6 +5,7 @@ import { DateParser } from '../DateParser';
 import { Explanation } from '../Explain/Explanation';
 import type { Comparator } from '../Sorter';
 import { compareByDate } from '../../lib/DateTools';
+import type { GrouperFunction } from '../Grouper';
 import { Field } from './Field';
 import { Filter, type FilterFunction, FilterOrErrorMessage } from './Filter';
 import { FilterInstructions } from './FilterInstructions';
@@ -199,4 +200,18 @@ export abstract class DateField extends Field {
             return compareByDate(this.date(a), this.date(b));
         };
     }
+
+    public supportsGrouping(): boolean {
+        return true;
+    }
+
+    public grouper(): GrouperFunction {
+        return (task: Task) => {
+            const date = this.date(task);
+            if (date === null) {
+                return ['No ' + this.fieldName() + ' date'];
+            }
+            return [date.format('YYYY-MM-DD dddd')];
+        };
+    }
 }
diff --git a/src/Query/Group.ts b/src/Query/Group.ts
index 525a53f6..3ad6dd5f 100644
--- a/src/Query/Group.ts
+++ b/src/Query/Group.ts
@@ -1,7 +1,6 @@
 import type { Task } from '../Task';
 import { Priority } from '../Task';
 import { TaskGroups } from './TaskGroups';
-import { HappensDateField } from './Filter/HappensDateField';
 import { Grouper } from './Grouper';
 import type { GrouperFunction, GroupingProperty } from './Grouper';
 
@@ -9,8 +8,6 @@ import type { GrouperFunction, GroupingProperty } from './Grouper';
  * Implementation of the 'group by' instruction.
  */
 export class Group {
-    private static readonly groupDateFormat = 'YYYY-MM-DD dddd';
-
     public static fromGroupingProperty(property: GroupingProperty): Grouper {
         return new Grouper(property, Group.grouperForProperty(property));
     }
@@ -42,20 +39,14 @@ export class Group {
 
     private static groupers: Record<GroupingProperty, GrouperFunction> = {
         backlink: Group.groupByBacklink,
-        created: Group.groupByCreatedDate,
-        done: Group.groupByDoneDate,
-        due: Group.groupByDueDate,
         filename: Group.groupByFileName,
         folder: Group.groupByFolder,
-        happens: Group.groupByHappensDate,
         heading: Group.groupByHeading,
         path: Group.groupByPath,
         priority: Group.groupByPriority,
         recurrence: Group.groupByRecurrence,
         recurring: Group.groupByRecurring,
         root: Group.groupByRoot,
-        scheduled: Group.groupByScheduledDate,
-        start: Group.groupByStartDate,
         status: Group.groupByStatus,
         tags: Group.groupByTags,
     };
@@ -100,38 +91,6 @@ 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')];
-    }
-
-    private static groupByScheduledDate(task: Task): string[] {
-        return [Group.stringFromDate(task.scheduledDate, 'scheduled')];
-    }
-
-    private static groupByDueDate(task: Task): string[] {
-        return [Group.stringFromDate(task.dueDate, 'due')];
-    }
-
-    private static groupByDoneDate(task: Task): string[] {
-        return [Group.stringFromDate(task.doneDate, 'done')];
-    }
-
-    private static groupByHappensDate(task: Task): string[] {
-        const earliestDateIfAny = new HappensDateField().earliestDate(task);
-        return [Group.stringFromDate(earliestDateIfAny, 'happens')];
-    }
-
-    private static stringFromDate(date: moment.Moment | null, field: string): string {
-        if (date === null) {
-            return 'No ' + field + ' date';
-        }
-        return date.format(Group.groupDateFormat);
-    }
-
     private static groupByPath(task: Task): string[] {
         // Does this need to be made stricter?
         // Is there a better way of getting the file name?
diff --git a/src/Query/Grouper.ts b/src/Query/Grouper.ts
index 204b7875..80371084 100644
--- a/src/Query/Grouper.ts
+++ b/src/Query/Grouper.ts
@@ -7,20 +7,14 @@ export type GrouperFunction = (task: Task) => string[];
 
 export type GroupingProperty =
     | 'backlink'
-    | 'created'
-    | 'done'
-    | 'due'
     | 'filename'
     | 'folder'
-    | 'happens'
     | 'heading'
     | 'path'
     | 'priority'
     | 'recurrence'
     | 'recurring'
     | 'root'
-    | 'scheduled'
-    | 'start'
     | 'status'
     | 'tags';
 
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index 61c0037e..592fcc53 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -23,7 +23,7 @@ export class Query implements IQuery {
     private _grouping: Grouper[] = [];
 
     private readonly groupByRegexp =
-        /^group by (backlink|created|done|due|filename|folder|happens|heading|path|priority|recurrence|recurring|root|scheduled|start|status|tags)/;
+        /^group by (backlink|filename|folder|heading|path|priority|recurrence|recurring|root|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 a6e03d52..31dc42dd 100644
--- a/tests/Group.test.ts
+++ b/tests/Group.test.ts
@@ -270,32 +270,6 @@ describe('Group names', () => {
             precedingHeading: 'heading _italic text_',
         },
 
-        // -----------------------------------------------------------
-        // group by done
-        {
-            groupBy: 'done',
-            taskLine: '- [ ] a ✅ 1970-01-01',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            groupBy: 'done',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['No done date'],
-        },
-
-        // -----------------------------------------------------------
-        // group by due
-        {
-            groupBy: 'due',
-            taskLine: '- [ ] a 📅 1970-01-01',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            groupBy: 'due',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['No due date'],
-        },
-
         // -----------------------------------------------------------
         // group by filename
         {
@@ -333,47 +307,6 @@ describe('Group names', () => {
             path: 'a.md',
         },
 
-        // -----------------------------------------------------------
-        // group by happens
-        {
-            groupBy: 'happens',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['No happens date'],
-        },
-        {
-            groupBy: 'happens',
-            taskLine: '- [ ] due is only date 📅 1970-01-01',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            groupBy: 'happens',
-            taskLine: '- [ ] scheduled is only date ⏳ 1970-01-02',
-            expectedGroupNames: ['1970-01-02 Friday'],
-        },
-        {
-            groupBy: 'happens',
-            taskLine: '- [ ] start is only date 🛫 1970-01-03',
-            expectedGroupNames: ['1970-01-03 Saturday'],
-        },
-        {
-            // Check that earliest date is prioritised: due
-            groupBy: 'happens',
-            taskLine: '- [ ] due is earliest date 🛫 1970-01-03 ⏳ 1970-01-02 📅 1970-01-01',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            // Check that earliest date is prioritised: scheduled
-            groupBy: 'happens',
-            taskLine: '- [ ] scheduled is earliest date 🛫 1970-01-03 ⏳ 1970-01-01 📅 1970-01-02',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            // Check that earliest date is prioritised: start
-            groupBy: 'happens',
-            taskLine: '- [ ] start is earliest date 🛫 1970-01-01 ⏳ 1970-01-02 📅 1970-01-03',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-
         // -----------------------------------------------------------
         // group by heading
         {
@@ -509,45 +442,6 @@ describe('Group names', () => {
             path: 'a.md',
         },
 
-        // -----------------------------------------------------------
-        // group by scheduled
-        {
-            groupBy: 'scheduled',
-            taskLine: '- [ ] a ⏳ 1970-01-01',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            groupBy: 'scheduled',
-            taskLine: '- [ ] a',
-            expectedGroupNames: ['No scheduled date'],
-        },
-
-        // -----------------------------------------------------------
-        // group by start
-        {
-            groupBy: 'start',
-            taskLine: '- [ ] a 🛫 1970-01-01',
-            expectedGroupNames: ['1970-01-01 Thursday'],
-        },
-        {
-            groupBy: 'start',
-            taskLine: '- [ ] a',
-            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
index cba68b5f..85d9cfb7 100644
--- a/tests/Query/Filter/CreatedDateField.test.ts
+++ b/tests/Query/Filter/CreatedDateField.test.ts
@@ -89,3 +89,21 @@ describe('sorting by created', () => {
         expectTaskComparesEqual(sorter, date2, date2);
     });
 });
+
+describe('grouping by created date', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new CreatedDateField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it('group by created date', () => {
+        // Arrange
+        const grouper = new CreatedDateField().createGrouper();
+        const taskWithDate = new TaskBuilder().createdDate('1970-01-01').build();
+        const taskWithoutDate = new TaskBuilder().build();
+
+        // Assert
+        expect(grouper.grouper(taskWithDate)).toEqual(['1970-01-01 Thursday']);
+        expect(grouper.grouper(taskWithoutDate)).toEqual(['No created date']);
+    });
+});
diff --git a/tests/Query/Filter/DoneDateField.test.ts b/tests/Query/Filter/DoneDateField.test.ts
index b8cf9c72..039b07ba 100644
--- a/tests/Query/Filter/DoneDateField.test.ts
+++ b/tests/Query/Filter/DoneDateField.test.ts
@@ -86,3 +86,21 @@ describe('sorting by done', () => {
         expectTaskComparesAfter(new DoneDateField().createReverseSorter(), date1, date2);
     });
 });
+
+describe('grouping by done date', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new DoneDateField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it('group by done date', () => {
+        // Arrange
+        const grouper = new DoneDateField().createGrouper();
+        const taskWithDate = new TaskBuilder().doneDate('1970-01-01').build();
+        const taskWithoutDate = new TaskBuilder().build();
+
+        // Assert
+        expect(grouper.grouper(taskWithDate)).toEqual(['1970-01-01 Thursday']);
+        expect(grouper.grouper(taskWithoutDate)).toEqual(['No done date']);
+    });
+});
diff --git a/tests/Query/Filter/DueDateField.test.ts b/tests/Query/Filter/DueDateField.test.ts
index 7102e3e5..6abf2658 100644
--- a/tests/Query/Filter/DueDateField.test.ts
+++ b/tests/Query/Filter/DueDateField.test.ts
@@ -515,3 +515,21 @@ describe('due date', () => {
         table.verify();
     });
 });
+
+describe('grouping by due date', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new DueDateField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it('group by due date', () => {
+        // Arrange
+        const grouper = new DueDateField().createGrouper();
+        const taskWithDate = new TaskBuilder().dueDate('1970-01-01').build();
+        const taskWithoutDate = new TaskBuilder().build();
+
+        // Assert
+        expect(grouper.grouper(taskWithDate)).toEqual(['1970-01-01 Thursday']);
+        expect(grouper.grouper(taskWithoutDate)).toEqual(['No due date']);
+    });
+});
diff --git a/tests/Query/Filter/HappensDateField.test.ts b/tests/Query/Filter/HappensDateField.test.ts
index 1022e5ab..0d2221cb 100644
--- a/tests/Query/Filter/HappensDateField.test.ts
+++ b/tests/Query/Filter/HappensDateField.test.ts
@@ -6,6 +6,7 @@ import { HappensDateField } from '../../../src/Query/Filter/HappensDateField';
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
 import { testFilter } from '../../TestingTools/FilterTestHelpers';
 import * as CustomMatchersForSorting from '../../CustomMatchers/CustomMatchersForSorting';
+import { fromLine } from '../../TestHelpers';
 
 window.moment = moment;
 
@@ -133,3 +134,26 @@ describe('sorting by happens', () => {
         CustomMatchersForSorting.expectTaskComparesAfter(new HappensDateField().createReverseSorter(), date1, date2);
     });
 });
+
+describe('grouping by happens date', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new HappensDateField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it.each([
+        ['- [ ] a', ['No happens date']],
+        ['- [ ] due is only date 📅 1970-01-01', ['1970-01-01 Thursday']],
+        ['- [ ] scheduled is only date ⏳ 1970-01-02', ['1970-01-02 Friday']],
+        ['- [ ] start is only date 🛫 1970-01-03', ['1970-01-03 Saturday']],
+        ['- [ ] due is earliest date 🛫 1970-01-03 ⏳ 1970-01-02 📅 1970-01-01', ['1970-01-01 Thursday']],
+        ['- [ ] scheduled is earliest date 🛫 1970-01-03 ⏳ 1970-01-01 📅 1970-01-02', ['1970-01-01 Thursday']],
+        ['- [ ] start is earliest date 🛫 1970-01-01 ⏳ 1970-01-02 📅 1970-01-03', ['1970-01-01 Thursday']],
+    ])('group by happens date: task "%s" should have groups %s', (taskLine: string, expectedResult: string[]) => {
+        // Arrange
+        const grouper = new HappensDateField().createGrouper();
+
+        // Assert
+        expect(grouper.grouper(fromLine({ line: taskLine }))).toEqual(expectedResult);
+    });
+});
diff --git a/tests/Query/Filter/ScheduledDateField.test.ts b/tests/Query/Filter/ScheduledDateField.test.ts
index 9750f047..610870ba 100644
--- a/tests/Query/Filter/ScheduledDateField.test.ts
+++ b/tests/Query/Filter/ScheduledDateField.test.ts
@@ -40,3 +40,21 @@ describe('sorting by scheduled', () => {
         expectTaskComparesAfter(new ScheduledDateField().createReverseSorter(), date1, date2);
     });
 });
+
+describe('grouping by scheduled date', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new ScheduledDateField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it('group by scheduled date', () => {
+        // Arrange
+        const grouper = new ScheduledDateField().createGrouper();
+        const taskWithDate = new TaskBuilder().scheduledDate('1970-01-01').build();
+        const taskWithoutDate = new TaskBuilder().build();
+
+        // Assert
+        expect(grouper.grouper(taskWithDate)).toEqual(['1970-01-01 Thursday']);
+        expect(grouper.grouper(taskWithoutDate)).toEqual(['No scheduled date']);
+    });
+});
diff --git a/tests/Query/Filter/StartDateField.test.ts b/tests/Query/Filter/StartDateField.test.ts
index 33c3fe2a..ad9fabaf 100644
--- a/tests/Query/Filter/StartDateField.test.ts
+++ b/tests/Query/Filter/StartDateField.test.ts
@@ -56,3 +56,21 @@ describe('sorting by start', () => {
         expectTaskComparesAfter(new StartDateField().createReverseSorter(), date1, date2);
     });
 });
+
+describe('grouping by start date', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new StartDateField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it('group by start date', () => {
+        // Arrange
+        const grouper = new StartDateField().createGrouper();
+        const taskWithDate = new TaskBuilder().startDate('1970-01-01').build();
+        const taskWithoutDate = new TaskBuilder().build();
+
+        // Assert
+        expect(grouper.grouper(taskWithDate)).toEqual(['1970-01-01 Thursday']);
+        expect(grouper.grouper(taskWithoutDate)).toEqual(['No start date']);
+    });
+});