Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2758 (2024-04-09 09:21:48)

refactor: Implement new preprocessor for Boolean searches
<!--- Provide a general summary of your changes in the Title above -->

# Description

*Note: this is labelled as a refactor as the new code is not yet used in the Tasks plugin.*

This extends `BooleanPreprocessor` to add a more sophisticated implementation for preprocessing Boolean/combined filters.

<!--- Describe your changes in detail -->
- Add `BooleanPreprocessorResult
- Add `BooleanPreprocessor.preprocessExpression()`

This change - and especially the new method `isAFilter()` - is the result of several weeks and many, many hours of trying to maintain existing working searches, whilst also adding support for filters that contain the following characters:

- `(`
- `)`
- `"`

### Caveats

The one current caveat with this implementation is that it does not work for filters the **end** with any of the characters `()"`, as they are swallowed up and treated as delimiters.

So for example, this filter:

```
(description includes "maybe")
```

is interpreted as this simplified line - note the stray `"`:

```
(f1")
```

where `f1` is this - note the missing `"`:

```
description includes "maybe
```
## 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. -->

Likely the last refactor before addressing:

- https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1308
- https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1500

## How has this been tested?

- Adding and updating tests
- Diffing the output of these two files:
    - `BooleanField.test.boolean_query_-_exhaustive_tests_preprocess.approved.txt`
    - `BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt`

## Screenshots (if appropriate)

## Types of changes

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

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

