Repository: obsidian-tasks-group/obsidian-tasks
Pull request #806 (2022-06-18 08:51:13)

refactor: Add RecurringField class, for handling recurring/not recurring filters
## Description
<!--- Describe your changes in detail -->

1. Add `RecurringField ` class
2. Rework Query to use `RecurringField ` for `is recurring` and `is not recurring` filters
3. Add `RecurringField.test.ts` with simple tests for this new class

## Motivation and Context

More progress on #664 

## How has this been tested?

By running the existing tests, and adding some new ones.

<!--- 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: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] Refactor (non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## 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! -->
- [ ] 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 accordingly.
- [x] My change has adequate Unit Test coverage.

By creating a Pull Request you agree to our [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md). For further guidance on contributing please see [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)



Patch:
diff --git a/src/Query.ts b/src/Query.ts
index ccd7ea25..5580021d 100644
--- a/src/Query.ts
+++ b/src/Query.ts
@@ -16,6 +16,7 @@ import { PriorityField } from './Query/Filter/PriorityField';
 import { ScheduledDateField } from './Query/Filter/ScheduledDateField';
 import { StartDateField } from './Query/Filter/StartDateField';
 import { HappensDateField } from './Query/Filter/HappensDateField';
+import { RecurringField } from './Query/Filter/RecurringField';
 import { StatusField } from './Query/Filter/StatusField';
 import { TagsField } from './Query/Filter/TagsField';
 
@@ -71,9 +72,6 @@ export class Query implements IQuery {
         /^hide (task count|backlink|priority|start date|scheduled date|done date|due date|recurrence rule|edit button)/;
     private readonly shortModeRegexp = /^short/;
 
-    private readonly recurringString = 'is recurring';
-    private readonly notRecurringString = 'is not recurring';
-
     private readonly limitRegexp = /^limit (to )?(\d+)( tasks?)?/;
     private readonly excludeSubItemsString = 'exclude sub-items';
 
@@ -88,12 +86,6 @@ export class Query implements IQuery {
                 switch (true) {
                     case line === '':
                         break;
-                    case line === this.recurringString:
-                        this._filters.push((task) => task.recurrence !== null);
-                        break;
-                    case line === this.notRecurringString:
-                        this._filters.push((task) => task.recurrence === null);
-                        break;
                     case line === this.excludeSubItemsString:
                         this._filters.push((task) => task.indentation === '');
                         break;
@@ -102,6 +94,8 @@ export class Query implements IQuery {
                         break;
                     case this.parseFilter(line, new StatusField()):
                         break;
+                    case this.parseFilter(line, new RecurringField()):
+                        break;
                     case this.parseFilter(line, new PriorityField()):
                         break;
                     case this.parseFilter(line, new HappensDateField()):
diff --git a/src/Query/Filter/RecurringField.ts b/src/Query/Filter/RecurringField.ts
new file mode 100644
index 00000000..9efecb1d
--- /dev/null
+++ b/src/Query/Filter/RecurringField.ts
@@ -0,0 +1,40 @@
+import { Field } from './Field';
+import { FilterOrErrorMessage } from './Filter';
+
+export class RecurringField extends Field {
+    private readonly instructionForFieldPresence = 'is recurring';
+    private readonly instructionForFieldAbsence = 'is not recurring';
+
+    public canCreateFilterForLine(line: string): boolean {
+        return (
+            line === this.instructionForFieldAbsence ||
+            line === this.instructionForFieldPresence
+        );
+    }
+
+    public createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
+        const result = new FilterOrErrorMessage();
+
+        if (line === this.instructionForFieldPresence) {
+            result.filter = (task) => task.recurrence !== null;
+            return result;
+        }
+
+        if (line === this.instructionForFieldAbsence) {
+            result.filter = (task) => task.recurrence === null;
+            return result;
+        }
+
+        result.error = `do not understand filter: ${line}`;
+
+        return result;
+    }
+
+    protected fieldName(): string {
+        return 'recurring';
+    }
+
+    protected filterRegexp(): RegExp | null {
+        return null;
+    }
+}


Test patch:
diff --git a/tests/Query/Filter/RecurringField.test.ts b/tests/Query/Filter/RecurringField.test.ts
new file mode 100644
index 00000000..011687a1
--- /dev/null
+++ b/tests/Query/Filter/RecurringField.test.ts
@@ -0,0 +1,50 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { RecurringField } from '../../../src/Query/Filter/RecurringField';
+import type { FilterOrErrorMessage } from '../../../src/Query/Filter/Filter';
+import { testTaskFilter } from '../../TestingTools/FilterTestHelpers';
+import { fromLine } from '../../TestHelpers';
+
+window.moment = moment;
+
+function testRecurringFilter(
+    filter: FilterOrErrorMessage,
+    line: string,
+    expected: boolean,
+) {
+    const task = fromLine({ line });
+    testTaskFilter(filter, task, expected);
+}
+
+describe('recurring', () => {
+    const non_recurring = '- [ ] non-recurring task';
+    const recurring = '- [ ] recurring 🔁 every day 📅 2022-06-17';
+    // Invalid recurrence rules are discarded, and treated as non-recurring
+    const invalid = '- [ ] recurring 🔁 invalid rule 📅 2022-06-17';
+
+    it('is recurring', () => {
+        // Arrange
+        const filter = new RecurringField().createFilterOrErrorMessage(
+            'is recurring',
+        );
+
+        // Assert
+        testRecurringFilter(filter, non_recurring, false);
+        testRecurringFilter(filter, recurring, true);
+        testRecurringFilter(filter, invalid, false);
+    });
+
+    it('is not recurring', () => {
+        // Arrange
+        const filter = new RecurringField().createFilterOrErrorMessage(
+            'is not recurring',
+        );
+
+        // Assert
+        testRecurringFilter(filter, non_recurring, true);
+        testRecurringFilter(filter, recurring, false);
+        testRecurringFilter(filter, invalid, true);
+    });
+});