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

Issue:
regex support for recurring field
### ⚠️ 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

I would like to apply a regex to the recurring field.  For example, I have items which recur "every weekday" and would like to be exclude them in the filter.

### ✔️ Solution



(is | is not) recurring regex (matches| does not match) /some regex/

### ❓ Alternatives

recurring regex (matches | regex does not match) /some regex/

### 📝 Additional Context

_No response_

Pull request:
feat: Add text searching of task recurrence rules
<!--- Provide a general summary of your changes in the Title above -->

# Description

This implements the following filters:

```text
recurrence (includes, does not include) <string>
recurrence (regex matches, regex does not match) /regex/I
```

## Motivation and Context

Fixes #1448 and  #1218.

## How has this been tested?

By writing new tests and running the existing ones.

## 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)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)


Patch:
diff --git a/docs/queries/filters.md b/docs/queries/filters.md
index fa2bfe25..36b14d98 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -246,6 +246,15 @@ For more information, see [Priorities]({{ site.baseurl }}{% link getting-started
 
 - `is recurring`
 - `is not recurring`
+- `recurrence (includes|does not include) <part of recurrence rule>`
+  - Matches case-insensitive (disregards capitalization).
+  - Note that the text searched is generated programmatically and standardised, and so may not exactly match the text in any manually typed tasks. For example, a task with `🔁 every Sunday` will be searched as `every week on Sunday`.
+  - The easiest way to see the standardised recurrence rule of your tasks is to use `group by recurrence`, and review the resulting group headings.
+- `recurrence (regex matches|regex does not match) /<JavaScript-style Regex>/`
+  - Does regular expression match (case-sensitive by default).
+  - Essential reading: [Regular Expression Searches]({{ site.baseurl }}{% link queries/regular-expressions.md %}).
+
+> `recurrence` text searching was introduced in Tasks 1.22.0.
 
 ### Status
 
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index fbcb1008..7f147279 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -24,7 +24,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `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`  |                        |
-|                                                                                                                                                                                                     |                                             | `group by recurrence` | `hide recurrence rule` |
+| `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`       |                        |
diff --git a/src/Query/Filter/RecurrenceField.ts b/src/Query/Filter/RecurrenceField.ts
new file mode 100644
index 00000000..02b03a58
--- /dev/null
+++ b/src/Query/Filter/RecurrenceField.ts
@@ -0,0 +1,16 @@
+import type { Task } from '../../Task';
+import { TextField } from './TextField';
+
+export class RecurrenceField extends TextField {
+    fieldName(): string {
+        return 'recurrence';
+    }
+
+    value(task: Task): string {
+        if (task.recurrence !== null) {
+            return task.recurrence!.toText();
+        } else {
+            return '';
+        }
+    }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index f7bfaa05..57b74b44 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -14,6 +14,7 @@ import { TagsField } from './Filter/TagsField';
 import { BooleanField } from './Filter/BooleanField';
 import { FilenameField } from './Filter/FilenameField';
 import { UrgencyField } from './Filter/UrgencyField';
+import { RecurrenceField } from './Filter/RecurrenceField';
 
 import type { FilterOrErrorMessage } from './Filter/Filter';
 import type { Sorter } from './Sorter';
@@ -35,6 +36,7 @@ const fieldCreators = [
     () => new BooleanField(),
     () => new FilenameField(),
     () => new UrgencyField(),
+    () => new RecurrenceField(),
 ];
 
 export function parseFilter(filterString: string): FilterOrErrorMessage | null {


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 266fb6e6..2887db48 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -57,6 +57,8 @@ describe('Query parsing', () => {
         'priority is low',
         'priority is medium',
         'priority is none',
+        'recurrence does not include wednesday',
+        'recurrence includes wednesday',
         'scheduled after 2021-12-27',
         'scheduled before 2021-12-27',
         'scheduled date is invalid',
diff --git a/tests/Query/Filter/RecurrenceField.test.ts b/tests/Query/Filter/RecurrenceField.test.ts
new file mode 100644
index 00000000..228a32e4
--- /dev/null
+++ b/tests/Query/Filter/RecurrenceField.test.ts
@@ -0,0 +1,82 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+
+import { RecurrenceField } from '../../../src/Query/Filter/RecurrenceField';
+import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { toBeValid, toMatchTask } from '../../CustomMatchers/CustomMatchersForFilters';
+import { RecurrenceBuilder } from '../../TestingTools/RecurrenceBuilder';
+
+window.moment = moment;
+
+expect.extend({
+    toBeValid,
+    toMatchTask,
+});
+
+describe('recurrence', () => {
+    // Note: We don't need to check all behaviours that are implemented in the base class.
+    // These are minimal tests to confirm that the filters are correctly wired up,
+    // to guard against possible future coding errors.
+
+    // Easy construction of Tasks with given rule text
+    function with_recurrence(ruleText: string) {
+        const recurrence = new RecurrenceBuilder().rule(ruleText).startDate('2022-07-14').build();
+        return new TaskBuilder().recurrence(recurrence).build();
+    }
+
+    it('value', () => {
+        const field = new RecurrenceField();
+        expect(field.value(new TaskBuilder().build())).toStrictEqual('');
+        expect(field.value(with_recurrence('every Sunday when done'))).toStrictEqual('every week on Sunday when done');
+        expect(field.value(with_recurrence('every 6 months on the 2nd Wednesday'))).toStrictEqual(
+            'every 6 months on the 2nd Wednesday',
+        );
+    });
+
+    it('by recurrence (includes)', () => {
+        // Arrange
+        const filter = new RecurrenceField().createFilterOrErrorMessage('recurrence includes wednesday');
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTask(with_recurrence('every Wednesday'));
+        expect(filter).not.toMatchTask(new TaskBuilder().build());
+    });
+
+    it('by recurrence (does not include)', () => {
+        // Arrange
+        const filter = new RecurrenceField().createFilterOrErrorMessage('recurrence does not include when done');
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTask(new TaskBuilder().build());
+        expect(filter).toMatchTask(with_recurrence('every week on Sunday'));
+        expect(filter).not.toMatchTask(with_recurrence('every 10 days when done'));
+    });
+
+    it('by recurrence (regex matches)', () => {
+        // Arrange
+        const filter = new RecurrenceField().createFilterOrErrorMessage(String.raw`recurrence regex matches /\d/`); // any digit present
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTask(with_recurrence('every month on the 31st'));
+        expect(filter).not.toMatchTask(new TaskBuilder().build());
+        expect(filter).not.toMatchTask(with_recurrence('every month on the last'));
+    });
+
+    it('by recurrence (regex does not match)', () => {
+        // Arrange
+        const filter = new RecurrenceField().createFilterOrErrorMessage(
+            String.raw`recurrence regex does not match /\d/`, // no digit present
+        );
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).not.toMatchTask(with_recurrence('every month on the 31st'));
+        expect(filter).toMatchTask(new TaskBuilder().build());
+        expect(filter).toMatchTask(with_recurrence('every month on the last'));
+    });
+});