Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2186 (2023-08-01 14:27:13)

feat: New date options: 'on or before', 'on or after',  'in or before' & 'in or after'
<!--- Provide a general summary of your changes in the Title above -->

# Description

Add `in|on or before|after` keywords for date filters.

## Motivation and Context

It is clunky to write and read `(due next mon) OR (due after next mon)`. `due in or after next mon` is shorter to write and easily readable.

See #124 for more details.

## How has this been tested?

New unit tests added.

## Types of changes


Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)

## 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

- [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)


claremacrae: I've done some manual testing of this, exploring the old and new behaviour...

Old:

```
due in or before this week =>
  due date is between:
    2023-07-31 (Monday 31st July 2023) and
    2023-08-06 (Sunday 6th August 2023) inclusive
```

New:

```
due in or before this week =>
  due date is on or before 2023-08-06 (Sunday 6th August 2023)
```

---

I would not have predicted this behaviour - it's a bigger change than I was expecting...

**I think it is the correct behaviour**. It just wasn't immediately obvious to me what the changed behaviour would be.

And I would not have been able to deduce that this was the intended behaviour from reading the docs, the code or the tests.

Now I've tested the behaviour, I can see it in `buildFilterFunction()` - but before I saw this behaviour by manual testing, I just thought that I was now finding `buildFilterFunction()` to be a bit cryptic and confusing...

I'll update my draft comments in the code review as a result of this, but I think my main takeaway is that this is actually a significant change in behaviour for filters of the kind shown above...

And so my current inclination is that this change may warrant another table of the sort seen in the docs here:
https://publish.obsidian.md/tasks/Queries/Filters#Appendix+Tasks+2.0.0+improvements+to+date+filters

---

Another example:

```
due on or before today =>
  due date is on 2023-08-01 (Tuesday 1st August 2023)
```

Becomes

```
due on or before today =>
  due date is on or before 2023-08-01 (Tuesday 1st August 2023)
```

ilandikov: > You probably know this, but it currently has a conflict with the main branch due to your earlier PR.

Yup, I rebased and merged now =) 

> Apologies in advance if my comments seem a little harsh. But this is such foundational code in Tasks that it is really important that the code and tests are very clear, so that when the next date search feature is added, the behaviour added here is super easy to understand.

Totally agree, your comment are logical and not harsh for me =)

> However, I think the unit tests have evolved to the level of complexity where they don't seem to actually explain the intent of the values used...
> 
> I've tried to make suggestions, but if they unclear, it may be easier to talk about this next time we pair.

Yes, it is hard to understand them now. I think it will be a good use of our time to improve that next time. I tried to solve most of the comments, hope it is fine for you.

In general I find that your wordings are much better and clearer so if you prefer changing some comments, docs, etc, pls don't hesitate to do so and push directly to this branch (If I make changes I push them right away usually so there won't be any conflicts). I will agree with your wordings in any case and it will save us time.

Patch:
diff --git a/docs/Introduction.md b/docs/Introduction.md
index 66b314f7..12968531 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -6,6 +6,8 @@ publish: true
 
 ## What's New?
 
