Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1550 (2023-01-18 20:59:53)

feat: Add 'group by status.name'
# Description

<!--- Describe your changes in detail -->

Add `group by status.name`

This adds optional support for grouping (or naming of groups) to the `Field` classes.

For now, parsing `Field`-based grouping instructions is done in `FilterParser`, called from `Query.parseGroupBy2()`.

The old grouping functions will be gradually migrated from `Group.ts` to the respective `Field` classes.

Note: A typo in the first commit referred to `status.type` - it should have said `status.name`

## Motivation and Context

<!--- 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?

<!--- 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)
expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)

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)
- [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/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss
index 46df5754..fc8d2182 100644
--- a/docs/_sass/custom/custom.scss
+++ b/docs/_sass/custom/custom.scss
@@ -7,7 +7,7 @@ blockquote {
     border-left: 3px solid #ccc;
 }
 
-td {
+th, td {
     vertical-align: top;
 }
 
diff --git a/docs/getting-started/statuses.md b/docs/getting-started/statuses.md
index bddea0c1..3a863938 100644
--- a/docs/getting-started/statuses.md
+++ b/docs/getting-started/statuses.md
@@ -60,6 +60,7 @@ The tasks shown are purely examples for context. The `~` column is just an arbit
 | Matches `status.type includes CANCELLED` | no | no | no | YES | no |
 | Matches `status.type includes NON_TASK` | no | no | no | no | YES |
 | Name for `group by status` | Todo | Done | Done | Done | Done |
+| Name for `group by status.name` | Todo | In Progress | Done | Cancelled | My custom status |
 
 <!-- placeholder to force blank line after table --> <!-- endInclude -->
 
@@ -90,7 +91,7 @@ This table shows the statuses provided by default:
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `space` | Todo | `x` | `TODO` | No |
 | `/` | In Progress | `x` | `IN_PROGRESS` | Yes |
@@ -135,7 +136,7 @@ Remember to set up your chosen CSS Snippet or Theme before setting up the custom
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forwarded | `x` | `TODO` | Yes |
 | `<` | Schedule | `x` | `TODO` | Yes |
@@ -163,7 +164,7 @@ Remember to set up your chosen CSS Snippet or Theme before setting up the custom
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forward | `x` | `TODO` | Yes |
 | `D` | Deferred/Scheduled | `x` | `TODO` | Yes |
@@ -206,6 +207,7 @@ We are tracking this in [issue #1486](https://github.com/obsidian-tasks-group/ob
 - `not done` - matches tasks with status types `TODO` and `IN_PROGRESS`
 - `status.name` text search
 - `sort by status.name`
+- `group by status.name`
 - `status.type` text search
 - `sort by status.type`
 
diff --git a/docs/how-to/set-up-custom-statuses.md b/docs/how-to/set-up-custom-statuses.md
index eab9a0ff..daf3d540 100644
--- a/docs/how-to/set-up-custom-statuses.md
+++ b/docs/how-to/set-up-custom-statuses.md
@@ -73,7 +73,7 @@ Suppose that you wanted to create a set of 3 statuses that cycle between each ot
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `!` | Important | `D` | `TODO` | Yes |
 | `D` | Doing - Important | `X` | `TODO` | Yes |
diff --git a/docs/queries/grouping.md b/docs/queries/grouping.md
index bdb8112d..888a19b4 100644
--- a/docs/queries/grouping.md
+++ b/docs/queries/grouping.md
@@ -64,11 +64,19 @@ You can group by the following properties.
 {: .released }
 `happens` grouping option was introduced in Tasks 1.11.0.
 
-### Other task properties
+### Task statuses
 
 1. `status` (Done or Todo, which is capitalized for visibility in the headings)
     - Note that the Done group is displayed before the Todo group,
       which differs from the Sorting ordering of this property.
+1. `group by status.name`
+    - This groups by the names you give to your custom statuses, in alphabetical order.
+
+{: .released }  
+`group by status.name` was introduced in Tasks X.Y.Z.
+
+### Other task properties
+
 1. `priority`
     - The priority of the task, namely one of:
         - `Priority 1: High`
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index 367598ef..02e0ab5d 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -15,43 +15,43 @@ has_toc: false
 
 This table summarizes the filters and other options available inside a `tasks` block.
 
-| [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`<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`                       |                       |                        |
-| `status.type (includes, does not include) <string>`<br>`status.type (regex matches, regex does not match) /regex/i`                                                                                 | `sort by status.type`                       |                       |                        |
-| `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`                                                                                                                    | `sort by happens`                           | `group by happens`    |                        |
-| `is recurring`<br>`is not recurring`                                                                                                                                                                |                                             | `group by recurring`  |                        |
-| `recurrence (includes, does not include) <string>`<br>`recurrence (regex matches, regex does not match) /regex/i`                                                                                   |                                             | `group by recurrence` | `hide recurrence rule` |
-| `priority is (above, below, not)? (low, none, medium, high)`                                                                                                                                        | `sort by priority`                          | `group by priority`   | `hide priority`        |
-|                                                                                                                                                                                                     | `sort by urgency`                           |                       | `show urgency`         |
-| `path (includes, does not include) <path>`<br>`path (regex matches, regex does not match) /regex/i`                                                                                                 | `sort by path`                              | `group by path`       |                        |
-|                                                                                                                                                                                                     |                                             | `group by root`       |                        |
-|                                                                                                                                                                                                     |                                             | `group by folder`     |                        |
-| `filename (includes, does not include) <filename>`<br>`filename (regex matches, regex does not match) /regex/i`                                                                                     | `sort by filename`                          | `group by filename`   |                        |
-| `heading (includes, does not include) <string>`<br>`heading (regex matches, regex does not match) /regex/i`                                                                                         | `sort by heading`                           | `group by heading`    |                        |
-|                                                                                                                                                                                                     |                                             | `group by backlink`   | `hide backlink`        |
-| `description (includes, does not include) <string>`<br>`description (regex matches, regex does not match) /regex/i`                                                                                 | `sort by description`                       |                       |                        |
-| `tag (includes, does not include) <tag>`<br>`tags (include, do not include) <tag>`<br>`tag (regex matches, regex does not match) /regex/i`<br>`tags (regex matches, regex does not match) /regex/i` | `sort by tag`<br>`sort by tag <tag_number>` | `group by tags`       |                        |
-| [**Combining Filters**][5]                                                                                                                                                                          |                                             |                       |                        |
-| `(filter 1) AND (filter 2)`                                                                                                                                                                         |                                             |                       |                        |
-| `(filter 1) OR (filter 2)`                                                                                                                                                                          |                                             |                       |                        |
-| `NOT (filter 1)`                                                                                                                                                                                    |                                             |                       |                        |
-| `(filter 1) XOR (filter 2)`                                                                                                                                                                         |                                             |                       |                        |
-| `(filter 1) AND NOT (filter 2)`                                                                                                                                                                     |                                             |                       |                        |
-| `(filter 1) OR NOT (filter 2)`                                                                                                                                                                      |                                             |                       |                        |
-| `(filter 1) AND ((filter 2) OR (filter 3))`                                                                                                                                                         |                                             |                       |                        |
-| **Other Filter Options**                                                                                                                                                                            |                                             |                       |                        |
-| `exclude sub-items`                                                                                                                                                                                 |                                             |                       |                        |
-| `limit to <number> tasks`<br>`limit <number>`                                                                                                                                                       |                                             |                       |                        |
-| **Other Layout Options**                                                                                                                                                                            |                                             |                       |                        |
-| `hide edit button`                                                                                                                                                                                  |                                             |                       |                        |
-| `hide task count`                                                                                                                                                                                   |                                             |                       |                        |
-| `short mode`                                                                                                                                                                                        |                                             |                       |                        |
-| **Other Instructions**                                                                                                                                                                              |                                             |                       |                        |
-| `explain`                                                                                                                                                                                           |                                             |                       |                        |
-| `# comment`                                                                                                                                                                                         |                                             |                       |                        |
+| [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`<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 (includes, does not include) <string>`<br>`status.type (regex matches, regex does not match) /regex/i`                                                                                 | `sort by status.type`                       |                        |                        |
+| `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`                                                                                                                    | `sort by happens`                           | `group by happens`     |                        |
+| `is recurring`<br>`is not recurring`                                                                                                                                                                |                                             | `group by recurring`   |                        |
+| `recurrence (includes, does not include) <string>`<br>`recurrence (regex matches, regex does not match) /regex/i`                                                                                   |                                             | `group by recurrence`  | `hide recurrence rule` |
+| `priority is (above, below, not)? (low, none, medium, high)`                                                                                                                                        | `sort by priority`                          | `group by priority`    | `hide priority`        |
+|                                                                                                                                                                                                     | `sort by urgency`                           |                        | `show urgency`         |
+| `path (includes, does not include) <path>`<br>`path (regex matches, regex does not match) /regex/i`                                                                                                 | `sort by path`                              | `group by path`        |                        |
+|                                                                                                                                                                                                     |                                             | `group by root`        |                        |
+|                                                                                                                                                                                                     |                                             | `group by folder`      |                        |
+| `filename (includes, does not include) <filename>`<br>`filename (regex matches, regex does not match) /regex/i`                                                                                     | `sort by filename`                          | `group by filename`    |                        |
+| `heading (includes, does not include) <string>`<br>`heading (regex matches, regex does not match) /regex/i`                                                                                         | `sort by heading`                           | `group by heading`     |                        |
+|                                                                                                                                                                                                     |                                             | `group by backlink`    | `hide backlink`        |
+| `description (includes, does not include) <string>`<br>`description (regex matches, regex does not match) /regex/i`                                                                                 | `sort by description`                       |                        |                        |
+| `tag (includes, does not include) <tag>`<br>`tags (include, do not include) <tag>`<br>`tag (regex matches, regex does not match) /regex/i`<br>`tags (regex matches, regex does not match) /regex/i` | `sort by tag`<br>`sort by tag <tag_number>` | `group by tags`        |                        |
+| [**Combining Filters**][5]                                                                                                                                                                          |                                             |                        |                        |
+| `(filter 1) AND (filter 2)`                                                                                                                                                                         |                                             |                        |                        |
+| `(filter 1) OR (filter 2)`                                                                                                                                                                          |                                             |                        |                        |
+| `NOT (filter 1)`                                                                                                                                                                                    |                                             |                        |                        |
+| `(filter 1) XOR (filter 2)`                                                                                                                                                                         |                                             |                        |                        |
+| `(filter 1) AND NOT (filter 2)`                                                                                                                                                                     |                                             |                        |                        |
+| `(filter 1) OR NOT (filter 2)`                                                                                                                                                                      |                                             |                        |                        |
+| `(filter 1) AND ((filter 2) OR (filter 3))`                                                                                                                                                         |                                             |                        |                        |
+| **Other Filter Options**                                                                                                                                                                            |                                             |                        |                        |
+| `exclude sub-items`                                                                                                                                                                                 |                                             |                        |                        |
+| `limit to <number> tasks`<br>`limit <number>`                                                                                                                                                       |                                             |                        |                        |
+| **Other Layout Options**                                                                                                                                                                            |                                             |                        |                        |
+| `hide edit button`                                                                                                                                                                                  |                                             |                        |                        |
+| `hide task count`                                                                                                                                                                                   |                                             |                        |                        |
+| `short mode`                                                                                                                                                                                        |                                             |                        |                        |
+| **Other Instructions**                                                                                                                                                                              |                                             |                        |                        |
+| `explain`                                                                                                                                                                                           |                                             |                        |                        |
+| `# comment`                                                                                                                                                                                         |                                             |                        |                        |
diff --git a/src/Query/Filter/Field.ts b/src/Query/Filter/Field.ts
index 12577673..b3878a71 100644
--- a/src/Query/Filter/Field.ts
+++ b/src/Query/Filter/Field.ts
@@ -1,6 +1,8 @@
 import { Sorter } from '../Sorter';
 import type { Comparator } from '../Sorter';
 import * as RegExpTools from '../../lib/RegExpTools';
+import { Grouper } from '../Grouper';
+import type { GrouperFunction } from '../Grouper';
 import type { FilterOrErrorMessage } from './Filter';
 
 /**
@@ -123,7 +125,7 @@ export abstract class Field {
      * Return whether the code for this field implements sorting of tasks.
      *
      * If overriding this to return true, in order to enable sorting,
-     * the method ${@link comparator} must also be overridden.
+     * the method {@link comparator} must also be overridden.
      */
     public supportsSorting(): boolean {
         return false;
@@ -213,8 +215,8 @@ export abstract class Field {
     /**
      * Return a function to compare two Task objects, for use in sorting by this field's value.
      *
-     * See ${@link supportsSorting} for what to do, to enable support of sorting in a
-     * particular ${@link Field} implementation.
+     * See {@link supportsSorting} for what to do, to enable support of sorting in a
+     * particular {@link Field} implementation.
      */
     public comparator(): Comparator {
         throw Error(`comparator() unimplemented for ${this.fieldNameSingular()}`);
@@ -247,4 +249,38 @@ export abstract class Field {
     public createReverseSorter(): Sorter {
         return this.createSorter(true);
     }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Grouping
+    // -----------------------------------------------------------------------------------------------------------------
+
+    /**
+     * Return whether the code for this field implements grouping of tasks.
+     *
+     * If overriding this to return true, in order to enable grouping,
+     * the method {@link grouper} must also be overridden.
+     */
+    public supportsGrouping(): boolean {
+        return false;
+    }
+
+    /**
+     * Return a function to get a list of a task's group names, for use in grouping by this field's value.
+     *
+     * See {@link supportsGrouping} for what to do, to enable support of grouping in a
+     * particular {@link Field} implementation.
+     */
+    public grouper(): GrouperFunction {
+        throw Error(`grouper() unimplemented for ${this.fieldNameSingular()}`);
+    }
+
+    /**
+     * Create a {@link Grouper} object for grouping tasks by this field's value.
+     *
+     * For now, parsing of `group by` lines is currently done in {@link FilterParser.parseGrouper()}.
+     * Later, this will probably be moved to the {@link Field} classes.
+     */
+    public createGrouper(): Grouper {
+        return new Grouper(this.fieldNameSingular(), this.grouper());
+    }
 }
diff --git a/src/Query/Filter/StatusNameField.ts b/src/Query/Filter/StatusNameField.ts
index 76a1f3af..de0fbb6f 100644
--- a/src/Query/Filter/StatusNameField.ts
+++ b/src/Query/Filter/StatusNameField.ts
@@ -1,8 +1,9 @@
 import type { Task } from '../../Task';
+import type { GrouperFunction } from '../Grouper';
 import { TextField } from './TextField';
 
 /**
- * A ${@link Field} implementation for searching status.name
+ * A {@link Field} implementation for searching status.name
  */
 export class StatusNameField extends TextField {
     constructor() {
@@ -20,4 +21,14 @@ export class StatusNameField extends TextField {
     supportsSorting(): boolean {
         return true;
     }
+
+    public supportsGrouping(): boolean {
+        return true;
+    }
+
+    public grouper(): GrouperFunction {
+        return (task: Task) => {
+            return [this.value(task)];
+        };
+    }
 }
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index 2b328a7d..965bdda3 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -20,6 +20,7 @@ import { StatusTypeField } from './Filter/StatusTypeField';
 import { RecurrenceField } from './Filter/RecurrenceField';
 import type { FilterOrErrorMessage } from './Filter/Filter';
 import type { Sorter } from './Sorter';
+import type { Grouper } from './Grouper';
 
 const fieldCreators = [
     () => new StatusNameField(), // status.name is before status, to avoid ambiguity
@@ -70,3 +71,25 @@ export function parseSorter(sorterString: string): Sorter | null {
     }
     return null;
 }
+
+export function parseGrouper(line: string): Grouper | null {
+    // New style parsing, using grouping which is done by the Field classes.
+
+    // Optimisation: Check whether line begins with 'group by'
+    const groupByRegexp = /^group by /;
+    if (line.match(groupByRegexp) === null) {
+        return null;
+    }
+
+    // See if any of the fields can parse the line.
+    for (const creator of fieldCreators) {
+        const field = creator();
+        const fieldName = field.fieldNameSingular();
+        if (line === `group by ${fieldName}`) {
+            if (field.supportsGrouping()) {
+                return field.createGrouper();
+            }
+        }
+    }
+    return null;
+}
diff --git a/src/Query/Group.ts b/src/Query/Group.ts
index e1b2b77f..2fbbb5ef 100644
--- a/src/Query/Group.ts
+++ b/src/Query/Group.ts
@@ -12,7 +12,7 @@ export class Group {
     private static readonly groupDateFormat = 'YYYY-MM-DD dddd';
 
     public static fromGroupingProperty(property: GroupingProperty): Grouper {
-        return new Grouper(Group.grouperForProperty(property));
+        return new Grouper(property, Group.grouperForProperty(property));
     }
 
     /**
diff --git a/src/Query/Grouper.ts b/src/Query/Grouper.ts
index 0eae9cac..e83ac100 100644
--- a/src/Query/Grouper.ts
+++ b/src/Query/Grouper.ts
@@ -24,9 +24,11 @@ export type GroupingProperty =
     | 'tags';
 
 export class Grouper {
+    public readonly property: string;
     public readonly grouper: GrouperFunction;
 
-    public constructor(grouper: GrouperFunction) {
+    constructor(property: string, grouper: GrouperFunction) {
+        this.property = property;
         this.grouper = grouper;
     }
 }
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index e3c63286..86ea2191 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -5,7 +5,7 @@ import { getSettings } from '../Config/Settings';
 import { Sort } from './Sort';
 import type { Sorter } from './Sorter';
 import type { TaskGroups } from './TaskGroups';
-import { parseFilter, parseSorter } from './FilterParser';
+import * as FilterParser from './FilterParser';
 import { Group } from './Group';
 import type { Grouper } from './Grouper';
 import type { GroupingProperty } from './Grouper';
@@ -53,6 +53,8 @@ export class Query implements IQuery {
                         break;
                     case this.parseSortBy({ line }):
                         break;
+                    case this.parseGroupBy2({ line }):
+                        break;
                     case this.groupByRegexp.test(line):
                         this.parseGroupBy({ line });
                         break;
@@ -180,7 +182,7 @@ export class Query implements IQuery {
     }
 
     private parseFilter(line: string) {
-        const filterOrError = parseFilter(line);
+        const filterOrError = FilterParser.parseFilter(line);
         if (filterOrError != null) {
             if (filterOrError.filter) this._filters.push(filterOrError.filter);
             else this._error = filterOrError.error;
@@ -200,7 +202,7 @@ export class Query implements IQuery {
     }
 
     private parseSortBy({ line }: { line: string }): boolean {
-        const sortingMaybe = parseSorter(line);
+        const sortingMaybe = FilterParser.parseSorter(line);
         if (sortingMaybe) {
             this._sorting.push(sortingMaybe);
             return true;
@@ -208,6 +210,17 @@ export class Query implements IQuery {
         return false;
     }
 
+    /**
+     * Old-style parsing of `group by` lines, for grouping that is implemented with static
+     * methods in {@link Group}, that are looked up from a {@link GroupingProperty}.
+     *
+     * These will be gradually migrated to the grouping method in {@link Field}
+     * classes, after which this method will be deleted.
+     *
+     * @param line
+     * @private
+     * @see parseGroupBy2
+     */
     private parseGroupBy({ line }: { line: string }): void {
         const fieldMatch = line.match(this.groupByRegexp);
         if (fieldMatch !== null) {
@@ -216,4 +229,22 @@ export class Query implements IQuery {
             this._error = 'do not understand query grouping';
         }
     }
+
+    /**
+     * New-style parsing of `group by` lines, for grouping that is implemented in the {@link Field}
+     * classes.
+     *
+     * Once the original {@link parseGroupBy} has been removed, rename this to parseGroupBy()
+     * @param line
+     * @private
+     * @see parseGroupBy
+     */
+    private parseGroupBy2({ line }: { line: string }): boolean {
+        const groupingMaybe = FilterParser.parseGrouper(line);
+        if (groupingMaybe) {
+            this._grouping.push(groupingMaybe);
+            return true;
+        }
+        return false;
+    }
 }


Test patch:
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
index 67b591bc..8e4b632b 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `space` | Todo | `x` | `TODO` | No |
 | `/` | In Progress | `x` | `IN_PROGRESS` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
index 3103789f..f0ae2a48 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `!` | Important | `D` | `TODO` | Yes |
 | `D` | Doing - Important | `X` | `TODO` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
index 223d33ff..453bc16d 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forward | `x` | `TODO` | Yes |
 | `D` | Deferred/Scheduled | `x` | `TODO` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
index 0c409fa0..b968c032 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name | Next Status Character | Status Type | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forwarded | `x` | `TODO` | Yes |
 | `<` | Schedule | `x` | `TODO` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md b/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
index ba3bceb0..e4705300 100644
--- a/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
+++ b/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
@@ -15,6 +15,7 @@
 | Matches `status.type includes CANCELLED` | no | no | no | YES | no |
 | Matches `status.type includes NON_TASK` | no | no | no | no | YES |
 | Name for `group by status` | Todo | Done | Done | Done | Done |
+| Name for `group by status.name` | Todo | In Progress | Done | Cancelled | My custom status |
 
 
 <!-- placeholder to force blank line after table -->
diff --git a/tests/DocsSamplesForStatuses.test.ts b/tests/DocsSamplesForStatuses.test.ts
index db7e7579..4a7709a3 100644
--- a/tests/DocsSamplesForStatuses.test.ts
+++ b/tests/DocsSamplesForStatuses.test.ts
@@ -9,6 +9,7 @@ import { StatusConfiguration, StatusType } from '../src/StatusConfiguration';
 import type { FilterOrErrorMessage } from '../src/Query/Filter/Filter';
 import * as FilterParser from '../src/Query/FilterParser';
 import { Group } from '../src/Query/Group';
+import { StatusNameField } from '../src/Query/Filter/StatusNameField';
 import { TaskBuilder } from './TestingTools/TaskBuilder';
 
 class MarkdownTable {
@@ -62,7 +63,7 @@ function getPrintableIndicator(indicator: string) {
 function verifyStatusesAsMarkdownTable(statuses: Status[]) {
     const table = new MarkdownTable([
         'Status Character',
-        'Status Name',
+        'Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name`',
         'Next Status Character',
         'Status Type',
         'Needs Custom Styling',
@@ -164,6 +165,16 @@ function verifyTransitionsAsMarkdownTable(statuses: Status[]) {
         table.addRow(cells);
     }
 
+    {
+        const cells: string[] = ['Name for `group by status.name`'];
+        tasks.forEach((task) => {
+            const groupNamesForTask = new StatusNameField().createGrouper().grouper(task);
+            const names = groupNamesForTask.join(',');
+            cells.push(names);
+        });
+        table.addRow(cells);
+    }
+
     table.verify();
 }
 
@@ -174,7 +185,7 @@ describe('Status Transitions', () => {
             Status.makeInProgress(),
             Status.makeDone(),
             Status.makeCancelled(),
-            new Status(new StatusConfiguration('~', 'Non Task', ' ', false, StatusType.NON_TASK)),
+            new Status(new StatusConfiguration('~', 'My custom status', ' ', false, StatusType.NON_TASK)),
         ];
         verifyTransitionsAsMarkdownTable(statuses);
     });
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 4e63b7cd..8bcbaab4 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -189,6 +189,7 @@ describe('Query parsing', () => {
             'group by scheduled',
             'group by start',
             'group by status',
+            'group by status.type',
             'group by tags',
         ];
         test.concurrent.each<string>(filters)('recognises %j', (filter) => {
@@ -257,10 +258,15 @@ describe('Query parsing', () => {
         });
     });
 
-    it('should parse ambiguous queries correctly', () => {
+    it('should parse ambiguous sort by queries correctly', () => {
         expect(new Query({ source: 'sort by status' }).sorting[0].property).toEqual('status');
         expect(new Query({ source: 'sort by status.name' }).sorting[0].property).toEqual('status.name');
     });
+
+    it('should parse ambiguous group by queries correctly', () => {
+        expect(new Query({ source: 'group by status' }).grouping[0].property).toEqual('status');
+        expect(new Query({ source: 'group by status.name' }).grouping[0].property).toEqual('status.name');
+    });
 });
 
 describe('Query', () => {
diff --git a/tests/Query/Filter/StatusNameField.test.ts b/tests/Query/Filter/StatusNameField.test.ts
index 521edbdc..5f9f575e 100644
--- a/tests/Query/Filter/StatusNameField.test.ts
+++ b/tests/Query/Filter/StatusNameField.test.ts
@@ -89,3 +89,19 @@ describe('sorting by status.name', () => {
         expectTaskComparesAfter(sorter, todoTask, unknTask);
     });
 });
+
+describe('grouping by status.name', () => {
+    it('supports Field grouping methods correctly', () => {
+        const field = new StatusNameField();
+        expect(field.supportsGrouping()).toEqual(true);
+    });
+
+    it('group by status.name', () => {
+        // Arrange
+        const grouper = new StatusNameField().createGrouper();
+
+        // // Assert
+        expect(grouper.grouper(todoTask)).toEqual(['Todo']);
+        expect(grouper.grouper(inprTask)).toEqual(['In Progress']);
+    });
+});
diff --git a/tests/Query/FilterParser.test.ts b/tests/Query/FilterParser.test.ts
new file mode 100644
index 00000000..a29a9f74
--- /dev/null
+++ b/tests/Query/FilterParser.test.ts
@@ -0,0 +1,8 @@
+import * as FilterParser from '../../src/Query/FilterParser';
+
+describe('FilterParser', () => {
+    it('should parse new-style group line correctly', () => {
+        expect(FilterParser.parseGrouper('group by status.name')).not.toBeNull();
+        expect(FilterParser.parseGrouper('group by status-name')).toBeNull();
+    });
+});