- [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/BooleanDelimiters.ts b/src/Query/Filter/BooleanDelimiters.ts
index bf7ec1e8..0157ee53 100644
--- a/src/Query/Filter/BooleanDelimiters.ts
+++ b/src/Query/Filter/BooleanDelimiters.ts
@@ -14,15 +14,18 @@ export class BooleanDelimiters {
     public readonly closeFilterChars;
     public readonly closeFilter;
 
-    public constructor(openFilterChars: string, closeFilterChars: string) {
+    public readonly openAndCloseFilterChars;
+
+    private constructor(openFilterChars: string, closeFilterChars: string, openAndCloseFilterChars: string) {
         this.openFilterChars = openFilterChars;
         this.closeFilterChars = closeFilterChars;
+        this.openAndCloseFilterChars = openAndCloseFilterChars;
 
         this.openFilter = anyOfTheseChars(this.openFilterChars);
         this.closeFilter = anyOfTheseChars(this.closeFilterChars);
     }
 
     public static allSupportedDelimiters(): BooleanDelimiters {
-        return new BooleanDelimiters('("', ')"');
+        return new BooleanDelimiters('("', ')"', '()"');
     }
 }
diff --git a/src/Query/Filter/BooleanPreprocessor.ts b/src/Query/Filter/BooleanPreprocessor.ts
index cbedc271..fc156913 100644
--- a/src/Query/Filter/BooleanPreprocessor.ts
+++ b/src/Query/Filter/BooleanPreprocessor.ts
@@ -1,6 +1,16 @@
 import { BooleanDelimiters, anyOfTheseChars } from './BooleanDelimiters';
 
+type BooleanPreprocessorResult = {
+    simplifiedLine: string;
+    filters: { [key: string]: string };
+};
+
 export class BooleanPreprocessor {
+    public static preprocessExpression(line: string): BooleanPreprocessorResult {
+        const parts = BooleanPreprocessor.splitLine(line);
+        return BooleanPreprocessor.getFiltersAndSimplifiedLine(parts);
+    }
+
     public static splitLine(line: string) {
         const delimiters = BooleanDelimiters.allSupportedDelimiters();
 
@@ -58,4 +68,57 @@ export class BooleanPreprocessor {
             .flatMap((substring) => substring.split(closingDelimitersAndSpacesAtEndRegex))
             .filter((substring) => substring !== '');
     }
+
+    private static getFiltersAndSimplifiedLine(parts: string[]) {
+        const delimiters = BooleanDelimiters.allSupportedDelimiters();
+
+        // Holds the reconstructed expression with placeholders
+        let simplifiedLine = '';
+        let currentIndex = 1; // Placeholder index starts at 1
+        const filters: { [key: string]: string } = {}; // To store filter placeholders and their corresponding text
+
+        // Loop to add placeholders-for-filters or operators to the simplifiedLine
+        parts.forEach((part) => {
+            // Check if the part is an operator by matching against the regex without surrounding parentheses
+            if (!BooleanPreprocessor.isAFilter(part, delimiters)) {
+                // It's an operator, space or parenthesis, so add it directly to the simplifiedLine
+                simplifiedLine += `${part}`;
+            } else {
+                // It's a filter, replace it with a placeholder, and save it:
+                const placeholder = `f${currentIndex}`;
+                filters[placeholder] = part;
+                simplifiedLine += placeholder;
+                currentIndex++;
+            }
+        });
+
+        return { simplifiedLine, filters };
+    }
+
+    private static isAFilter(part: string, delimiters: BooleanDelimiters) {
+        // This set of regular expressions was built up empirically through a lot of iteration,
+        // over a very thorough set of sample Boolean filters, in order to detect all the outputs
+        // from splitLine() that were not actually Tasks filters.
+        const onlySpacesAndParentheses = new RegExp(
+            '^' + anyOfTheseChars(' ' + delimiters.openAndCloseFilterChars) + '+$',
+        );
+
+        const binaryOperatorAndParentheses = new RegExp(
+            '^ *' + delimiters.closeFilter + ' *(AND|OR|XOR) *' + delimiters.openFilter + ' *$',
+        );
+
+        const unaryOperatorAndParentheses = new RegExp('^(AND|OR|XOR|NOT) *' + delimiters.openFilter + '$');
+
+        const remnantsOfNot = new RegExp('^' + delimiters.closeFilter + ' *(AND|OR|XOR)$');
+
+        const justOperators = /^(AND|OR|XOR|NOT)$/;
+
+        return ![
+            onlySpacesAndParentheses,
+            binaryOperatorAndParentheses,
+            unaryOperatorAndParentheses,
+            remnantsOfNot,
+            justOperators,
+        ].some((regex) => RegExp(regex).exec(part));
+    }
 }


Test patch:
diff --git a/tests/Query/Filter/BooleanDelimiters.test.ts b/tests/Query/Filter/BooleanDelimiters.test.ts
index 38f60bba..4caccd2b 100644
--- a/tests/Query/Filter/BooleanDelimiters.test.ts
+++ b/tests/Query/Filter/BooleanDelimiters.test.ts
@@ -9,5 +9,7 @@ describe('BooleanDelimiters', () => {
 
         expect(delimiters.closeFilterChars).toEqual(')"');
         expect(delimiters.closeFilter).toEqual('[)"]');
+
+        expect(delimiters.openAndCloseFilterChars).toEqual('()"');
     });
 });
diff --git a/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt
new file mode 100644
index 00000000..0590ef55
--- /dev/null
+++ b/tests/Query/Filter/BooleanField.test.boolean_query_-_exhaustive_tests_preprocess_-_rewrite.approved.txt
@@ -0,0 +1,3247 @@
+Results of preprocessing boolean expressions
+
+
+
+
+Input:
+'(not done) AND (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'((not done)) AND ((is recurring))'
+=>
+Result:
+{
+    "simplifiedLine": "((f1)) AND ((f2))",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) OR (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) XOR (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) AND NOT (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND NOT (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) OR NOT (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT (not done)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT (f1)",
+    "filters": {
+        "f1": "not done"
+    }
+}
+
+--------------------------------------------------------
+
+
+
+Input:
+'"not done" AND (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" AND (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'"not done" OR (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" OR (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'"not done" XOR (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" XOR (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'"not done" AND NOT (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" AND NOT (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'"not done" OR NOT (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" OR NOT (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+
+Input:
+'(not done) AND "is recurring"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND \"f2\"",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'((not done)) AND "is recurring"'
+=>
+Result:
+{
+    "simplifiedLine": "((f1)) AND \"f2\"",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) OR "is recurring"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR \"f2\"",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) XOR "is recurring"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR \"f2\"",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) AND NOT "is recurring"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND NOT \"f2\"",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) OR NOT "is recurring"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT \"f2\"",
+    "filters": {
+        "f1": "not done",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+
+Input:
+'"HAS DUE DATE" OR (DESCRIPTION INCLUDES SPECIAL)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" OR (f2)",
+    "filters": {
+        "f1": "HAS DUE DATE",
+        "f2": "DESCRIPTION INCLUDES SPECIAL"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'"description includes d1" AND "description includes d2"'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" AND \"f2\"",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "description includes d2"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'"has due date" OR (description includes special)'
+=>
+Result:
+{
+    "simplifiedLine": "\"f1\" OR (f2)",
+    "filters": {
+        "f1": "has due date",
+        "f2": "description includes special"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( (description includes 1) AND (description includes 2) AND (description includes 3) ) OR ( (description includes 5) AND (description includes 6) AND (description includes 7) ) AND NOT (description includes 7)'
+=>
+Result:
+{
+    "simplifiedLine": "( (f1) AND (f2) AND (f3) ) OR ( (f4) AND (f5) AND (f6) ) AND NOT (f7)",
+    "filters": {
+        "f1": "description includes 1",
+        "f2": "description includes 2",
+        "f3": "description includes 3",
+        "f4": "description includes 5",
+        "f5": "description includes 6",
+        "f6": "description includes 7",
+        "f7": "description includes 7"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( (description includes a) AND (description includes b) ) AND (description includes c)'
+=>
+Result:
+{
+    "simplifiedLine": "( (f1) AND (f2) ) AND (f3)",
+    "filters": {
+        "f1": "description includes a",
+        "f2": "description includes b",
+        "f3": "description includes c"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( (description includes a) AND (description includes b) AND (description includes c) ) OR ( (description includes d) AND (description includes e) AND (description includes f) )'
+=>
+Result:
+{
+    "simplifiedLine": "( (f1) AND (f2) AND (f3) ) OR ( (f4) AND (f5) AND (f6) )",
+    "filters": {
+        "f1": "description includes a",
+        "f2": "description includes b",
+        "f3": "description includes c",
+        "f4": "description includes d",
+        "f5": "description includes e",
+        "f6": "description includes f"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( (description includes a) OR (description includes b) ) OR (description includes c)'
+=>
+Result:
+{
+    "simplifiedLine": "( (f1) OR (f2) ) OR (f3)",
+    "filters": {
+        "f1": "description includes a",
+        "f2": "description includes b",
+        "f3": "description includes c"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( (description includes a) OR (description includes b) OR (description includes c) ) AND ( (description includes d) OR (description includes e) OR (description includes f) )'
+=>
+Result:
+{
+    "simplifiedLine": "( (f1) OR (f2) OR (f3) ) AND ( (f4) OR (f5) OR (f6) )",
+    "filters": {
+        "f1": "description includes a",
+        "f2": "description includes b",
+        "f3": "description includes c",
+        "f4": "description includes d",
+        "f5": "description includes e",
+        "f6": "description includes f"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( (filter by function task.description.includes('a')) OR (filter by function task.description.includes('b')) OR (filter by function task.description.includes('c')) ) AND ( (filter by function task.description.includes('d')) OR (filter by function task.description.includes('e')) OR (filter by function task.description.includes('f')) )'
+=>
+Result:
+{
+    "simplifiedLine": "( (f1)) OR (f2)) OR (f3)) ) AND ( (f4)) OR (f5)) OR (f6)) )",
+    "filters": {
+        "f1": "filter by function task.description.includes('a'",
+        "f2": "filter by function task.description.includes('b'",
+        "f3": "filter by function task.description.includes('c'",
+        "f4": "filter by function task.description.includes('d'",
+        "f5": "filter by function task.description.includes('e'",
+        "f6": "filter by function task.description.includes('f'"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( description includes a )   AND ( (description includes b)  AND (description includes c) )'
+=>
+Result:
+{
+    "simplifiedLine": "( f1 )   AND ( (f2)  AND (f3) )",
+    "filters": {
+        "f1": "description includes a",
+        "f2": "description includes b",
+        "f3": "description includes c"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( description includes a )   OR  ( (description includes b)  OR  (description includes c) )'
+=>
+Result:
+{
+    "simplifiedLine": "( f1 )   OR  ( (f2)  OR  (f3) )",
+    "filters": {
+        "f1": "description includes a",
+        "f2": "description includes b",
+        "f3": "description includes c"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( description regex matches /(buy|order|voucher|lakeland|purchase|\spresent)/i ) OR ( path includes Home/Shopping )'
+=>
+Result:
+{
+    "simplifiedLine": "( f1 ) OR ( f2 )",
+    "filters": {
+        "f1": "description regex matches /(buy|order|voucher|lakeland|purchase|\\spresent)/i",
+        "f2": "path includes Home/Shopping"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( description regex matches /buy/i ) AND ( path includes some/sample/note.md )'
+=>
+Result:
+{
+    "simplifiedLine": "( f1 ) AND ( f2 )",
+    "filters": {
+        "f1": "description regex matches /buy/i",
+        "f2": "path includes some/sample/note.md"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( filter by function ! 'NON_TASK,CANCELLED'.includes(task.status.type) ) OR ( filter by function const date = task.due.moment; return date ? !date.isValid() : false; ) OR ( filter by function task.due.moment?.isSameOrBefore(moment(), 'day') || false ) OR ( filter by function task.urgency.toFixed(2) === 1.95.toFixed(2) ) OR ( filter by function (!task.isRecurring) && task.originalMarkdown.includes('🔁') ) OR ( filter by function task.file.path.toLocaleLowerCase() === 'TASKS RELEASES/4.1.0 RELEASE.MD'.toLocaleLowerCase() ) OR ( filter by function const taskDate = task.due.moment; const now = moment(); return taskDate?.isSame(now, 'day') || ( !taskDate && task.heading?.includes(now.format('YYYY-MM-DD')) ) || false ) OR ( filter by function const wanted = '#context/home'; return task.heading?.includes(wanted) || task.tags.find( (tag) => tag === wanted ) && true || false; )'
+=>
+Result:
+{
+    "simplifiedLine": "( f1) ) OR ( f2 ) OR ( f3 ) OR ( f4) ) OR ( f5) ) OR ( f6) ) OR ( f7 ) OR ( f8 )",
+    "filters": {
+        "f1": "filter by function ! 'NON_TASK,CANCELLED'.includes(task.status.type",
+        "f2": "filter by function const date = task.due.moment; return date ? !date.isValid() : false;",
+        "f3": "filter by function task.due.moment?.isSameOrBefore(moment(), 'day') || false",
+        "f4": "filter by function task.urgency.toFixed(2) === 1.95.toFixed(2",
+        "f5": "filter by function (!task.isRecurring) && task.originalMarkdown.includes('🔁'",
+        "f6": "filter by function task.file.path.toLocaleLowerCase() === 'TASKS RELEASES/4.1.0 RELEASE.MD'.toLocaleLowerCase(",
+        "f7": "filter by function const taskDate = task.due.moment; const now = moment(); return taskDate?.isSame(now, 'day') || ( !taskDate && task.heading?.includes(now.format('YYYY-MM-DD')) ) || false",
+        "f8": "filter by function const wanted = '#context/home'; return task.heading?.includes(wanted) || task.tags.find( (tag) => tag === wanted ) && true || false;"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'( filter by function ! 'NON_TASK,CANCELLED'.includes(task.status.type); ) OR ( filter by function const date = task.due.moment; return date ? !date.isValid() : false; ) OR ( filter by function task.due.moment?.isSameOrBefore(moment(), 'day') || false; ) OR ( filter by function task.urgency.toFixed(2) === 1.95.toFixed(2); ) OR ( filter by function (!task.isRecurring) && task.originalMarkdown.includes('🔁'); ) OR ( filter by function task.file.path.toLocaleLowerCase() === 'TASKS RELEASES/4.1.0 RELEASE.MD'.toLocaleLowerCase(); ) OR ( filter by function const taskDate = task.due.moment; const now = moment(); return taskDate?.isSame(now, 'day') || ( !taskDate && task.heading?.includes(now.format('YYYY-MM-DD')) ) || false; ) OR ( filter by function const wanted = '#context/home'; return task.heading?.includes(wanted) || task.tags.find( (tag) => tag === wanted ) && true || false; )'
+=>
+Result:
+{
+    "simplifiedLine": "( f1 ) OR ( f2 ) OR ( f3 ) OR ( f4 ) OR ( f5 ) OR ( f6 ) OR ( f7 ) OR ( f8 )",
+    "filters": {
+        "f1": "filter by function ! 'NON_TASK,CANCELLED'.includes(task.status.type);",
+        "f2": "filter by function const date = task.due.moment; return date ? !date.isValid() : false;",
+        "f3": "filter by function task.due.moment?.isSameOrBefore(moment(), 'day') || false;",
+        "f4": "filter by function task.urgency.toFixed(2) === 1.95.toFixed(2);",
+        "f5": "filter by function (!task.isRecurring) && task.originalMarkdown.includes('🔁');",
+        "f6": "filter by function task.file.path.toLocaleLowerCase() === 'TASKS RELEASES/4.1.0 RELEASE.MD'.toLocaleLowerCase();",
+        "f7": "filter by function const taskDate = task.due.moment; const now = moment(); return taskDate?.isSame(now, 'day') || ( !taskDate && task.heading?.includes(now.format('YYYY-MM-DD')) ) || false;",
+        "f8": "filter by function const wanted = '#context/home'; return task.heading?.includes(wanted) || task.tags.find( (tag) => tag === wanted ) && true || false;"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(((((NOT  ( description includes d1 ))))))'
+=>
+Result:
+{
+    "simplifiedLine": "(((((NOT  ( f1 ))))))",
+    "filters": {
+        "f1": "description includes d1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(((((description includes #context/location1)))))'
+=>
+Result:
+{
+    "simplifiedLine": "(((((f1)))))",
+    "filters": {
+        "f1": "description includes #context/location1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'((due this week) AND (description includes Hello World)) OR NOT ((due this week) AND (description includes Hello World))'
+=>
+Result:
+{
+    "simplifiedLine": "((f1) AND (f2)) OR NOT ((f3) AND (f4))",
+    "filters": {
+        "f1": "due this week",
+        "f2": "description includes Hello World",
+        "f3": "due this week",
+        "f4": "description includes Hello World"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(DESCRIPTION INCLUDES wibble) OR (has due date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "DESCRIPTION INCLUDES wibble",
+        "f2": "has due date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(DUE THIS WEEK) AND (DESCRIPTION INCLUDES HELLO WORLD)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2)",
+    "filters": {
+        "f1": "DUE THIS WEEK",
+        "f2": "DESCRIPTION INCLUDES HELLO WORLD"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(HAS START DATE) AND "DESCRIPTION INCLUDES SOME"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND \"f2\"",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SOME"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(HAS START DATE) AND ((DESCRIPTION INCLUDES SOME) OR (HAS DUE DATE))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND ((f2) OR (f3))",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SOME",
+        "f3": "HAS DUE DATE"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(HAS START DATE) AND NOT (DESCRIPTION INCLUDES SOME)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND NOT (f2)",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SOME"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(HAS START DATE) OR ((DESCRIPTION INCLUDES SPECIAL) AND (HAS DUE DATE))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR ((f2) AND (f3))",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SPECIAL",
+        "f3": "HAS DUE DATE"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(HAS START DATE) OR NOT (DESCRIPTION INCLUDES SPECIAL)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SPECIAL"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(HAS START DATE) XOR (DESCRIPTION INCLUDES SPECIAL)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR (f2)",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SPECIAL"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(cancelled after 2021-12-27) OR NOT (cancelled after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "cancelled after 2021-12-27",
+        "f2": "cancelled after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(cancelled before 2021-12-27) OR NOT (cancelled before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "cancelled before 2021-12-27",
+        "f2": "cancelled before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(cancelled date is invalid) OR NOT (cancelled date is invalid)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "cancelled date is invalid",
+        "f2": "cancelled date is invalid"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(cancelled in 2021-12-27 2021-12-29) OR NOT (cancelled in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "cancelled in 2021-12-27 2021-12-29",
+        "f2": "cancelled in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(cancelled on 2021-12-27) OR NOT (cancelled on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "cancelled on 2021-12-27",
+        "f2": "cancelled on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(cancelled this week) OR NOT (cancelled this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "cancelled this week",
+        "f2": "cancelled this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(created after 2021-12-27) OR NOT (created after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "created after 2021-12-27",
+        "f2": "created after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(created before 2021-12-27) OR NOT (created before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "created before 2021-12-27",
+        "f2": "created before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(created date is invalid) OR NOT (created date is invalid)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "created date is invalid",
+        "f2": "created date is invalid"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(created in 2021-12-27 2021-12-29) OR NOT (created in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "created in 2021-12-27 2021-12-29",
+        "f2": "created in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(created on 2021-12-27) OR NOT (created on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "created on 2021-12-27",
+        "f2": "created on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(created this week) OR NOT (created this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "created this week",
+        "f2": "created this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description does not include wibble) OR NOT (description does not include wibble)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "description does not include wibble",
+        "f2": "description does not include wibble"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes "hello world") OR (description includes "42")'
+=>
+Result:
+{
+    "simplifiedLine": "(f1\") OR (f2\")",
+    "filters": {
+        "f1": "description includes \"hello world",
+        "f2": "description includes \"42"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes #context/location1)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)",
+    "filters": {
+        "f1": "description includes #context/location1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes #context/location1) OR (description includes #context/location2 ) OR (  description includes #context/location3 ) OR   (  description includes #context/location4 )'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2 ) OR (  f3 ) OR   (  f4 )",
+    "filters": {
+        "f1": "description includes #context/location1",
+        "f2": "description includes #context/location2",
+        "f3": "description includes #context/location3",
+        "f4": "description includes #context/location4"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes 1)   AND   (description includes 2)   AND   (description includes 3)   AND   (description includes 4)   AND   (description includes 5)   AND   (description includes 6)   AND   (description includes 7)   AND   (description includes 8)   AND   (description includes 9)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)   AND   (f2)   AND   (f3)   AND   (f4)   AND   (f5)   AND   (f6)   AND   (f7)   AND   (f8)   AND   (f9)",
+    "filters": {
+        "f1": "description includes 1",
+        "f2": "description includes 2",
+        "f3": "description includes 3",
+        "f4": "description includes 4",
+        "f5": "description includes 5",
+        "f6": "description includes 6",
+        "f7": "description includes 7",
+        "f8": "description includes 8",
+        "f9": "description includes 9"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes 1) AND (description includes 2) AND (description includes 3)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2) AND (f3)",
+    "filters": {
+        "f1": "description includes 1",
+        "f2": "description includes 2",
+        "f3": "description includes 3"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes 1) AND (description includes 2) AND (description includes 3) AND (description includes 4) AND (description includes 5) AND (description includes 6) AND (description includes 7) AND (description includes 8) AND (description includes 9)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2) AND (f3) AND (f4) AND (f5) AND (f6) AND (f7) AND (f8) AND (f9)",
+    "filters": {
+        "f1": "description includes 1",
+        "f2": "description includes 2",
+        "f3": "description includes 3",
+        "f4": "description includes 4",
+        "f5": "description includes 5",
+        "f6": "description includes 6",
+        "f7": "description includes 7",
+        "f8": "description includes 8",
+        "f9": "description includes 9"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes AND) OR NOT (description includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "description includes AND",
+        "f2": "description includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes SHOULD NOT BE RECOGNISED AS A BOOLEAN)AND(description includes BECAUSE THERE ARE NO SPACES AROUND THE 'AND' OPERATOR)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)AND(f2)",
+    "filters": {
+        "f1": "description includes SHOULD NOT BE RECOGNISED AS A BOOLEAN",
+        "f2": "description includes BECAUSE THERE ARE NO SPACES AROUND THE 'AND' OPERATOR"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) AND   NOT (priority medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND   NOT (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "priority medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) AND (priority medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "priority medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) AND NOT (description includes d2)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND NOT (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "description includes d2"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) OR   NOT (priority medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR   NOT (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "priority medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) OR (description includes d2)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "description includes d2"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) OR (description includes d2) OR (priority medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2) OR (f3)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "description includes d2",
+        "f3": "priority medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) OR (priority medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "priority medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) OR NOT (description includes d2)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "description includes d2"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) XOR (description includes d2)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "description includes d2"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes d1) XOR (priority medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR (f2)",
+    "filters": {
+        "f1": "description includes d1",
+        "f2": "priority medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes line 1) OR (description includes line 1 continued with \ backslash)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "description includes line 1",
+        "f2": "description includes line 1 continued with \\ backslash"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description includes wibble) OR NOT (description includes wibble)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "description includes wibble",
+        "f2": "description includes wibble"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(description regex matches /#t\s/i) OR (description regex matches /#t$/i)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "description regex matches /#t\\s/i",
+        "f2": "description regex matches /#t$/i"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done after 2021-12-27) OR NOT (done after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done after 2021-12-27",
+        "f2": "done after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done before 2021-12-27) OR NOT (done before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done before 2021-12-27",
+        "f2": "done before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done date is invalid) OR NOT (done date is invalid)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done date is invalid",
+        "f2": "done date is invalid"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done in 2021-12-27 2021-12-29) OR NOT (done in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done in 2021-12-27 2021-12-29",
+        "f2": "done in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done on 2021-12-27) OR NOT (done on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done on 2021-12-27",
+        "f2": "done on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done this week) OR NOT (done this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done this week",
+        "f2": "done this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(done) OR NOT (done)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "done",
+        "f2": "done"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due after 2021-12-27) OR NOT (due after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "due after 2021-12-27",
+        "f2": "due after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due before 2021-12-27) OR NOT (due before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "due before 2021-12-27",
+        "f2": "due before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due before tomorrow) AND (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2)",
+    "filters": {
+        "f1": "due before tomorrow",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due date is invalid) OR NOT (due date is invalid)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "due date is invalid",
+        "f2": "due date is invalid"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due in 2021-12-27 2021-12-29) OR NOT (due in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "due in 2021-12-27 2021-12-29",
+        "f2": "due in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due on 2021-12-27) OR NOT (due on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "due on 2021-12-27",
+        "f2": "due on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due this week) AND (description includes Hello World)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2)",
+    "filters": {
+        "f1": "due this week",
+        "f2": "description includes Hello World"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(due this week) OR NOT (due this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "due this week",
+        "f2": "due this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(exclude sub-items) OR NOT (exclude sub-items)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "exclude sub-items",
+        "f2": "exclude sub-items"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(filename includes wibble) OR NOT (filename includes wibble)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "filename includes wibble",
+        "f2": "filename includes wibble"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(filter by function task.isDone) OR NOT (filter by function task.isDone)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "filter by function task.isDone",
+        "f2": "filter by function task.isDone"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(folder does not include some/path) OR NOT (folder does not include some/path)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "folder does not include some/path",
+        "f2": "folder does not include some/path"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(folder includes AND) OR NOT (folder includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "folder includes AND",
+        "f2": "folder includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(folder includes some/path) OR NOT (folder includes some/path)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "folder includes some/path",
+        "f2": "folder includes some/path"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(happens after 2021-12-27) OR NOT (happens after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "happens after 2021-12-27",
+        "f2": "happens after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(happens before 2021-12-27) OR NOT (happens before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "happens before 2021-12-27",
+        "f2": "happens before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(happens in 2021-12-27 2021-12-29) OR NOT (happens in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "happens in 2021-12-27 2021-12-29",
+        "f2": "happens in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(happens on 2021-12-27) OR NOT (happens on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "happens on 2021-12-27",
+        "f2": "happens on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(happens this week) OR NOT (happens this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "happens this week",
+        "f2": "happens this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has cancelled date) OR NOT (has cancelled date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has cancelled date",
+        "f2": "has cancelled date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has created date) OR NOT (has created date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has created date",
+        "f2": "has created date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has depends on) OR NOT (has depends on)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has depends on",
+        "f2": "has depends on"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has done date) OR NOT (has done date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has done date",
+        "f2": "has done date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has due date) OR ((HAS START DATE) AND (due after 2021-12-27))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR ((f2) AND (f3))",
+    "filters": {
+        "f1": "has due date",
+        "f2": "HAS START DATE",
+        "f3": "due after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has due date) OR NOT (has due date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has due date",
+        "f2": "has due date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has happens date) OR NOT (has happens date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has happens date",
+        "f2": "has happens date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has id) OR NOT (has id)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has id",
+        "f2": "has id"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has scheduled date) OR NOT (has scheduled date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has scheduled date",
+        "f2": "has scheduled date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) AND "description includes some"'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND \"f2\"",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes some"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) AND ((description includes some) OR (has due date))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND ((f2) OR (f3))",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes some",
+        "f3": "has due date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) AND (description includes some)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND (f2)",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes some"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) AND NOT (description includes some)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND NOT (f2)",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes some"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) OR ((description includes special) AND (has due date))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR ((f2) AND (f3))",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes special",
+        "f3": "has due date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) OR NOT (description includes special)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes special"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) OR NOT (has start date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has start date",
+        "f2": "has start date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has start date) XOR (description includes special)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR (f2)",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes special"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has tag) OR NOT (has tag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has tag",
+        "f2": "has tag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(has tags) OR NOT (has tags)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "has tags",
+        "f2": "has tags"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(heading does not include wibble) OR NOT (heading does not include wibble)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "heading does not include wibble",
+        "f2": "heading does not include wibble"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(heading includes AND) OR NOT (heading includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "heading includes AND",
+        "f2": "heading includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(heading includes wibble) OR NOT (heading includes wibble)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "heading includes wibble",
+        "f2": "heading includes wibble"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(id does not include abc123) OR NOT (id does not include abc123)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "id does not include abc123",
+        "f2": "id does not include abc123"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(id includes AND) OR NOT (id includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "id includes AND",
+        "f2": "id includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(id includes abc123) OR NOT (id includes abc123)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "id includes abc123",
+        "f2": "id includes abc123"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is blocked) OR NOT (is blocked)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "is blocked",
+        "f2": "is blocked"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is blocking) OR NOT (is blocking)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "is blocking",
+        "f2": "is blocking"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is not blocked) OR NOT (is not blocked)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "is not blocked",
+        "f2": "is not blocked"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is not blocking) OR NOT (is not blocking)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "is not blocking",
+        "f2": "is not blocking"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is not recurring) OR NOT (is not recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "is not recurring",
+        "f2": "is not recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is not recurring) XOR ((path includes ab/c) OR (happens before 2021-12-27))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR ((f2) OR (f3))",
+    "filters": {
+        "f1": "is not recurring",
+        "f2": "path includes ab/c",
+        "f3": "happens before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(is recurring) OR NOT (is recurring)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "is recurring",
+        "f2": "is recurring"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no cancelled date) OR NOT (no cancelled date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no cancelled date",
+        "f2": "no cancelled date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no created date) OR NOT (no created date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no created date",
+        "f2": "no created date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no depends on) OR NOT (no depends on)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no depends on",
+        "f2": "no depends on"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no due date) OR NOT (no due date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no due date",
+        "f2": "no due date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no happens date) OR NOT (no happens date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no happens date",
+        "f2": "no happens date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no id) OR NOT (no id)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no id",
+        "f2": "no id"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no scheduled date) OR NOT (no scheduled date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no scheduled date",
+        "f2": "no scheduled date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no start date) OR NOT (no start date)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no start date",
+        "f2": "no start date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no tag) OR NOT (no tag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no tag",
+        "f2": "no tag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(no tags) OR NOT (no tags)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "no tags",
+        "f2": "no tags"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(not done) OR NOT (not done)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "not done",
+        "f2": "not done"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path does not include some/path) OR NOT (path does not include some/path)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "path does not include some/path",
+        "f2": "path does not include some/path"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes ()some example()) OR (path includes ((some example)))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)) OR (f2)))",
+    "filters": {
+        "f1": "path includes ()some example(",
+        "f2": "path includes ((some example"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes (some example) OR (path includes )some example()'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "path includes (some example",
+        "f2": "path includes )some example("
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes (some example)) OR (path includes )some example()'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)) OR (f2)",
+    "filters": {
+        "f1": "path includes (some example",
+        "f2": "path includes )some example("
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes )some example() OR (path includes (some example))'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2))",
+    "filters": {
+        "f1": "path includes )some example(",
+        "f2": "path includes (some example"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes A) OR (path includes Test.md)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "path includes A",
+        "f2": "path includes Test.md"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes AND) OR NOT (path includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "path includes AND",
+        "f2": "path includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a) AND NOT(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND NOT(f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a) AND(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) AND(f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a) OR NOT(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT(f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a) OR(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR(f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a) XOR(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) XOR(f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a)AND (path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)AND (f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a)AND NOT(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)AND NOT(f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a)OR (path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)OR (f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a)OR NOT (path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)OR NOT (f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes a)XOR (path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1)XOR (f2)",
+    "filters": {
+        "f1": "path includes a",
+        "f2": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(path includes some/path) OR NOT (path includes some/path)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "path includes some/path",
+        "f2": "path includes some/path"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is above none) OR NOT (priority is above none)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "priority is above none",
+        "f2": "priority is above none"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is below none) OR NOT (priority is below none)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "priority is below none",
+        "f2": "priority is below none"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is high) OR NOT (priority is high)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "priority is high",
+        "f2": "priority is high"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is highest) OR (priority is lowest)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR (f2)",
+    "filters": {
+        "f1": "priority is highest",
+        "f2": "priority is lowest"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is low) OR NOT (priority is low)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "priority is low",
+        "f2": "priority is low"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is medium) OR NOT (priority is medium)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "priority is medium",
+        "f2": "priority is medium"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(priority is none) OR NOT (priority is none)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "priority is none",
+        "f2": "priority is none"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(recurrence does not include wednesday) OR NOT (recurrence does not include wednesday)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "recurrence does not include wednesday",
+        "f2": "recurrence does not include wednesday"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(recurrence includes wednesday) OR NOT (recurrence includes wednesday)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "recurrence includes wednesday",
+        "f2": "recurrence includes wednesday"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(root does not include some) OR NOT (root does not include some)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "root does not include some",
+        "f2": "root does not include some"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(root includes AND) OR NOT (root includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "root includes AND",
+        "f2": "root includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(root includes some) OR NOT (root includes some)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "root includes some",
+        "f2": "root includes some"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(scheduled after 2021-12-27) OR NOT (scheduled after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "scheduled after 2021-12-27",
+        "f2": "scheduled after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(scheduled before 2021-12-27) OR NOT (scheduled before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "scheduled before 2021-12-27",
+        "f2": "scheduled before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(scheduled date is invalid) OR NOT (scheduled date is invalid)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "scheduled date is invalid",
+        "f2": "scheduled date is invalid"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(scheduled in 2021-12-27 2021-12-29) OR NOT (scheduled in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "scheduled in 2021-12-27 2021-12-29",
+        "f2": "scheduled in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(scheduled on 2021-12-27) OR NOT (scheduled on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "scheduled on 2021-12-27",
+        "f2": "scheduled on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(scheduled this week) OR NOT (scheduled this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "scheduled this week",
+        "f2": "scheduled this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(start date is invalid) OR NOT (start date is invalid)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "start date is invalid",
+        "f2": "start date is invalid"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(starts after 2021-12-27) OR NOT (starts after 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "starts after 2021-12-27",
+        "f2": "starts after 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(starts before 2021-12-27) OR NOT (starts before 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "starts before 2021-12-27",
+        "f2": "starts before 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(starts in 2021-12-27 2021-12-29) OR NOT (starts in 2021-12-27 2021-12-29)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "starts in 2021-12-27 2021-12-29",
+        "f2": "starts in 2021-12-27 2021-12-29"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(starts on 2021-12-27) OR NOT (starts on 2021-12-27)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "starts on 2021-12-27",
+        "f2": "starts on 2021-12-27"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(starts this week) OR NOT (starts this week)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "starts this week",
+        "f2": "starts this week"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(status.name includes cancelled) OR NOT (status.name includes cancelled)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "status.name includes cancelled",
+        "f2": "status.name includes cancelled"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(status.type is IN_PROGRESS) OR NOT (status.type is IN_PROGRESS)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "status.type is IN_PROGRESS",
+        "f2": "status.type is IN_PROGRESS"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tag does not include #sometag) OR NOT (tag does not include #sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tag does not include #sometag",
+        "f2": "tag does not include #sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tag does not include sometag) OR NOT (tag does not include sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tag does not include sometag",
+        "f2": "tag does not include sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tag includes #sometag) OR NOT (tag includes #sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tag includes #sometag",
+        "f2": "tag includes #sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tag includes AND) OR NOT (tag includes AND)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tag includes AND",
+        "f2": "tag includes AND"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tag includes sometag) OR NOT (tag includes sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tag includes sometag",
+        "f2": "tag includes sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tags do not include #sometag) OR NOT (tags do not include #sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tags do not include #sometag",
+        "f2": "tags do not include #sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tags do not include sometag) OR NOT (tags do not include sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tags do not include sometag",
+        "f2": "tags do not include sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tags include #sometag) OR NOT (tags include #sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tags include #sometag",
+        "f2": "tags include #sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'(tags include sometag) OR NOT (tags include sometag)'
+=>
+Result:
+{
+    "simplifiedLine": "(f1) OR NOT (f2)",
+    "filters": {
+        "f1": "tags include sometag",
+        "f2": "tags include sometag"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'AND (description includes d1)'
+=>
+Result:
+{
+    "simplifiedLine": "f1)",
+    "filters": {
+        "f1": "AND (description includes d1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT   (  happens before blahblahblah  )'
+=>
+Result:
+{
+    "simplifiedLine": "NOT   (  f1  )",
+    "filters": {
+        "f1": "happens before blahblahblah"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT  ( description includes d1 )'
+=>
+Result:
+{
+    "simplifiedLine": "NOT  ( f1 )",
+    "filters": {
+        "f1": "description includes d1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT ( NOT ( is blocking ) )'
+=>
+Result:
+{
+    "simplifiedLine": "NOT ( NOT ( f1 ) )",
+    "filters": {
+        "f1": "is blocking"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT ((HAS START DATE) OR (DESCRIPTION INCLUDES SPECIAL))'
+=>
+Result:
+{
+    "simplifiedLine": "NOT ((f1) OR (f2))",
+    "filters": {
+        "f1": "HAS START DATE",
+        "f2": "DESCRIPTION INCLUDES SPECIAL"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT ((has start date) OR (description includes special))'
+=>
+Result:
+{
+    "simplifiedLine": "NOT ((f1) OR (f2))",
+    "filters": {
+        "f1": "has start date",
+        "f2": "description includes special"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT (HAS START DATE)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT (f1)",
+    "filters": {
+        "f1": "HAS START DATE"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT (description blahblah d1)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT (f1)",
+    "filters": {
+        "f1": "description blahblah d1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT (description includes d1)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT (f1)",
+    "filters": {
+        "f1": "description includes d1"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT (happens before blahblahblah)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT (f1)",
+    "filters": {
+        "f1": "happens before blahblahblah"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT (has start date)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT (f1)",
+    "filters": {
+        "f1": "has start date"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'NOT(path includes b)'
+=>
+Result:
+{
+    "simplifiedLine": "NOT(f1)",
+    "filters": {
+        "f1": "path includes b"
+    }
+}
+
+--------------------------------------------------------
+
+
+Input:
+'OR (description includes d1)'
+=>
+Result:
+{
+    "simplifiedLine": "f1)",
+    "filters": {
+        "f1": "OR (description includes d1"
+    }
+}
+
+--------------------------------------------------------
+
+
diff --git a/tests/Query/Filter/BooleanField.test.ts b/tests/Query/Filter/BooleanField.test.ts
index 9c456a84..2b3a258d 100644
--- a/tests/Query/Filter/BooleanField.test.ts
+++ b/tests/Query/Filter/BooleanField.test.ts
@@ -339,6 +339,10 @@ describe('boolean query - exhaustive tests', () => {
         verifyBooleanExpressionPreprocessing(BooleanField.preprocessExpressionV1);
     });
 
+    it('preprocess - rewrite', () => {
+        verifyBooleanExpressionPreprocessing(BooleanPreprocessor.preprocessExpression);
+    });
+
     it('explain', () => {
         verifyBooleanExpressionExplanation();
     });
diff --git a/tests/Query/Filter/BooleanPreprocessor.test.ts b/tests/Query/Filter/BooleanPreprocessor.test.ts
index 0ac0e7e1..ec538028 100644
--- a/tests/Query/Filter/BooleanPreprocessor.test.ts
+++ b/tests/Query/Filter/BooleanPreprocessor.test.ts
@@ -1,145 +1,167 @@
 import { BooleanPreprocessor } from '../../../src/Query/Filter/BooleanPreprocessor';
 
-function split(line: string) {
-    return BooleanPreprocessor.splitLine(line);
+function preprocess(line: string) {
+    return BooleanPreprocessor.preprocessExpression(line);
 }
 
 describe('BooleanPreprocessor', () => {
     describe('single operators - surrounded by at least one space', () => {
         it('single sub-expression', () => {
-            expect(split('(not done)')).toMatchInlineSnapshot(`
-                [
-                  "(",
-                  "not done",
-                  ")",
-                ]
+            expect(preprocess('(not done)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "not done",
+                  },
+                  "simplifiedLine": "(f1)",
+                }
             `);
         });
 
         it('simple AND', () => {
-            expect(split('(done) AND (has done date)')).toMatchInlineSnapshot(`
-                [
-                  "(",
-                  "done",
-                  ") AND (",
-                  "has done date",
-                  ")",
-                ]
+            expect(preprocess('(done) AND (has done date)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "done",
+                    "f2": "has done date",
+                  },
+                  "simplifiedLine": "(f1) AND (f2)",
+                }
+            `);
+        });
+
+        it('simple AND - filters capitalised', () => {
+            expect(preprocess('(DONE) AND (HAS DONE DATE)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "DONE",
+                    "f2": "HAS DONE DATE",
+                  },
+                  "simplifiedLine": "(f1) AND (f2)",
+                }
             `);
         });
 
         it('simple AND NOT', () => {
-            expect(split('(done) AND  NOT (has done date)')).toMatchInlineSnapshot(`
-                [
-                  "(",
-                  "done",
-                  ") AND",
-                  "  ",
-                  "NOT (",
-                  "has done date",
-                  ")",
-                ]
+            expect(preprocess('(done) AND  NOT (has done date)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "done",
+                    "f2": "has done date",
+                  },
+                  "simplifiedLine": "(f1) AND  NOT (f2)",
+                }
             `);
         });
 
         it('simple OR', () => {
-            expect(split('(done) OR (has done date)')).toMatchInlineSnapshot(`
-                [
-                  "(",
-                  "done",
-                  ") OR (",
-                  "has done date",
-                  ")",
-                ]
+            expect(preprocess('(done) OR (has done date)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "done",
+                    "f2": "has done date",
+                  },
+                  "simplifiedLine": "(f1) OR (f2)",
+                }
             `);
         });
 
         it('simple OR NOT', () => {
-            expect(split('(done) OR  NOT (has done date)')).toMatchInlineSnapshot(`
-                [
-                  "(",
-                  "done",
-                  ") OR",
-                  "  ",
-                  "NOT (",
-                  "has done date",
-                  ")",
-                ]
+            expect(preprocess('(done) OR  NOT (has done date)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "done",
+                    "f2": "has done date",
+                  },
+                  "simplifiedLine": "(f1) OR  NOT (f2)",
+                }
             `);
         });
 
         it('simple XOR', () => {
-            expect(split('"done" XOR "has done date"')).toMatchInlineSnapshot(`
-                [
-                  """,
-                  "done",
-                  "" ",
-                  "XOR",
-                  " "",
-                  "has done date",
-                  """,
-                ]
+            expect(preprocess('"done" XOR "has done date"')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "done",
+                    "f2": "has done date",
+                  },
+                  "simplifiedLine": ""f1" XOR "f2"",
+                }
             `);
         });
 
         it('simple unary NOT', () => {
-            expect(split('NOT  (not done)')).toMatchInlineSnapshot(`
-                [
-                  "NOT  (",
-                  "not done",
-                  ")",
-                ]
+            expect(preprocess('NOT  (not done)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "not done",
+                  },
+                  "simplifiedLine": "NOT  (f1)",
+                }
             `);
         });
     });
 
     describe('single operators - missing spaces around operator', () => {
         it('simple AND - but spaces missing around AND', () => {
-            expect(split('(done)AND(has done date)')).toMatchInlineSnapshot(`
-                [
-                  "(",
-                  "done",
-                  ")AND(",
-                  "has done date",
-                  ")",
-                ]
+            expect(preprocess('(done)AND(has done date)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "done",
+                    "f2": "has done date",
+                  },
+                  "simplifiedLine": "(f1)AND(f2)",
+                }
             `);
         });
 
         it('simple unary NOT - but spaces missing after NOT', () => {
-            expect(split('NOT(not done)')).toMatchInlineSnapshot(`
-                [
-                  "NOT(",
-                  "not done",
-                  ")",
-                ]
+            expect(preprocess('NOT(not done)')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "not done",
+                  },
+                  "simplifiedLine": "NOT(f1)",
+                }
             `);
         });
     });
 
+    describe('filters ending with delimiters', () => {
+        it('swallows last character if filter ends with closing delimiter character )', () => {
+            const result = preprocess('"description includes (maybe)"');
+            // TODO Fix imbalanced delimiters by requiring the same delimiter set to be used in Boolean lines.
+            expect(result.simplifiedLine).toEqual('"f1)"');
+            expect(result.filters['f1']).toEqual('description includes (maybe');
+        });
+
+        it('swallows last character if filter ends with closing delimiter character "', () => {
+            const result = preprocess('(description includes "maybe")');
+            // TODO Fix imbalanced delimiters by requiring the same delimiter set to be used in Boolean lines.
+            expect(result.simplifiedLine).toEqual('(f1")');
+            expect(result.filters['f1']).toEqual('description includes "maybe');
+        });
+    });
+
     describe('extra delimiters', () => {
         it('redundant ( surrounding unary NOT', () => {
-            expect(split('(((((NOT  ( description includes d1 ))))))')).toMatchInlineSnapshot(`
-                [
-                  "(((((",
-                  "NOT  (",
-                  " ",
-                  "description includes d1",
-                  " ))))))",
-                ]
+            expect(preprocess('(((((NOT  ( description includes d1 ))))))')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "description includes d1",
+                  },
+                  "simplifiedLine": "(((((NOT  ( f1 ))))))",
+                }
             `);
         });
 
         it('redundant " surrounding unary NOT', () => {
-            expect(split('"""""NOT  " description includes d1 """"""')).toMatchInlineSnapshot(`
-                [
-                  """"""",
-                  "NOT",
-                  "  "",
-                  " ",
-                  "description includes d1",
-                  " """"""",
-                ]
+            expect(preprocess('"""""NOT  " description includes d1 """"""')).toMatchInlineSnapshot(`
+                {
+                  "filters": {
+                    "f1": "description includes d1",
+                  },
+                  "simplifiedLine": """"""NOT  " f1 """"""",
+                }
             `);
         });
     });