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

Issue:
Allow easy searching/filtering for date ranges relative to current date: "this week/month/quarter/year" (analogous to "today")
### ⚠️ Please check that this feature request hasn't been suggested before.

- [X] I searched previous [Ideas in Discussions](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/categories/ideas-any-new-feature-requests-go-in-issues-please?discussions_q=category%3A%22Ideas%3A+Any+New+Feature+Requests+go+in+Issues+please%22+-label%3A%22status%3A+released%22+-label%3Aduplicate+sort%3Atop) didn't find any similar feature requests.
- [X] I searched previous [Issues](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+enhancement%22+sort%3Acomments-desc) didn't find any similar feature requests.

### 🔖 Feature description

The `today` feature is helpful but in case the query is done on a longer period, it has be specified as `after YYYY-MM-DD` && `before YYYY-MM-DD` which is not related to the current state of dates.

### ✔️ Solution

I propose to create keywords like `this week/month/quarter/semester/year` for the current periods

### ❓ Alternatives

dataview plugin has another approach - they provide "start/end of the week/month" keywords: `sow`, `eow`, `som`, `eom`. This could be another way:  `after som/sow` && `before eom/eow`.

### 📝 Additional Context

Maybe somehow related to https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1582

Pull request:
feat: Add searches on date ranges
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

## Motivation and Context

Fixes #1613, enables searches on `last|this|next week|month|quarter|year` relative to the current date, ie the search for today and in a week will produce different results

Also introduces searches between two dates in `YYYY-MM-DD` format, for example `due on 2023-02-25 2023-03-25`

Done by pair programming with @claremacrae 

Initial PR #1731 

## How has this been tested?

Tests for new functions have been added

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

Internal changes:

- [ ] **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)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)


Patch:
diff --git a/docs/queries/filters.md b/docs/queries/filters.md
index 4ed7c730..4302be39 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -19,12 +19,19 @@ parent: Queries
 
 ---
 
-## Dates
+## Searching for dates
 
-`<date>` filters can be given in natural language or in formal notation.
+### Specific dates
+
+`<date>` filters can be given in natural language or in formal notation (`YYYY-MM-DD`).
 The following are some examples of valid `<date>` filters as inspiration:
 
+Formal notation:
+
 - `2021-05-25`
+
+Natural language notation:
+
 - `yesterday`
 - `today`
 - `tomorrow`
@@ -38,6 +45,49 @@ Use `next tuesday` instead if you mean "next tuesday".
 
 When the day changes, relative dates like `due today` are re-evaluated so that the list stays up-to-date.
 