+- X.Y.Z: 🔥 Add `on or before` and `on or after` to [[Filters#Date search options|date search options]]
+- X.Y.Z: 🔥 Add `in or before` and `in or after` to [[Filters#Date range options|date range search search options]]
 - 4.5.0: 🔥 Support task in list items starting with [[Getting Started#Finding tasks in your vault|`+` signs]]
 - 4.4.0: 🔥 Support [[Expressions#More complex expressions|variables, if statements, and functions]] in custom filters and groups
 - 4.3.0: 🔥 Bug fixes, usability improvements and `explain` support for [[Regular Expressions|regular expression]] searches
diff --git a/docs/Queries/Filters.md b/docs/Queries/Filters.md
index 60cc9105..349d8bf3 100644
--- a/docs/Queries/Filters.md
+++ b/docs/Queries/Filters.md
@@ -37,7 +37,52 @@ You can find out more about this very powerful facility in [[Custom Filters]].
 
 Tasks allows a lot of flexibility in the dates inside query blocks.
 
-### Absolute dates
+There are basically two broad types of date search:
+
+- [[#Searching particular dates]]
+- [[#Searching date ranges]]
+
+### Searching particular dates
+
+This section describes searches that use single dates, for example:
+
+    ```tasks
+    starts before 2023-04-20
+    due on or before today
+    ```
+
+See also [[#Searching date ranges]].
+
+#### Date search options
+
+There are several options available when searching with a particular date:
+
+- `on <date>` or `<date>`
+  - will match the date.
+  - `on` is the default for date searches and may be omitted.
+- `before <date>`
+  - will match all dates before the date.
+- `after <date>`
+  - will match all dates after the date.
+- `on or before <date>`
+  - will match the date and all earlier dates.
+- `on or after <date>`
+  - will match the date and all later dates.
+
+This table may help visualise these options:
+
+| option         | all earlier dates | `search date` | all later dates |
+|----------------| ----------------- | ------------ | --------------- |
+| `before`       | matches           |              |                 |
+| `on or before` | matches           | matches      |                 |
+| `on`           |                   | matches      |                 |
+| `on or after`  |                   | matches      | matches         |
+| `after`        |                   |              | matches         |
+
+> [!released]
+> `on or before` and `on or after` were introduced in Tasks X.Y.Z.
+
+#### Absolute dates
 
 `<date>` filters can be given with 'absolute' dates, whose preferred format is `YYYY-MM-DD`.
 
@@ -50,7 +95,7 @@ Examples:
   - The [chrono](https://github.com/wanasit/chrono) library reads dates very flexibly, so you can use free text for absolute dates in your filters.
   - The `YYYY-MM-DD` format is somewhat safer, though, as there is no chance of ambiguity in reading your text.
 
-### Relative dates
+#### Relative dates
 
 `<date>` filters can be given with `relative` dates.
 
@@ -80,16 +125,47 @@ Date range searches were introduced in Tasks 2.0.0.
 
 Tasks allows date searches to specify a pair of dates, `<date range>`.
 
-These searches are inclusive: the dates at either end are found by the search.
+This section describes date range searches, for example:
 
-#### Absolute date ranges
+    ```tasks
+    due 2023-11-25 2023-11-30
+    happens this week
+    ```
 
-`<date range>` may be specified as 2 valid dates in `YYYY-MM-DD` format.
+See also [[#Searching particular dates]].
 
-Dates on either end are included, that is, it is an inclusive search.
+#### Date range options
+
+There are several options available when searching with date ranges:
+
+- `in <date range>` or `<date range>`
+  - will match the **start** date, the **end** date and all dates in between.
+  - `in` is the default for date range searches and may be omitted.
+- `before <date range>`
+  - will match all dates before the **start** date.
+- `after <date range>`
+  - will match all dates after the **end** date.
+- `in or before <date range>`
+  - will match the **end** date and all earlier dates.
+- `in or after <date range>`
+  - will match the **start** date and all later dates.
 
-- `before <date range>` will match before the earliest date of the range.
-- `after <date range>` will match after the latest date of the range.
+This table may help visualise these options:
+
+| option         | all earlier dates | `start date` | all dates<br>inside the range | `end date` | all later dates |
+| -------------- | ----------------- | ------------ | -------------------------------- | ---------- | --------------- |
+| `before`       | matches           |              |                                  |            |                 |
+| `in or before` | matches           | matches      | matches                          | matches    |                 |
+| `in`           |                   | matches      | matches                          | matches    |                 |
+| `in or after`  |                   | matches      | matches                          | matches    | matches         |
+| `after`        |                   |              |                                  |            | matches         |
+
+> [!released]
+> `in or before` and `in or after` were introduced in Tasks X.Y.Z.
+
+#### Absolute date ranges
+
+`<date range>` may be specified as 2 valid dates in `YYYY-MM-DD` format.
 
 Notes:
 
@@ -129,7 +205,7 @@ Example relative date ranges:
 - `in this week` (from this week's Monday to Sunday inclusive)
 - `after this month`
 - `next quarter`
-- `before next year`
+- `on or before next year`
 
 > [!warning]
 Prior to Tasks 2.0.0, the interpretation of relative date ranges was confusing, and not what most users naturally expected.
@@ -333,8 +409,8 @@ Find any tasks that have status symbols you have not yet added to your Tasks set
 
 - `no due date`
 - `has due date`
-- `due (before|after|on) <date>`
-- `due (before|after|in) <date range>`
+- `due (on|before|after|on or before|on or after) <date>`
+- `due (in|before|after|in or before|in or after) <date range>`
   - `YYYY-MM-DD YYYY-MM-DD`
   - `(last|this|next) (week|month|quarter|year)`
   - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
@@ -348,6 +424,7 @@ For more information, see [[Dates#Due date|Due date]].
 > - `due date is invalid` was introduced in Tasks 1.16.0.
 > - `due (before|after|in) <date range>` searches were introduced in Tasks 2.0.0.
 > - `due (before|after|in) (YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)` searches were introduced in Tasks 3.1.0.
+> - `due (on or before|on or after) <date>` and `due (in or before|in or after) <date range>` searches were introduced in Tasks X.Y.Z
 
 Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by due date** is now possible, using `task.due`.
 
@@ -385,8 +462,8 @@ For users who are comfortable with JavaScript, these more complicated examples m
 
 - `no done date`
 - `has done date`
-- `done (before|after|on) <date>`
-- `done (before|after|in) <date range>`
+- `done (on|before|after|on or before|on or after) <date>`
+- `done (in|before|after|in or before|in or after) <date range>`
   - `YYYY-MM-DD YYYY-MM-DD`
   - `(last|this|next) (week|month|quarter|year)`
   - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
@@ -400,6 +477,7 @@ For more information, see [[Dates#Done date|Done date]].
 > - `done date is invalid` was introduced in Tasks 1.16.0.
 > - `done (before|after|in) <date range>` searches were introduced in Tasks 2.0.0.
 > - `done (before|after|in) (YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)` searches were introduced in Tasks 3.1.0.
+> - `done (on or before|on or after) <date>` and `done (in or before|in or after) <date range>` searches were introduced in Tasks X.Y.Z
 
 Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by done date** is now possible, using `task.done`.
 
@@ -417,8 +495,8 @@ For more examples, see [[#Due Date]].
 
 - `no scheduled date`
 - `has scheduled date`
-- `scheduled (before|after|on) <date>`
-- `scheduled (before|after|in) <date range>`
+- `scheduled (on|before|after|on or before|on or after) <date>`
+- `scheduled (in|before|after|in or before|in or after) <date range>`
   - `YYYY-MM-DD YYYY-MM-DD`
   - `(last|this|next) (week|month|quarter|year)`
   - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
@@ -432,6 +510,7 @@ For more information, see [[Dates#Scheduled date|Scheduled date]].
 > - `scheduled date is invalid` was introduced in Tasks 1.16.0.
 > - `scheduled (before|after|in) <date range>` searches were introduced in Tasks 2.0.0.
 > - `scheduled (before|after|in) (YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)` searches were introduced in Tasks 3.1.0.
+> - `scheduled (on or before|on or after) <date>` and `scheduled (in or before|in or after) <date range>` searches were introduced in Tasks X.Y.Z
 
 Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by scheduled date** is now possible, using `task.scheduled`.
 
@@ -449,8 +528,8 @@ For more examples, see [[#Due Date]].
 
 - `no start date`
 - `has start date`
-- `starts (before|after|on) <date>`
-- `starts (before|after|in) <date range>`
+- `starts (on|before|after|on or before|on or after) <date>`
+- `starts (in|before|after|in or before|in or after) <date range>`
   - `YYYY-MM-DD YYYY-MM-DD`
   - `(last|this|next) (week|month|quarter|year)`
   - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
@@ -464,6 +543,7 @@ For more information, see [[Dates#Start date|Start date]].
 > - `start date is invalid` was introduced in Tasks 1.16.0.
 > - `starts (before|after|in) <date range>` searches were introduced in Tasks 2.0.0.
 > - `starts (before|after|in) (YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)` searches were introduced in Tasks 3.1.0.
+> - `starts (on or before|on or after) <date>` and `starts (in or before|in or after) <date range>` searches were introduced in Tasks X.Y.Z
 
 Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by start date** is now possible, using `task.start`.
 
@@ -509,8 +589,8 @@ See [[Dates#Created date|created date]] for how to make Tasks record the created
 
 - `no created date`
 - `has created date`
-- `created (before|after|on) <date>`
-- `created (before|after|in) <date range>`
+- `created (on|before|after|on or before|on or after) <date>`
+- `created (in|before|after|in or before|in or after) <date range>`
   - `YYYY-MM-DD YYYY-MM-DD`
   - `(last|this|next) (week|month|quarter|year)`
   - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
@@ -526,6 +606,7 @@ Such a filter could be:
 >
 > - Created date was introduced in Tasks 2.0.0.
 > - `created (before|after|in) (YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)` searches were introduced in Tasks 3.1.0.
+> - `created (on or before|on or after) <date>` and `created (in or before|in or after) <date range>` searches were introduced in Tasks X.Y.Z
 
 Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by created date** is now possible, using `task.created`.
 
@@ -541,8 +622,8 @@ For more examples, see [[#Due Date]].
 
 ### Happens
 
-- `happens (before|after|on) <date>`
-- `happens (before|after|in) <date range>`
+- `happens (on|before|after|on or before|on or after) <date>`
+- `happens (in|before|after|in or before|in or after) <date range>`
   - `YYYY-MM-DD YYYY-MM-DD`
   - `(last|this|next) (week|month|quarter|year)`
   - `(YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)`
@@ -562,6 +643,7 @@ because the tasks starts before tomorrow. Only one of the dates needs to match.
 > - `no happens date` and `has happens date` were introduced in Tasks 1.7.0.
 > - `happens (before|after|in) <date range>` searches were introduced in Tasks 2.0.0.
 > - `happens (before|after|in) (YYYY-Www|YYYY-mm|YYYY-Qq|YYYY)` searches were introduced in Tasks 3.1.0.
+> - `happens (on or before|on or after) <date>` and `happens (in or before|in or after) <date range>` searches were introduced in Tasks X.Y.Z
 
 Since Tasks 4.2.0, **[[Custom Filters|custom filtering]] by happens date** is now possible, using `task.happens`.
 
diff --git a/docs/Quick Reference.md b/docs/Quick Reference.md
index 8bd44d2f..c6c19c58 100644
--- a/docs/Quick Reference.md
+++ b/docs/Quick Reference.md
@@ -17,12 +17,12 @@ This table summarizes the filters and other options available inside a `tasks` b
 |                                                                                                                                                                                                                                                                                                 |                                             |                        |                        | `task.status.symbol`                                |
 |                                                                                                                                                                                                                                                                                                 |                                             |                        |                        | `task.status.nextSymbol`                            |
 | **[[Filters#Filters for Dates in Tasks\|Dates]]**                                                                                                                                                                                                                                               |                                             |                        |                        |                                                     |
-| `done (before, after, on) <date>`<br>`done (before, after, in) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                          | `sort by done`                              | `group by done`        | `hide done date`       | `task.done`                                         |
-| `created (before, after, on) <date>`<br>`created (before, after, in) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has created date`<br>`no  created date`<br>`created date is invalid`           | `sort by created`                           | `group by created`     | `hide created date`    | `task.created`                                      |
-| `starts (before, after, on) <date>`<br>`starts (before, after, in) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                   | `sort by start`                             | `group by start`       | `hide start date`      | `task.start`                                        |
-| `scheduled (before, after, on) <date>`<br>`scheduled (before, after, in) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid` | `sort by scheduled`                         | `group by scheduled`   | `hide scheduled date`  | `task.scheduled`                                    |
-| `due (before, after, on) <date>`<br>`due (before, after, in) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                               | `sort by due`                               | `group by due`         | `hide due date`        | `task.due`                                          |
-| `happens (before, after, on) <date>`<br>`happens (before, after, in) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has happens date`<br>`no  happens date`                                        | `sort by happens`                           | `group by happens`     |                        | `task.happens`                                      |
+| `done (on, before, after, on or before, on or after) <date>`<br>`done (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                          | `sort by done`                              | `group by done`        | `hide done date`       | `task.done`                                         |
+| `created (on, before, after, on or before, on or after) <date>`<br>`created (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has created date`<br>`no  created date`<br>`created date is invalid`           | `sort by created`                           | `group by created`     | `hide created date`    | `task.created`                                      |
+| `starts (on, before, after, on or before, on or after) <date>`<br>`starts (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                   | `sort by start`                             | `group by start`       | `hide start date`      | `task.start`                                        |
+| `scheduled (on, before, after, on or before, on or after) <date>`<br>`scheduled (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid` | `sort by scheduled`                         | `group by scheduled`   | `hide scheduled date`  | `task.scheduled`                                    |
+| `due (on, before, after, on or before, on or after) <date>`<br>`due (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                               | `sort by due`                               | `group by due`         | `hide due date`        | `task.due`                                          |
+| `happens (on, before, after, on or before, on or after) <date>`<br>`happens (in, before, after, in or before, in or after) ...`<br>`... YYYY-MM-DD YYYY-MM-DD`<br>`... (last, this, next) (week, month, quarter, year)`<br>`... (YYYY-Www,YYYY-mm, YYYY-Qq, YYYY)`<br>`has happens date`<br>`no  happens date`                                        | `sort by happens`                           | `group by happens`     |                        | `task.happens`                                      |
 | **[[Filters#Recurrence\|Recurrence]]**                                                                                                                                                                                                                                                          |                                             |                        |                        |                                                     |
 | `is recurring`<br>`is not recurring`                                                                                                                                                                                                                                                            | `sort by recurring`                         | `group by recurring`   |                        | `task.isRecurring`                                  |
 | `recurrence (includes, does not include) <string>`<br>`recurrence (regex matches, regex does not match) /regex/i`                                                                                                                                                                               |                                             | `group by recurrence`  | `hide recurrence rule` | `task.recurrenceRule`                               |
diff --git a/src/Query/Filter/DateField.ts b/src/Query/Filter/DateField.ts
index ad03fb73..3504eee6 100644
--- a/src/Query/Filter/DateField.ts
+++ b/src/Query/Filter/DateField.ts
@@ -58,9 +58,9 @@ export abstract class DateField extends Field {
             );
         }
 
-        const keywordAndDateString = fieldNameKeywordDate[1]; // Will contain the whole line except the field name
-        const fieldKeyword = fieldNameKeywordDate[2]; // Will be 'before', 'after', 'in', 'on' or undefined
-        const fieldDateString = fieldNameKeywordDate[3]; // Will contain the remainder of the instruction
+        const keywordAndDateString = fieldNameKeywordDate[1]; // The whole line except the field name
+        const fieldKeyword = fieldNameKeywordDate[2]; // 'on', 'in', 'before', 'after', 'on|in or before|after' or undefined
+        const fieldDateString = fieldNameKeywordDate[3]; // The remainder of the instruction
 
         // Try interpreting everything after the keyword as a date range:
         let fieldDates = DateParser.parseDateRange(fieldDateString);
@@ -99,15 +99,33 @@ export abstract class DateField extends Field {
      */
     protected buildFilterFunction(fieldKeyword: string, fieldDates: DateRange): FilterFunction {
         let dateFilter: DateFilterFunction;
-        if (fieldKeyword === 'before') {
-            dateFilter = (date) => (date ? date.isBefore(fieldDates.start) : this.filterResultIfFieldMissing());
-        } else if (fieldKeyword === 'after') {
-            dateFilter = (date) => (date ? date.isAfter(fieldDates.end) : this.filterResultIfFieldMissing());
-        } else {
-            dateFilter = (date) =>
-                date
-                    ? date.isSameOrAfter(fieldDates.start) && date.isSameOrBefore(fieldDates.end)
-                    : this.filterResultIfFieldMissing();
+        switch (fieldKeyword) {
+            case 'before':
+                dateFilter = (date) => (date ? date.isBefore(fieldDates.start) : this.filterResultIfFieldMissing());
+                break;
+            case 'after':
+                dateFilter = (date) => (date ? date.isAfter(fieldDates.end) : this.filterResultIfFieldMissing());
+                break;
+            case 'on or before':
+            case 'in or before':
+                // 'on or before'/'in or before' a date range uses the end of the range
+                // as the search limit, so that it matches every date in the
+                // inclusive date range, and all dates before the range.
+                dateFilter = (date) => (date ? date.isSameOrBefore(fieldDates.end) : this.filterResultIfFieldMissing());
+                break;
+            case 'on or after':
+            case 'in or after':
+                // 'on or after'/'in or after' a date range uses the beginning of the range
+                // as the search limit, so that it matches every date in the
+                // inclusive date range, and all dates after the range.
+                dateFilter = (date) =>
+                    date ? date.isSameOrAfter(fieldDates.start) : this.filterResultIfFieldMissing();
+                break;
+            default:
+                dateFilter = (date) =>
+                    date
+                        ? date.isSameOrAfter(fieldDates.start) && date.isSameOrBefore(fieldDates.end)
+                        : this.filterResultIfFieldMissing();
         }
         return this.getFilter(dateFilter);
     }
@@ -119,7 +137,9 @@ export abstract class DateField extends Field {
     }
 
     protected filterRegExp(): RegExp {
-        return new RegExp(`^${this.fieldNameForFilterInstruction()} ((before|after|on|in)? ?(.*))`);
+        return new RegExp(
+            `^${this.fieldNameForFilterInstruction()} (((?:on|in) or before|before|(?:on|in) or after|after|on|in)? ?(.*))`,
+        );
     }
 
     /**
@@ -149,19 +169,33 @@ export abstract class DateField extends Field {
         filterResultIfFieldMissing: boolean,
         filterDates: DateRange,
     ): Explanation {
-        let relationship;
+        let relationship = fieldKeyword;
         // 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;
+            case 'on or after':
+                // 'before <date range>' and 'on or after <date range>' reference the Start of the range:
+                //  - 'before this week' is before the Monday
+                //  - 'on or after this week' is starting from Monday inclusive.
                 explanationDates = filterDates.start.format(dateFormat);
                 break;
             case 'after':
-                relationship = fieldKeyword;
+            case 'on or before':
+                // 'after <date range>' and 'on or before <date range>' reference the End of the range:
+                //  - 'after this month' is after the last day of this month
+                //  - 'on or before this month' is before the last day of this month inclusive.
                 explanationDates = filterDates.end.format(dateFormat);
                 break;
+            case 'in or before':
+                relationship = 'on or before';
+                explanationDates = filterDates.end.format(dateFormat);
+                break;
+            case 'in or after':
+                relationship = 'on or after';
+                explanationDates = filterDates.start.format(dateFormat);
+                break;
             default:
                 if (!filterDates.start.isSame(filterDates.end)) {
                     // This is a special case where a multi-line explanation has to be built


Test patch:
diff --git a/tests/Query/Filter/DueDateField.test.ts b/tests/Query/Filter/DueDateField.test.ts
index 3b75705c..553900ab 100644
--- a/tests/Query/Filter/DueDateField.test.ts
+++ b/tests/Query/Filter/DueDateField.test.ts
@@ -38,6 +38,28 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
+    it('by due date (on or before) should match the given date and all earlier dates', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due on or before 2023-08-01');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2023-07-31', true);
+        testTaskFilterForTaskWithDueDate(filter, '2023-08-01', true);
+        testTaskFilterForTaskWithDueDate(filter, '2023-08-02', false);
+    });
+
+    it('by due date (on or after) should match the given date and all later dates', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due on or after 2022-02-01');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-01-31', false);
+        testTaskFilterForTaskWithDueDate(filter, '2022-02-01', true);
+        testTaskFilterForTaskWithDueDate(filter, '2022-02-02', true);
+    });
+
     it('by due date - before absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due before 2022-04-20 2022-04-24');
@@ -50,6 +72,18 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
+    it('by due date - on or before absolute range should match the date range and all earlier dates', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due on or before 2021-07-10 2021-10-04');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2021-07-09', true);
+        testTaskFilterForTaskWithDueDate(filter, '2021-07-10', true);
+        testTaskFilterForTaskWithDueDate(filter, '2021-10-04', true);
+        testTaskFilterForTaskWithDueDate(filter, '2021-10-05', false);
+    });
+
     it('by due date - on absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due on 2022-04-20 2022-04-24');
@@ -62,6 +96,18 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
+    it('by due date - on or after absolute range should match the date range and all later dates', () => {
+        // Arrange
+        const filter = new DueDateField().createFilterOrErrorMessage('due on or after 2023-03-10 2023-04-01');
+
+        // Act, Assert
+        testTaskFilterForTaskWithDueDate(filter, null, false);
+        testTaskFilterForTaskWithDueDate(filter, '2023-03-09', false);
+        testTaskFilterForTaskWithDueDate(filter, '2023-03-10', true);
+        testTaskFilterForTaskWithDueDate(filter, '2023-04-01', true);
+        testTaskFilterForTaskWithDueDate(filter, '2023-04-02', true);
+    });
+
     it('by due date - after absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due after 2022-04-20 2022-04-24');
@@ -453,6 +499,26 @@ describe('explain due date queries', () => {
   2023-01-17 (Tuesday 17th January 2023) inclusive`,
         );
     });
+
+    it('should explain "on or before" with a single date', () => {
+        const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due on or before 2023-08-08');
+        expect(filterOrMessage).toHaveExplanation('due date is on or before 2023-08-08 (Tuesday 8th August 2023)');
+    });
+
+    it('should explain "on or after" with a single date', () => {
+        const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due on or after 2023-07-29');
+        expect(filterOrMessage).toHaveExplanation('due date is on or after 2023-07-29 (Saturday 29th July 2023)');
+    });
+
+    it('should explain "in or before" with an absolute date range', () => {
+        const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due in or before 2023-10-20 2023-11-01');
+        expect(filterOrMessage).toHaveExplanation('due date is on or before 2023-11-01 (Wednesday 1st November 2023)');
+    });
+
+    it('should explain "in or after" with an absolute date range', () => {
+        const filterOrMessage = new DueDateField().createFilterOrErrorMessage('due in or after 2023-10-20 2023-11-01');
+        expect(filterOrMessage).toHaveExplanation('due date is on or after 2023-10-20 (Friday 20th October 2023)');
+    });
 });
 
 describe('sorting by due', () => {