+### Date ranges
+
+`<date range>` can be given in 2 ways.
+
+{: .released }
+Date range searches were introduced in Tasks 1.26.0.
+
+#### Date ranges in formal notation
+
+`<date range>` may be specified as 2 valid dates in `YYYY-MM-DD` format. Dates on either end are included, that is it is an inclusive search.
+
+`before <date range>` will match before the earliest date of the range. `after <date range>` will match after the latest date of the range.
+
+Notes:
+
+- If one of the `YYYY-MM-DD` dates is invalid, then it is ignored and the filter will behave as `<date>` not `<date range>`
+- Date range cannot be specified by 2 natural language dates eg `next monday three weeks`
+- `in` and `on` can be omitted
+
+Example:
+
+- `2022-01-01 2023-02-01`
+
+#### Date ranges in natural language notation
+
+Tasks supports natural language date ranges as: `last|this|next week|month|quarter|year`. Tasks will process this range and convert it to formal notation (`YYYY-MM-DD`) internally.
+
+Notes:
+
+- Currently all weeks are defined as [ISO 8601](https://en.wikipedia.org/wiki/ISO_week_date) weeks starting on Monday and ending on Sunday. We will provide more flexibility in a future release
+- Natural language date ranges support only the keywords specified, for example `previous half of year` and `next semester` are not supported
+
+Examples:
+
+- `this week` (from this week's Monday to Sunday inclusive)
+- `after this month`
+- `next quarter`
+- `before next year`
+
+### Troubleshooting date searches
+
+Add `explain` in case of issue and double check that both dates are valid or the natural date range is correct and supported.
+
 ### Finding Tasks with Invalid Dates
 
 {: .released }
@@ -121,44 +171,56 @@ For full details of combining filters with boolean operators, see [Combining Fil
 - `no done date`
 - `has done date`
 - `done (before|after|on) <date>`
+- `done (before|after|in) YYYY-MM-DD YYYY-MM-DD`
+- `done (before|after|in) (last|this|next) (week|month|quarter|year)`
 - `done date is invalid`
 
 {: .released }
 `no done date` and `has done date` were introduced in Tasks 1.7.0.<br>
-`done date is invalid` was introduced in Tasks 1.16.0.
+`done date is invalid` was introduced in Tasks 1.16.0.<br>
+`done (before|after|in) <date range>` searches were introduced in Tasks 1.26.0.
 
 ### Due Date
 
 - `no due date`
 - `has due date`
 - `due (before|after|on) <date>`
+- `due (before|after|in) YYYY-MM-DD YYYY-MM-DD`
+- `due (before|after|in) (last|this|next) (week|month|quarter|year)`
 - `due date is invalid`
 
 {: .released }
 `has due date` was introduced in Tasks 1.6.0.<br>
-`due date is invalid` was introduced in Tasks 1.16.0.
+`due date is invalid` was introduced in Tasks 1.16.0.<br>
+`due (before|after|in) <date range>` searches were introduced in Tasks 1.26.0.
 
 ### Scheduled Date
 
 - `no scheduled date`
 - `has scheduled date`
 - `scheduled (before|after|on) <date>`
+- `scheduled (before|after|in) YYYY-MM-DD YYYY-MM-DD`
+- `scheduled (before|after|in) (last|this|next) (week|month|quarter|year)`
 - `scheduled date is invalid`
 
 {: .released }
 `has scheduled date` was introduced in Tasks 1.6.0.<br>
-`scheduled date is invalid` was introduced in Tasks 1.16.0.
+`scheduled date is invalid` was introduced in Tasks 1.16.0.<br>
+`scheduled (before|after|in) <date range>` searches were introduced in Tasks 1.26.0.
 
 ### Start Date
 
 - `no start date`
 - `has start date`
 - `starts (before|after|on) <date>`
+- `starts (before|after|in) YYYY-MM-DD YYYY-MM-DD`
+- `starts (before|after|in) (last|this|next) (week|month|quarter|year)`
 - `start date is invalid`
 
 {: .released }
 `has start date` was Introduced in Tasks 1.6.0.<br>
-`start date is invalid` was introduced in Tasks 1.16.0.
+`start date is invalid` was introduced in Tasks 1.16.0.<br>
+`starts (before|after|in) <date range>` searches were introduced in Tasks 1.26.0.
 
 When filtering queries by [start date]({{ site.baseurl }}{% link getting-started/dates.md %}#-start),
 the result will include tasks without a start date.
@@ -173,6 +235,8 @@ Such filter could be:
 ### Happens
 
 - `happens (before|after|on) <date>`
+- `happens (before|after|in) YYYY-MM-DD YYYY-MM-DD`
+- `happens (before|after|in) (last|this|next) (week|month|quarter|year)`
 
 `happens` returns any task for a matching start date, scheduled date, _or_ due date.
 For example, `happens before tomorrow` will return all tasks that are starting, scheduled, or due earlier than tomorrow.
@@ -185,7 +249,8 @@ because the tasks starts before tomorrow. Only one of the dates needs to match.
   - Return tasks where _any_ of start date, scheduled date, _or_ due date are set.
 
 {: .released }
-`no happens date` and `has happens date` were introduced in Tasks 1.7.0.
+`no happens date` and `has happens date` were introduced in Tasks 1.7.0.<br>
+`happens (before|after|in) <date range>` searches were introduced in Tasks 1.26.0.
 
 ## Filters for Task Statuses
 
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index ff820872..acb6fb4a 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -18,14 +18,14 @@ This table summarizes the filters and other options available inside a `tasks` b
 | [Filters][1]                                                                                                                                                                                        | [Sort][2]                                   | [Group][3]             | [Display][4]           |
 | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- | ---------------------- |
 | `done`<br>`not done`                                                                                                                                                                                | `sort by status`                            | `group by status`      |                        |
-| `done (before, after, on) <date>`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                                                                                                   | `sort by done`                              | `group by done`        | `hide done date`       |
+| `done (before, after, on) <date>`<br>`done (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`done (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                                                                                                   | `sort by done`                              | `group by done`        | `hide done date`       |
 | `status.name (includes, does not include) <string>`<br>`status.name (regex matches, regex does not match) /regex/i`                                                                                 | `sort by status.name`                       | `group by status.name` |                        |
 | `status.type (is, is not) (TODO, DONE, IN_PROGRESS, CANCELLED, NON_TASK)`                                                                                                                           | `sort by status.type`                       | `group by status.type` |                        |
 |                                                                                                                                                                                                     |                                             |                        | `hide created date`    |
-| `starts (before, after, on) <date>`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                                                                                              | `sort by start`                             | `group by start`       | `hide start date`      |
-| `scheduled (before, after, on) <date>`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid`                                                                               | `sort by scheduled`                         | `group by scheduled`   | `hide scheduled date`  |
-| `due (before, after, on) <date>`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                                                                                                       | `sort by due`                               | `group by due`         | `hide due date`        |
-| `happens (before, after, on) <date>`<br>`has happens date`<br>`no  happens date`                                                                                                                    | `sort by happens`                           | `group by happens`     |                        |
+| `starts (before, after, on) <date>`<br>`starts (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`starts (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                                                                                              | `sort by start`                             | `group by start`       | `hide start date`      |
+| `scheduled (before, after, on) <date>`<br>`scheduled (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`scheduled (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid`                                                                               | `sort by scheduled`                         | `group by scheduled`   | `hide scheduled date`  |
+| `due (before, after, on) <date>`<br>`due (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`due (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                                                                                                       | `sort by due`                               | `group by due`         | `hide due date`        |
+| `happens (before, after, on) <date>`<br>`happens (before, after, in) YYYY-MM-DD YYYY-MM-DD`<br>`happens (before, after, in) (last, this, next) (week, month, quarter, year)`<br>`has happens date`<br>`no  happens date`                                                                                                                    | `sort by happens`                           | `group by happens`     |                        |
 | `is recurring`<br>`is not recurring`                                                                                                                                                                |                                             | `group by recurring`   |                        |
 | `recurrence (includes, does not include) <string>`<br>`recurrence (regex matches, regex does not match) /regex/i`                                                                                   |                                             | `group by recurrence`  | `hide recurrence rule` |
 | `priority is (above, below, not)? (low, none, medium, high)`                                                                                                                                        | `sort by priority`                          | `group by priority`    | `hide priority`        |
diff --git a/src/Query/DateParser.ts b/src/Query/DateParser.ts
index 2a64b595..c403de61 100644
--- a/src/Query/DateParser.ts
+++ b/src/Query/DateParser.ts
@@ -32,9 +32,51 @@ export class DateParser {
         const endDate = result[1] && result[1].start ? result[1].start : startDate;
         const start = window.moment(startDate.date());
         const end = window.moment(endDate.date());
+
+        let dateRange: [moment.Moment, moment.Moment] = [start, end];
         if (end.isBefore(start)) {
-            return [end, start];
+            dateRange = [end, start];
+        }
+
+        const naturalDateRangeRegexp = /(last|this|next) (week|month|quarter|year)/;
+        const naturalDateRangeMatch = input.match(naturalDateRangeRegexp);
+        if (naturalDateRangeMatch && naturalDateRangeMatch.length === 3) {
+            const lastThisNext = naturalDateRangeMatch[1];
+            const delta = moment.duration();
+            const range = naturalDateRangeMatch[2];
+            switch (range) {
+                case 'month':
+                case 'quarter':
+                case 'year':
+                case 'week':
+                    // This switch-case is only to avoid recasting String in unitOfTime.DurationConstructor accepted by Duration.add()
+                    delta.add(1, range);
+            }
+
+            dateRange = [moment(), moment()];
+            switch (lastThisNext) {
+                case 'last':
+                    dateRange.forEach((d) => d.subtract(delta));
+                    break;
+                case 'next':
+                    dateRange.forEach((d) => d.add(delta));
+                    break;
+            }
+
+            switch (range) {
+                case 'month':
+                case 'quarter':
+                case 'year':
+                    dateRange = [dateRange[0].startOf(range), dateRange[1].endOf(range)];
+                    break;
+                case 'week':
+                    dateRange = [dateRange[0].startOf('isoWeek'), dateRange[1].endOf('isoWeek')];
+                    break;
+            }
         }
-        return [start, end];
+
+        // Dates shall be at midnight eg 00:00
+        dateRange.forEach((d) => d.startOf('day'));
+        return dateRange;
     }
 }
diff --git a/src/Query/Filter/DateField.ts b/src/Query/Filter/DateField.ts
index dac54b33..b792996c 100644
--- a/src/Query/Filter/DateField.ts
+++ b/src/Query/Filter/DateField.ts
@@ -52,17 +52,18 @@ export abstract class DateField extends Field {
         const fieldNameKeywordDate = Field.getMatch(this.filterRegExp(), line);
         if (fieldNameKeywordDate !== null) {
             const fieldKeyword = fieldNameKeywordDate[1];
-            const fieldDate = DateParser.parseDate(fieldNameKeywordDate[2]);
-            if (!fieldDate.isValid()) {
+            const fieldDateString = fieldNameKeywordDate[2];
+            const fieldDates: [moment.Moment, moment.Moment] = DateParser.parseDateRange(fieldDateString);
+            if (!fieldDates[0].isValid() || !fieldDates[1].isValid()) {
                 result.error = 'do not understand ' + this.fieldName() + ' date';
             } else {
-                const filterFunction = this.buildFilterFunction(fieldKeyword, fieldDate);
+                const filterFunction = this.buildFilterFunction(fieldKeyword, fieldDates);
 
                 const explanation = DateField.buildExplanation(
                     this.fieldNameForExplanation(),
                     fieldKeyword,
                     this.filterResultIfFieldMissing(),
-                    fieldDate,
+                    fieldDates,
                 );
                 result.filter = new Filter(line, filterFunction, new Explanation(explanation));
             }
@@ -75,17 +76,20 @@ export abstract class DateField extends Field {
     /**
      * Builds function that actually filters the tasks depending on the date
      * @param fieldKeyword relationship to be held with the date 'before', 'after'
-     * @param fieldDate the date to be used by the filter function
+     * @param fieldDates the date range to be used by the filter function
      * @returns the function that filters the tasks
      */
-    protected buildFilterFunction(fieldKeyword: string, fieldDate: moment.Moment): FilterFunction {
+    protected buildFilterFunction(fieldKeyword: string, fieldDates: [moment.Moment, moment.Moment]): FilterFunction {
         let dateFilter: DateFilterFunction;
         if (fieldKeyword === 'before') {
-            dateFilter = (date) => (date ? date.isBefore(fieldDate) : this.filterResultIfFieldMissing());
+            dateFilter = (date) => (date ? date.isBefore(fieldDates[0]) : this.filterResultIfFieldMissing());
         } else if (fieldKeyword === 'after') {
-            dateFilter = (date) => (date ? date.isAfter(fieldDate) : this.filterResultIfFieldMissing());
+            dateFilter = (date) => (date ? date.isAfter(fieldDates[1]) : this.filterResultIfFieldMissing());
         } else {
-            dateFilter = (date) => (date ? date.isSame(fieldDate) : this.filterResultIfFieldMissing());
+            dateFilter = (date) =>
+                date
+                    ? date.isSameOrAfter(fieldDates[0]) && date.isSameOrBefore(fieldDates[1])
+                    : this.filterResultIfFieldMissing();
         }
         return this.getFilter(dateFilter);
     }
@@ -97,7 +101,7 @@ export abstract class DateField extends Field {
     }
 
     protected filterRegExp(): RegExp {
-        return new RegExp(`^${this.fieldNameForFilterInstruction()} (before|after|on)? ?(.*)`);
+        return new RegExp(`^${this.fieldNameForFilterInstruction()} (before|after|on|in)? ?(.*)`);
     }
 
     /**
@@ -119,27 +123,41 @@ export abstract class DateField extends Field {
      * @param fieldName - for example, 'due'
      * @param fieldKeyword - one of the keywords like 'before' or 'after'
      * @param filterResultIfFieldMissing - whether the search matches tasks without the requested date value
-     * @param filterDate - the date used in the filter
+     * @param filterDates - the date range used in the filter
      */
     public static buildExplanation(
         fieldName: string,
         fieldKeyword: string,
         filterResultIfFieldMissing: boolean,
-        filterDate: moment.Moment,
+        filterDates: [moment.Moment, moment.Moment],
     ): string {
         let relationship;
+        // Example of formatted date: '2024-01-02 (Tuesday 2nd January 2024)'
+        const dateFormat = 'YYYY-MM-DD (dddd Do MMMM YYYY)';
+        let explanationDates;
         switch (fieldKeyword) {
             case 'before':
+                relationship = fieldKeyword;
+                explanationDates = filterDates[0].format(dateFormat);
+                break;
             case 'after':
                 relationship = fieldKeyword;
+                explanationDates = filterDates[1].format(dateFormat);
                 break;
             default:
-                relationship = 'on';
+                if (filterDates[0].isSame(filterDates[1])) {
+                    relationship = 'on';
+                    explanationDates = filterDates[0].format(dateFormat);
+                } else {
+                    relationship = 'between';
+                    explanationDates = `${filterDates[0].format(dateFormat)} and ${filterDates[1].format(
+                        dateFormat,
+                    )} inclusive`;
+                }
                 break;
         }
-        // Example of formatted date: '2024-01-02 (Tuesday 2nd January 2024)'
-        const actualDate = filterDate.format('YYYY-MM-DD (dddd Do MMMM YYYY)');
-        let result = `${fieldName} date is ${relationship} ${actualDate}`;
+
+        let result = `${fieldName} date is ${relationship} ${explanationDates}`;
         if (filterResultIfFieldMissing) {
             result += ` OR no ${fieldName} date`;
         }


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 0711c610..bc68fe7c 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -23,17 +23,23 @@ describe('Query parsing', () => {
         'done after 2021-12-27',
         'done before 2021-12-27',
         'done date is invalid',
+        'done in 2021-12-27 2021-12-29',
         'done on 2021-12-27',
+        'done this week',
         'done',
         'due after 2021-12-27',
         'due before 2021-12-27',
         'due date is invalid',
+        'due in 2021-12-27 2021-12-29',
         'due on 2021-12-27',
+        'due this week',
         'exclude sub-items',
         'filename includes wibble',
         'happens after 2021-12-27',
         'happens before 2021-12-27',
+        'happens in 2021-12-27 2021-12-29',
         'happens on 2021-12-27',
+        'happens this week',
         'has done date',
         'has due date',
         'has happens date',
@@ -64,11 +70,15 @@ describe('Query parsing', () => {
         'scheduled after 2021-12-27',
         'scheduled before 2021-12-27',
         'scheduled date is invalid',
+        'scheduled in 2021-12-27 2021-12-29',
         'scheduled on 2021-12-27',
+        'scheduled this week',
         'start date is invalid',
         'starts after 2021-12-27',
         'starts before 2021-12-27',
+        'starts in 2021-12-27 2021-12-29',
         'starts on 2021-12-27',
+        'starts this week',
         'status.name includes cancelled',
         'status.type is IN_PROGRESS',
         'tag does not include #sometag',
diff --git a/tests/Query/DateParser.test.ts b/tests/Query/DateParser.test.ts
index 6023106a..b6463cc3 100644
--- a/tests/Query/DateParser.test.ts
+++ b/tests/Query/DateParser.test.ts
@@ -35,6 +35,12 @@ describe('DateParser - single dates', () => {
     it('should recognise an invalid date correctly', () => {
         testParsingeSingleDate('2021-13-17', 'Invalid date');
     });
+
+    it('should return date at midnight', () => {
+        const dateToParse = '2023-07-08';
+        const parsedDate = DateParser.parseDate(dateToParse);
+        expect(parsedDate.format('YYYY-MM-DD HH:mm')).toStrictEqual('2023-07-08 00:00');
+    });
 });
 
 describe('DateParser - date ranges', () => {
@@ -68,4 +74,73 @@ describe('DateParser - date ranges', () => {
     it('should return 2 invalid dates when both dates are invalid', () => {
         testParsingDateRange('2015-99-29 2015-99-29', 'Invalid date', 'Invalid date');
     });
+
+    it('should return date ranges at midnight', () => {
+        const dateRangeToParse = '2023-09-28 2023-10-01';
+        const parsedDateRange = DateParser.parseDateRange(dateRangeToParse);
+        expect(parsedDateRange[0].format('YYYY-MM-DD HH:mm')).toStrictEqual('2023-09-28 00:00');
+        expect(parsedDateRange[1].format('YYYY-MM-DD HH:mm')).toStrictEqual('2023-10-01 00:00');
+    });
+});
+
+describe('DateParser - natural date ranges', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2021, 9, 6)); // 2021-10-06
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it('should return natural date range at midnight (week)', () => {
+        const dateRangeToParse = 'this week';
+        const parsedDateRange = DateParser.parseDateRange(dateRangeToParse);
+        expect(parsedDateRange[0].format('YYYY-MM-DD HH:mm')).toStrictEqual('2021-10-04 00:00');
+        expect(parsedDateRange[1].format('YYYY-MM-DD HH:mm')).toStrictEqual('2021-10-10 00:00');
+    });
+
+    it('should return natural date range (week)', () => {
+        testParsingDateRange('last week', '2021-09-27', '2021-10-03');
+        testParsingDateRange('this week', '2021-10-04', '2021-10-10');
+        testParsingDateRange('next week', '2021-10-11', '2021-10-17');
+    });
+
+    it('should return natural date range (month)', () => {
+        testParsingDateRange('last month', '2021-09-01', '2021-09-30');
+        testParsingDateRange('this month', '2021-10-01', '2021-10-31');
+        testParsingDateRange('next month', '2021-11-01', '2021-11-30');
+    });
+
+    it('should return natural date range (quarter)', () => {
+        testParsingDateRange('last quarter', '2021-07-01', '2021-09-30');
+        testParsingDateRange('this quarter', '2021-10-01', '2021-12-31');
+        testParsingDateRange('next quarter', '2022-01-01', '2022-03-31');
+    });
+
+    it('should return natural date range (year)', () => {
+        testParsingDateRange('last year', '2020-01-01', '2020-12-31');
+        testParsingDateRange('this year', '2021-01-01', '2021-12-31');
+        testParsingDateRange('next year', '2022-01-01', '2022-12-31');
+    });
+});
+
+describe('Date Parser - correct delta for next & last month & quarter (Today is 2021-04-03)', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2021, 3, 3)); // 2021-04-03
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it('should return longer range even if the current range is shorter in days', () => {
+        testParsingDateRange('last month', '2021-03-01', '2021-03-31');
+        testParsingDateRange('next month', '2021-05-01', '2021-05-31');
+        testParsingDateRange('last quarter', '2021-01-01', '2021-03-31');
+        testParsingDateRange('next quarter', '2021-07-01', '2021-09-30');
+        // The latest test case is not representative eg won't fail without the fix
+        // because the length of Q2 in days is same as Q3.
+    });
 });
diff --git a/tests/Query/Filter/DueDateField.test.due_date approval tests.approved.md b/tests/Query/Filter/DueDateField.test.due_date approval tests.approved.md
index b837b7de..e942e84e 100644
--- a/tests/Query/Filter/DueDateField.test.due_date approval tests.approved.md
+++ b/tests/Query/Filter/DueDateField.test.due_date approval tests.approved.md
@@ -1,7 +1,7 @@
 | date / keyword | last week | this week | next week | 2023-02-09 | 2023-02-07 2023-02-11 |
 | ----- | ----- | ----- | ----- | ----- | ----- |
-| before  | due before last week =><br>  due date is before 2023-02-03 (Friday 3rd February 2023)<br> | due before this week =><br>  due date is before 2023-02-05 (Sunday 5th February 2023)<br> | due before next week =><br>  due date is before 2023-02-17 (Friday 17th February 2023)<br> | due before 2023-02-09 =><br>  due date is before 2023-02-09 (Thursday 9th February 2023)<br> | due before 2023-02-07 2023-02-11 =><br>  due date is before 2023-02-07 (Tuesday 7th February 2023)<br> |
-| on  | due on last week =><br>  due date is on 2023-02-03 (Friday 3rd February 2023)<br> | due on this week =><br>  due date is on 2023-02-05 (Sunday 5th February 2023)<br> | due on next week =><br>  due date is on 2023-02-17 (Friday 17th February 2023)<br> | due on 2023-02-09 =><br>  due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due on 2023-02-07 2023-02-11 =><br>  due date is on 2023-02-07 (Tuesday 7th February 2023)<br> |
-| after  | due after last week =><br>  due date is after 2023-02-03 (Friday 3rd February 2023)<br> | due after this week =><br>  due date is after 2023-02-05 (Sunday 5th February 2023)<br> | due after next week =><br>  due date is after 2023-02-17 (Friday 17th February 2023)<br> | due after 2023-02-09 =><br>  due date is after 2023-02-09 (Thursday 9th February 2023)<br> | due after 2023-02-07 2023-02-11 =><br>  due date is after 2023-02-07 (Tuesday 7th February 2023)<br> |
-| in  | due in last week =><br>  due date is on 2023-02-03 (Friday 3rd February 2023)<br> | due in this week =><br>  due date is on 2023-02-05 (Sunday 5th February 2023)<br> | due in next week =><br>  due date is on 2023-02-17 (Friday 17th February 2023)<br> | due in 2023-02-09 =><br>  due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due in 2023-02-07 2023-02-11 =><br>  due date is on 2023-02-07 (Tuesday 7th February 2023)<br> |
-|  | due last week =><br>  due date is on 2023-02-03 (Friday 3rd February 2023)<br> | due this week =><br>  due date is on 2023-02-05 (Sunday 5th February 2023)<br> | due next week =><br>  due date is on 2023-02-17 (Friday 17th February 2023)<br> | due 2023-02-09 =><br>  due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due 2023-02-07 2023-02-11 =><br>  due date is on 2023-02-07 (Tuesday 7th February 2023)<br> |
+| before  | due before last week =><br>  due date is before 2023-01-30 (Monday 30th January 2023)<br> | due before this week =><br>  due date is before 2023-02-06 (Monday 6th February 2023)<br> | due before next week =><br>  due date is before 2023-02-13 (Monday 13th February 2023)<br> | due before 2023-02-09 =><br>  due date is before 2023-02-09 (Thursday 9th February 2023)<br> | due before 2023-02-07 2023-02-11 =><br>  due date is before 2023-02-07 (Tuesday 7th February 2023)<br> |
+| on  | due on last week =><br>  due date is between 2023-01-30 (Monday 30th January 2023) and 2023-02-05 (Sunday 5th February 2023) inclusive<br> | due on this week =><br>  due date is between 2023-02-06 (Monday 6th February 2023) and 2023-02-12 (Sunday 12th February 2023) inclusive<br> | due on next week =><br>  due date is between 2023-02-13 (Monday 13th February 2023) and 2023-02-19 (Sunday 19th February 2023) inclusive<br> | due on 2023-02-09 =><br>  due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due on 2023-02-07 2023-02-11 =><br>  due date is between 2023-02-07 (Tuesday 7th February 2023) and 2023-02-11 (Saturday 11th February 2023) inclusive<br> |
+| after  | due after last week =><br>  due date is after 2023-02-05 (Sunday 5th February 2023)<br> | due after this week =><br>  due date is after 2023-02-12 (Sunday 12th February 2023)<br> | due after next week =><br>  due date is after 2023-02-19 (Sunday 19th February 2023)<br> | due after 2023-02-09 =><br>  due date is after 2023-02-09 (Thursday 9th February 2023)<br> | due after 2023-02-07 2023-02-11 =><br>  due date is after 2023-02-11 (Saturday 11th February 2023)<br> |
+| in  | due in last week =><br>  due date is between 2023-01-30 (Monday 30th January 2023) and 2023-02-05 (Sunday 5th February 2023) inclusive<br> | due in this week =><br>  due date is between 2023-02-06 (Monday 6th February 2023) and 2023-02-12 (Sunday 12th February 2023) inclusive<br> | due in next week =><br>  due date is between 2023-02-13 (Monday 13th February 2023) and 2023-02-19 (Sunday 19th February 2023) inclusive<br> | due in 2023-02-09 =><br>  due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due in 2023-02-07 2023-02-11 =><br>  due date is between 2023-02-07 (Tuesday 7th February 2023) and 2023-02-11 (Saturday 11th February 2023) inclusive<br> |
+|  | due last week =><br>  due date is between 2023-01-30 (Monday 30th January 2023) and 2023-02-05 (Sunday 5th February 2023) inclusive<br> | due this week =><br>  due date is between 2023-02-06 (Monday 6th February 2023) and 2023-02-12 (Sunday 12th February 2023) inclusive<br> | due next week =><br>  due date is between 2023-02-13 (Monday 13th February 2023) and 2023-02-19 (Sunday 19th February 2023) inclusive<br> | due 2023-02-09 =><br>  due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due 2023-02-07 2023-02-11 =><br>  due date is between 2023-02-07 (Tuesday 7th February 2023) and 2023-02-11 (Saturday 11th February 2023) inclusive<br> |
diff --git a/tests/Query/Filter/DueDateField.test.ts b/tests/Query/Filter/DueDateField.test.ts
index 0e608b38..ed24856e 100644
--- a/tests/Query/Filter/DueDateField.test.ts
+++ b/tests/Query/Filter/DueDateField.test.ts
@@ -22,6 +22,10 @@ function testTaskFilterForTaskWithDueDate(filter: FilterOrErrorMessage, dueDate:
 }
 
 describe('due date', () => {
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
     it('by due date (before)', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due before 2022-04-20');
@@ -33,7 +37,77 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
-    it('due date is invalid', () => {
+    it('by due date - before inclusive range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due before 2022-04-20 2022-04-24');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
+    });
+
+    it('by due date - on inclusive range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due on 2022-04-20 2022-04-24');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
+    });
+
+    it('by due date - after inclusive range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due after 2022-04-20 2022-04-24');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', true);
+    });
+
+    it('by due date - in inclusive range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due in 2022-04-20 2022-04-24');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
+    });
+
+    it('by due date - inclusive range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due 2022-04-20 2022-04-24');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
+    });
+
+    it('due in two weeks', () => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2023, 3 - 1, 6));
+
+        const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due in two weeks');
+        expect(filterOrMessage).toHaveExplanation('due date is on 2023-03-20 (Monday 20th March 2023)');
+    });
+});
+
+describe('due date (error & corner cases)', () => {
+    it('invalid due date', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due date is invalid');
 
@@ -44,6 +118,265 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-00-01', true); // month 0 not valid
         testTaskFilterForTaskWithDueDate(filter, '2022-13-01', true); // month 13 not valid
     });
+
+    it('date range with both dates invalid', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due date 2023-13-01 2022-01-78');
+
+        // Act, Assert
+        expect(filter.filterFunction).toBeUndefined();
+        expect(filter.error).toBeDefined();
+    });
+
+    it('date range with first date invalid treated as a date', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due date 2023-13-01 2022-01-07');
+
+        // Act, Assert
+        // Currently the invalid date is ignored and date range is treated as a date
+        expect(filter).toHaveExplanation('due date is on 2022-01-07 (Friday 7th January 2022)');
+    });
+
+    it('date range with second date invalid treated as a date', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due date 2023-12-01 2022-01-78');
+
+        // Act, Assert
+        // Currently the invalid date is ignored and date range is treated as a date
+        expect(filter).toHaveExplanation('due date is on 2023-12-01 (Friday 1st December 2023)');
+    });
+
+    it('date range with invalid natural date range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due thees week');
+
+        // Act, Assert
+        expect(filter.filterFunction).toBeUndefined();
+        expect(filter.error).toBeDefined();
+    });
+
+    it('before reversed range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due before 2022-04-24 2022-04-20');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
+    });
+
+    it('in reversed range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due 2022-04-24 2022-04-20');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
+    });
+
+    it('after reversed range', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due after 2022-04-24 2022-04-20');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-19', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-20', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-24', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-04-25', true);
+    });
+});
+
+describe('due date before natural date range (Today is 2022-05-25)', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2022, 4, 25)); // 2022-05-25
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it.each([
+        // Week
+        ['last week', '2022-05-15', '2022-05-16', '2022-05-22', '2022-05-23'],
+        ['this week', '2022-05-22', '2022-05-23', '2022-05-29', '2022-05-30'],
+        ['next week', '2022-05-29', '2022-05-30', '2022-06-05', '2022-06-06'],
+
+        // Month
+        ['last month', '2022-03-31', '2022-04-01', '2022-04-30', '2022-05-01'],
+        ['this month', '2022-04-30', '2022-05-01', '2022-05-31', '2022-06-01'],
+        ['next month', '2022-05-31', '2022-06-01', '2022-06-30', '2022-07-01'],
+
+        // Quarter
+        ['last quarter', '2021-12-31', '2022-01-01', '2022-03-31', '2022-04-01'],
+        ['this quarter', '2022-03-31', '2022-04-01', '2022-06-30', '2022-07-01'],
+        ['next quarter', '2022-06-30', '2022-07-01', '2022-09-30', '2022-10-01'],
+
+        // Year
+        ['last year', '2020-12-31', '2021-01-01', '2021-12-31', '2022-01-01'],
+        ['this year', '2021-12-31', '2022-01-01', '2022-12-31', '2023-01-01'],
+        ['next year', '2022-12-31', '2023-01-01', '2023-12-31', '2024-01-01'],
+    ])(
+        'due before %s: task with due date on %s is included; %s, %s, %s are not',
+        (range: string, beforeRange: string, rangeStart: string, rangeEnd: string, afterRange: string) => {
+            // Arrange
+            const filter = new DueDateField().createFilterOrErrorMessage(`due before ${range}`);
+
+            // Act, Assert
+            testTaskFilterForTaskWithDueDate(filter, null, false);
+            testTaskFilterForTaskWithDueDate(filter, beforeRange, true);
+            testTaskFilterForTaskWithDueDate(filter, rangeStart, false);
+            testTaskFilterForTaskWithDueDate(filter, rangeEnd, false);
+            testTaskFilterForTaskWithDueDate(filter, afterRange, false);
+        },
+    );
+
+    it.each([
+        ['week', '2022-05-23 (Monday 23rd May 2022)'],
+        ['month', '2022-05-01 (Sunday 1st May 2022)'],
+        ['quarter', '2022-04-01 (Friday 1st April 2022)'],
+        ['year', '2022-01-01 (Saturday 1st January 2022)'],
+    ])('should explain before natural date range (%s)', (range: string, date: string) => {
+        const filter = new DueDateField().createFilterOrErrorMessage(`due before this ${range}`);
+        expect(filter).toHaveExplanation(`due date is before ${date}`);
+    });
+});
+
+describe('due date in natural date range (Today is 2023-02-28)', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2023, 1, 28)); // 2023-02-28
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it.each([
+        // Week
+        ['last week', '2023-02-19', '2023-02-20', '2023-02-26', '2021-02-27'],
+        ['this week', '2023-02-26', '2023-02-27', '2023-03-05', '2021-03-06'],
+        ['next week', '2023-03-05', '2023-03-06', '2023-03-12', '2021-03-13'],
+
+        // Month
+        ['last month', '2022-12-31', '2023-01-01', '2023-01-31', '2023-02-01'],
+        ['this month', '2023-01-31', '2023-02-01', '2023-02-28', '2023-03-01'],
+        ['next month', '2023-02-28', '2023-03-01', '2023-03-31', '2023-04-01'],
+
+        // Quarter
+        ['last quarter', '2022-09-30', '2022-10-01', '2022-12-31', '2023-01-01'],
+        ['this quarter', '2022-12-31', '2023-01-01', '2023-03-31', '2023-04-01'],
+        ['next quarter', '2023-03-31', '2023-04-01', '2023-06-30', '2023-07-01'],
+
+        // Year
+        ['last year', '2021-12-31', '2022-01-01', '2022-12-31', '2023-01-01'],
+        ['this year', '2022-12-31', '2023-01-01', '2023-12-31', '2024-01-01'],
+        ['next year', '2023-12-31', '2024-01-01', '2024-12-31', '2025-01-01'],
+    ])(
+        'due (in|on|) %s: task with due date %s not included, %s and %s incuded, %s not included again',
+        (range: string, beforeRange: string, rangeStart: string, rangeEnd: string, afterRange: string) => {
+            // Arrange
+            const filterOn = new DueDateField().createFilterOrErrorMessage(`due on ${range}`);
+            const filterIn = new DueDateField().createFilterOrErrorMessage(`due in ${range}`);
+            const filterEmpty = new DueDateField().createFilterOrErrorMessage(`due ${range}`);
+
+            // Act, Assert
+            testTaskFilterForTaskWithDueDate(filterOn, null, false);
+            testTaskFilterForTaskWithDueDate(filterOn, beforeRange, false);
+            testTaskFilterForTaskWithDueDate(filterOn, rangeStart, true);
+            testTaskFilterForTaskWithDueDate(filterOn, rangeEnd, true);
+            testTaskFilterForTaskWithDueDate(filterOn, afterRange, false);
+
+            testTaskFilterForTaskWithDueDate(filterIn, null, false);
+            testTaskFilterForTaskWithDueDate(filterIn, beforeRange, false);
+            testTaskFilterForTaskWithDueDate(filterIn, rangeStart, true);
+            testTaskFilterForTaskWithDueDate(filterIn, rangeEnd, true);
+            testTaskFilterForTaskWithDueDate(filterIn, afterRange, false);
+
+            testTaskFilterForTaskWithDueDate(filterEmpty, null, false);
+            testTaskFilterForTaskWithDueDate(filterEmpty, beforeRange, false);
+            testTaskFilterForTaskWithDueDate(filterEmpty, rangeStart, true);
+            testTaskFilterForTaskWithDueDate(filterEmpty, rangeEnd, true);
+            testTaskFilterForTaskWithDueDate(filterEmpty, afterRange, false);
+        },
+    );
+
+    it.each([
+        ['week', '2023-02-27 (Monday 27th February 2023)', '2023-03-05 (Sunday 5th March 2023)'],
+        ['month', '2023-02-01 (Wednesday 1st February 2023)', '2023-02-28 (Tuesday 28th February 2023)'],
+        ['quarter', '2023-01-01 (Sunday 1st January 2023)', '2023-03-31 (Friday 31st March 2023)'],
+        ['year', '2023-01-01 (Sunday 1st January 2023)', '2023-12-31 (Sunday 31st December 2023)'],
+    ])('should explain in a natural date range (%s)', (range: string, dateStart: string, dateEnd: string) => {
+        const filterOn = new DueDateField().createFilterOrErrorMessage(`due on this ${range}`);
+        const filterIn = new DueDateField().createFilterOrErrorMessage(`due in this ${range}`);
+        const filterEmpty = new DueDateField().createFilterOrErrorMessage(`due this ${range}`);
+
+        expect(filterOn).toHaveExplanation(`due date is between ${dateStart} and ${dateEnd} inclusive`);
+        expect(filterIn).toHaveExplanation(`due date is between ${dateStart} and ${dateEnd} inclusive`);
+        expect(filterEmpty).toHaveExplanation(`due date is between ${dateStart} and ${dateEnd} inclusive`);
+    });
+});
+
+describe('due date after natural date range (Today is 2021-11-01)', () => {
+    beforeAll(() => {
+        jest.useFakeTimers();
+        jest.setSystemTime(new Date(2021, 10, 1)); // 2021-11-01
+    });
+
+    afterAll(() => {
+        jest.useRealTimers();
+    });
+
+    it.each([
+        // Week
+        ['last week', '2021-10-24', '2021-10-25', '2021-10-31', '2021-11-01'],
+        ['this week', '2021-10-31', '2021-11-01', '2021-11-07', '2021-11-08'],
+        ['next week', '2021-11-07', '2021-11-08', '2021-11-14', '2021-11-15'],
+
+        // Month
+        ['last month', '2021-09-30', '2021-10-01', '2021-10-30', '2021-11-01'],
+        ['this month', '2021-10-31', '2021-11-01', '2021-11-30', '2021-12-01'],
+        ['next month', '2021-11-30', '2021-12-01', '2021-12-31', '2022-01-01'],
+
+        // Quarter
+        ['last quarter', '2021-06-30', '2021-07-01', '2021-09-30', '2021-10-01'],
+        ['this quarter', '2021-09-30', '2021-10-01', '2021-12-31', '2022-01-01'],
+        ['next quarter', '2021-12-31', '2022-01-01', '2022-01-31', '2022-04-01'],
+
+        // Year
+        ['last year', '2019-12-31', '2020-01-01', '2020-12-31', '2021-01-01'],
+        ['this year', '2020-12-31', '2021-01-01', '2021-12-31', '2022-01-01'],
+        ['next year', '2021-12-31', '2022-01-01', '2022-12-31', '2023-01-01'],
+    ])(
+        'due after %s: task with due date %s, %s, %s are not incuded, %s is',
+        (range: string, beforeRange: string, rangeStart: string, rangeEnd: string, afterRange: string) => {
+            // Arrange
+            const filter = new DueDateField().createFilterOrErrorMessage(`due after ${range}`);
+
+            // Act, Assert
+            testTaskFilterForTaskWithDueDate(filter, null, false);
+            testTaskFilterForTaskWithDueDate(filter, beforeRange, false);
+            testTaskFilterForTaskWithDueDate(filter, rangeStart, false);
+            testTaskFilterForTaskWithDueDate(filter, rangeEnd, false);
+            testTaskFilterForTaskWithDueDate(filter, afterRange, true);
+        },
+    );
+
+    it.each([
+        ['week', '2021-11-07 (Sunday 7th November 2021)'],
+        ['month', '2021-11-30 (Tuesday 30th November 2021)'],
+        ['quarter', '2021-12-31 (Friday 31st December 2021)'],
+        ['year', '2021-12-31 (Friday 31st December 2021)'],
+    ])('should explain after natural date range (%s)', (range: string, date: string) => {
+        const filter = new DueDateField().createFilterOrErrorMessage(`due after this ${range}`);
+        expect(filter).toHaveExplanation(`due date is after ${date}`);
+    });
 });
 
 describe('explain due date queries', () => {
@@ -56,6 +389,13 @@ describe('explain due date queries', () => {
         const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due 2023-01-02');
         expect(filterOrMessage).toHaveExplanation('due date is on 2023-01-02 (Monday 2nd January 2023)');
     });
+
+    it('should explain date range', () => {
+        const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due 2022-11-25 2023-01-17');
+        expect(filterOrMessage).toHaveExplanation(
+            'due date is between 2022-11-25 (Friday 25th November 2022) and 2023-01-17 (Tuesday 17th January 2023) inclusive',
+        );
+    });
 });
 
 describe('sorting by due', () => {