Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1754 (2023-03-11 09:53:51)

docs: Adopt 'absolute' & 'relative' instead of 'formal & natural language'
<!--- Provide a general summary of your changes in the Title above -->

# Description

I found the original description of types of date in task queries to be a bit confusing:

> natural language or in formal notation (`YYYY-MM-DD`)

I've changed the vocabulary to:

> - absolute (points to a fixed date in the calendar)
> - relative (depends on today's date)

Note that these two types of date are independent of the format.

For example, both `2021-05-25` and `25th May 2023` are absolute dates.

I've also reviewed all the date documentation quite thoroughly.

I converted some H4 headings to H3 because int the current publishing mechanism, the H4 headings just do not stand out at all in the body of the page.

I've also updated the relevant test names for consistency.

## Motivation and Context

Make it easier to write and read the docs.

## How has this been tested?

Viewing in local docker build of docs.

## Screenshots (if appropriate)

![image](https://user-images.githubusercontent.com/4840096/224477624-35cba884-fee1-4317-a749-9e4be91c3859.png)


## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [ ] 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)


claremacrae: @ilandikov I'll merge this later today.

No obligation, but if you had time to look at it and spotted anything wrong, please do say.

Some of the text was because I found out that this works, so chrono is very flexible in interpreting pairs of dates!

````
```tasks
due may june
explain
```
````

Gives:

```
due may june =>
  due date is between 2023-05-01 (Monday 1st May 2023) and 2023-06-01 (Thursday 1st June 2023) inclusive
```


ilandikov: > @ilandikov I'll merge this later today.
> 
> No obligation, but if you had time to look at it and spotted anything wrong, please do say.

Wow, this looks so much better. I have put my suggestions as a review, hope it is fine this way =)

> ```
> ```tasks
> due may june
> explain
> ```
> ```
> 
> Gives:
> 
> ```
> due may june =>
>   due date is between 2023-05-01 (Monday 1st May 2023) and 2023-06-01 (Thursday 1st June 2023) inclusive
> ```

well, this looks good, but isn't it bugged? I would expect `due may june` to be between 05-01 and 06-30 inclusive.... =)

Patch:
diff --git a/docs/queries/filters.md b/docs/queries/filters.md
index 5ea73044..ab2c39f3 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -21,16 +21,30 @@ parent: Queries
 
 ## Searching for dates
 
-### Specific dates
+Tasks allows a lot of flexibility in the dates inside query blocks.
 
-`<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:
+### Absolute dates
 
-Formal notation:
+`<date>` filters can be given with 'absolute' dates, whose preferred format is `YYYY-MM-DD`.
+
+Absolute dates specify a **particular date in a calendar**. They represent the same day, regardless of today's date.
+
+Examples:
 
 - `2021-05-25`
+- `25th May 2023`
+  - 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
+
+`<date>` filters can be given with `relative` dates.
 
-Natural language notation:
+Relative dates are **calculated from today's date**.
+
+When the day changes, relative dates like `due today` are re-evaluated so that the list stays up-to-date (so long as your computer was not hibernating at midnight - see [#1289](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1289)).
+
+Examples for inspiration:
 
 - `yesterday`
 - `today`
@@ -39,54 +53,76 @@ Natural language notation:
 - `last friday`
 - `14 days ago`
 - `in two weeks`
+- `14 October` (the current year will be used)
+- `May` (1st May in the current year will be used)
 
 Note that if it is Wednesday and you write `tuesday`, Tasks assumes you mean "yesterday", as that is the closest Tuesday.
 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.
+### Searching date ranges
 
 {: .released }
 Date range searches were introduced in Tasks 1.26.0.
 
-#### Date ranges in formal notation
+Tasks allows date searches to specify a pair of dates, `<date range>` .
 
-`<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.
+These searches are inclusive: the dates at either end are found by the 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.
+### Absolute date ranges
+
+`<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
+- `in` and `on` may be omitted.
+- 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 relative dates eg `next monday three weeks`.
+- It is technically possible to specify absolute dates in words, such as `25th May 2023`.
+  - However, we do not recommend using words for specifying the two dates in ranges.
+  - This is because we have found that using two adjacent non-numeric dates can lead to ambiguity and unintended results when the [chrono](https://github.com/wanasit/chrono) library parses the dates in your `<date range>` filter.
 
-Example:
+Example absolute date ranges:
 
 - `2022-01-01 2023-02-01`
 
-#### Date ranges in natural language notation
+### Relative date ranges
+
+Tasks supports a very specific set of relative `<date range>` values: `last|this|next week|month|quarter|year`. The pipe (`|`) character means 'or'.
+
+Tasks will process these ranges, based on today's date, and convert them to absolute date ranges (`YYYY-MM-DD YYYY-MM-DD`) internally.
 
-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.
+Dates on either end are included, that is, it is an inclusive search.
 
 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
+- 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.
+  - We are tracking this in [issue #1751](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1751)
+- Relative date ranges support only the exact keywords specified above.
+  - So, for example, `previous half of year` and `next semester` are not supported.
 
-Examples:
+Example relative date ranges:
 
-- `this week` (from this week's Monday to Sunday inclusive)
+- `in 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.
+If your date searches are giving unexpected results, add an [`explain`]({{ site.baseurl }}{% link queries/explaining-queries.md %}) line to your query.
+
+This will help you identify common mistakes such as:
+
+- Accidentally using an invalid absolute date in a filter.
+- Using unsupported keywords in relative date ranges.
+
+If relative dates in queries do not update from the previous day, and your computer was sleeping at midnight, this is likely caused by a known Chrome bug and you will need to re-open the note. See [#1289](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1289).
 
 ### Finding Tasks with Invalid Dates
 
@@ -412,6 +448,8 @@ For more information, see [Priorities]({{ site.baseurl }}{% link getting-started
 {: .released }
 Introduced in Tasks 1.6.0.
 
+- `no tags`
+- `has tags`
 - `tags (include|do not include) <tag>` _or_
 - `tag (includes|does not include) <tag>`
   - Matches case-insensitive (disregards capitalization).
@@ -427,7 +465,8 @@ Introduced in Tasks 1.6.0.
   - If searching for sub-tags, remember to escape the slashes in regular expressions: `\/`
 
 {: .released }
-`regex matches` and `regex does not match` were introduced in Tasks 1.13.0.
+`regex matches` and `regex does not match` were introduced in Tasks 1.13.0.<br>
+`no tags` and `has tags` were introduced in Tasks 1.26.0.
 
 #### Tag Query Examples
 {: .no_toc }
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index 30c1f413..caa66c6d 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -37,7 +37,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `heading (includes, does not include) <string>`<br>`heading (regex matches, regex does not match) /regex/i`                                                                                                                                                       | `sort by heading`                           | `group by heading`     |                        |
 |                                                                                                                                                                                                                                                                   |                                             | `group by backlink`    | `hide backlink`        |
 | `description (includes, does not include) <string>`<br>`description (regex matches, regex does not match) /regex/i`                                                                                                                                               | `sort by description`                       |                        |                        |
-| `tag (includes, does not include) <tag>`<br>`tags (include, do not include) <tag>`<br>`tag (regex matches, regex does not match) /regex/i`<br>`tags (regex matches, regex does not match) /regex/i`                                                               | `sort by tag`<br>`sort by tag <tag_number>` | `group by tags`        |                        |
+| `has tags`<br>`no tags`<br>`tag (includes, does not include) <tag>`<br>`tags (include, do not include) <tag>`<br>`tag (regex matches, regex does not match) /regex/i`<br>`tags (regex matches, regex does not match) /regex/i`                                    | `sort by tag`<br>`sort by tag <tag_number>` | `group by tags`        |                        |
 | [**Combining Filters**][5]                                                                                                                                                                                                                                        |                                             |                        |                        |
 | `(filter 1) AND (filter 2)`                                                                                                                                                                                                                                       |                                             |                        |                        |
 | `(filter 1) OR (filter 2)`                                                                                                                                                                                                                                        |                                             |                        |                        |
diff --git a/src/Query/DateParser.ts b/src/Query/DateParser.ts
index c403de61..14135cc6 100644
--- a/src/Query/DateParser.ts
+++ b/src/Query/DateParser.ts
@@ -38,12 +38,12 @@ export class DateParser {
             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 relativeDateRangeRegexp = /(last|this|next) (week|month|quarter|year)/;
+        const relativeDateRangeMatch = input.match(relativeDateRangeRegexp);
+        if (relativeDateRangeMatch && relativeDateRangeMatch.length === 3) {
+            const lastThisNext = relativeDateRangeMatch[1];
             const delta = moment.duration();
-            const range = naturalDateRangeMatch[2];
+            const range = relativeDateRangeMatch[2];
             switch (range) {
                 case 'month':
                 case 'quarter':
diff --git a/src/Query/Filter/TagsField.ts b/src/Query/Filter/TagsField.ts
index 324b0086..f6ceba44 100644
--- a/src/Query/Filter/TagsField.ts
+++ b/src/Query/Filter/TagsField.ts
@@ -1,6 +1,8 @@
 import type { Task } from '../../Task';
 import type { Comparator } from '../Sorter';
 import { Sorter } from '../Sorter';
+import type { FilterOrErrorMessage } from './Filter';
+import { FilterInstructions } from './FilterInstructions';
 import { MultiTextField } from './MultiTextField';
 
 /**
@@ -12,6 +14,33 @@ export class TagsField extends MultiTextField {
     // -----------------------------------------------------------------------------------------------------------------
     // Filtering
     // -----------------------------------------------------------------------------------------------------------------
+    private readonly filterInstructions: FilterInstructions;
+
+    constructor() {
+        super();
+        this.filterInstructions = new FilterInstructions();
+        this.filterInstructions.add(`has ${this.fieldNameSingular()}`, (task: Task) => this.values(task).length > 0);
+        this.filterInstructions.add(`has ${this.fieldNamePlural()}`, (task: Task) => this.values(task).length > 0);
+        this.filterInstructions.add(`no ${this.fieldNameSingular()}`, (task: Task) => this.values(task).length === 0);
+        this.filterInstructions.add(`no ${this.fieldNamePlural()}`, (task: Task) => this.values(task).length === 0);
+    }
+
+    public createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
+        const filterResult = this.filterInstructions.createFilterOrErrorMessage(line);
+        if (filterResult.filter !== undefined) {
+            return filterResult;
+        }
+
+        return super.createFilterOrErrorMessage(line);
+    }
+
+    public canCreateFilterForLine(line: string): boolean {
+        if (this.filterInstructions.canCreateFilterForLine(line)) {
+            return true;
+        }
+
+        return super.canCreateFilterForLine(line);
+    }
 
     public fieldNameSingular(): string {
         return 'tag';


Test patch:
diff --git a/contributing/Testing/Example Tests.md b/contributing/Testing/Example Tests.md
index 2f4f2c0a..03bdede3 100644
--- a/contributing/Testing/Example Tests.md
+++ b/contributing/Testing/Example Tests.md
@@ -138,7 +138,7 @@ describe('DateParser - single dates', () => {
 });
 
 describe('DateParser - date ranges', () => {
-    it('should parse date range from natural dates', () => {
+    it('should parse date range from absolute dates, supplied as words', () => {
         // Arrange
         testParsingDateRange('17 August 2013 19 August 2013', '2013-08-17', '2013-08-19');
     });
@@ -177,7 +177,7 @@ describe('DateParser - date ranges', () => {
     });
 });
 
-describe('DateParser - natural date ranges', () => {
+describe('DateParser - relative date ranges', () => {
     beforeAll(() => {
         jest.useFakeTimers();
         jest.setSystemTime(new Date(2021, 9, 6)); // 2021-10-06
@@ -187,32 +187,32 @@ describe('DateParser - natural date ranges', () => {
         jest.useRealTimers();
     });
 
-    it('should return natural date range at midnight (week)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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');
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index cd37bfbe..688d2ced 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -52,6 +52,8 @@ describe('Query parsing', () => {
         'has happens date',
         'has scheduled date',
         'has start date',
+        'has tags',
+        'has tag',
         'heading does not include wibble',
         'heading includes AND', // Verify Query doesn't confuse this with a boolean query
         'heading includes wibble',
@@ -63,6 +65,8 @@ describe('Query parsing', () => {
         'no happens date',
         'no scheduled date',
         'no start date',
+        'no tags',
+        'no tag',
         'not done',
         'path does not include some/path',
         'path includes AND', // Verify Query doesn't confuse this with a boolean query
diff --git a/tests/Query/DateParser.test.ts b/tests/Query/DateParser.test.ts
index b6463cc3..d467371b 100644
--- a/tests/Query/DateParser.test.ts
+++ b/tests/Query/DateParser.test.ts
@@ -44,7 +44,7 @@ describe('DateParser - single dates', () => {
 });
 
 describe('DateParser - date ranges', () => {
-    it('should parse date range from natural dates', () => {
+    it('should parse date range from absolute dates, supplied as words', () => {
         // Arrange
         testParsingDateRange('17 August 2013 19 August 2013', '2013-08-17', '2013-08-19');
     });
@@ -83,7 +83,7 @@ describe('DateParser - date ranges', () => {
     });
 });
 
-describe('DateParser - natural date ranges', () => {
+describe('DateParser - relative date ranges', () => {
     beforeAll(() => {
         jest.useFakeTimers();
         jest.setSystemTime(new Date(2021, 9, 6)); // 2021-10-06
@@ -93,32 +93,32 @@ describe('DateParser - natural date ranges', () => {
         jest.useRealTimers();
     });
 
-    it('should return natural date range at midnight (week)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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)', () => {
+    it('should return relative 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');
diff --git a/tests/Query/Filter/DueDateField.test.ts b/tests/Query/Filter/DueDateField.test.ts
index ed24856e..e7524e97 100644
--- a/tests/Query/Filter/DueDateField.test.ts
+++ b/tests/Query/Filter/DueDateField.test.ts
@@ -37,7 +37,7 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
-    it('by due date - before inclusive range', () => {
+    it('by due date - before absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due before 2022-04-20 2022-04-24');
 
@@ -49,7 +49,7 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
-    it('by due date - on inclusive range', () => {
+    it('by due date - on absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due on 2022-04-20 2022-04-24');
 
@@ -61,7 +61,7 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
-    it('by due date - after inclusive range', () => {
+    it('by due date - after absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due after 2022-04-20 2022-04-24');
 
@@ -73,7 +73,7 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', true);
     });
 
-    it('by due date - in inclusive range', () => {
+    it('by due date - in absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due in 2022-04-20 2022-04-24');
 
@@ -85,7 +85,7 @@ describe('due date', () => {
         testTaskFilterForTaskWithDueDate(filter, '2022-04-25', false);
     });
 
-    it('by due date - inclusive range', () => {
+    it('by due date - absolute range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due 2022-04-20 2022-04-24');
 
@@ -146,7 +146,7 @@ describe('due date (error & corner cases)', () => {
         expect(filter).toHaveExplanation('due date is on 2023-12-01 (Friday 1st December 2023)');
     });
 
-    it('date range with invalid natural date range', () => {
+    it('date range with invalid relative date range', () => {
         // Arrange
         const filter = new DueDateField().createFilterOrErrorMessage('due thees week');
 
@@ -192,7 +192,7 @@ describe('due date (error & corner cases)', () => {
     });
 });
 
-describe('due date before natural date range (Today is 2022-05-25)', () => {
+describe('due date before relative date range (Today is 2022-05-25)', () => {
     beforeAll(() => {
         jest.useFakeTimers();
         jest.setSystemTime(new Date(2022, 4, 25)); // 2022-05-25
@@ -242,13 +242,13 @@ describe('due date before natural date range (Today is 2022-05-25)', () => {
         ['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) => {
+    ])('should explain before relative 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)', () => {
+describe('due date in relative date range (Today is 2023-02-28)', () => {
     beforeAll(() => {
         jest.useFakeTimers();
         jest.setSystemTime(new Date(2023, 1, 28)); // 2023-02-28
@@ -279,7 +279,7 @@ describe('due date in natural date range (Today is 2023-02-28)', () => {
         ['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',
+        'due (in|on|) %s: task with due date %s not included, %s and %s included, %s not included again',
         (range: string, beforeRange: string, rangeStart: string, rangeEnd: string, afterRange: string) => {
             // Arrange
             const filterOn = new DueDateField().createFilterOrErrorMessage(`due on ${range}`);
@@ -312,7 +312,7 @@ describe('due date in natural date range (Today is 2023-02-28)', () => {
         ['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) => {
+    ])('should explain in a relative 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}`);
@@ -323,7 +323,7 @@ describe('due date in natural date range (Today is 2023-02-28)', () => {
     });
 });
 
-describe('due date after natural date range (Today is 2021-11-01)', () => {
+describe('due date after relative date range (Today is 2021-11-01)', () => {
     beforeAll(() => {
         jest.useFakeTimers();
         jest.setSystemTime(new Date(2021, 10, 1)); // 2021-11-01
@@ -354,7 +354,7 @@ describe('due date after natural date range (Today is 2021-11-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',
+        'due after %s: task with due date %s, %s, %s are not included, %s is',
         (range: string, beforeRange: string, rangeStart: string, rangeEnd: string, afterRange: string) => {
             // Arrange
             const filter = new DueDateField().createFilterOrErrorMessage(`due after ${range}`);
@@ -373,7 +373,7 @@ describe('due date after natural date range (Today is 2021-11-01)', () => {
         ['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) => {
+    ])('should explain after relative date range (%s)', (range: string, date: string) => {
         const filter = new DueDateField().createFilterOrErrorMessage(`due after this ${range}`);
         expect(filter).toHaveExplanation(`due date is after ${date}`);
     });
diff --git a/tests/Query/Filter/TagsField.test.ts b/tests/Query/Filter/TagsField.test.ts
index 1682f60d..735c38d6 100644
--- a/tests/Query/Filter/TagsField.test.ts
+++ b/tests/Query/Filter/TagsField.test.ts
@@ -8,6 +8,68 @@ import { Sort } from '../../../src/Query/Sort';
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
 import { expectTaskComparesAfter, expectTaskComparesBefore } from '../../CustomMatchers/CustomMatchersForSorting';
 
+describe('tag presence & absence', () => {
+    it.each(['has tag', 'has tags'])('should have "%s" filtering', (filterLine: string) => {
+        // Arrange
+        const filter = new TagsField().createFilterOrErrorMessage(filterLine);
+
+        // Act, Assert
+        expect(filter.filterFunction).toBeDefined();
+        expect(filter.error).toBeUndefined();
+
+        expect(filter).toMatchTaskFromLine('- [ ] stuff #one');
+        expect(filter).toMatchTaskFromLine('- [ ] stuff #one #two');
+        expect(filter).not.toMatchTaskFromLine('- [ ] no tag here');
+    });
+
+    it.each(['no tag', 'no tags'])('should have "%s" filtering', (filterLine: string) => {
+        // Arrange
+        const filter = new TagsField().createFilterOrErrorMessage(filterLine);
+
+        // Act, Assert
+        expect(filter.filterFunction).toBeDefined();
+        expect(filter.error).toBeUndefined();
+
+        expect(filter).not.toMatchTaskFromLine('- [ ] stuff #one');
+        expect(filter).not.toMatchTaskFromLine('- [ ] stuff #one #two');
+        expect(filter).toMatchTaskFromLine('- [ ] no tag here');
+    });
+
+    it('should filter together with the global filter ("has tags")', () => {
+        updateSettings({ globalFilter: '#task' });
+
+        // Arrange
+        const filter = new TagsField().createFilterOrErrorMessage('has tags');
+
+        // Act, Assert
+        expect(filter.filterFunction).toBeDefined();
+        expect(filter.error).toBeUndefined();
+
+        expect(filter).toMatchTaskFromLine('- [ ] #task stuff #one ');
+        expect(filter).toMatchTaskFromLine('- [ ] #task stuff #one #two ');
+        expect(filter).not.toMatchTaskFromLine('- [ ] #task global filter is not a tag');
+
+        resetSettings();
+    });
+
+    it('should filter together with the global filter ("no tags")', () => {
+        updateSettings({ globalFilter: '#task' });
+
+        // Arrange
+        const filter = new TagsField().createFilterOrErrorMessage('no tags');
+
+        // Act, Assert
+        expect(filter.filterFunction).toBeDefined();
+        expect(filter.error).toBeUndefined();
+
+        expect(filter).not.toMatchTaskFromLine('- [ ] #task stuff #one ');
+        expect(filter).not.toMatchTaskFromLine('- [ ] #task stuff #one #two ');
+        expect(filter).toMatchTaskFromLine('- [ ] #task global filter is not a tag');
+
+        resetSettings();
+    });
+});
+
 describe('tag/tags', () => {
     it('should honour any # in query', () => {
         const filter = new TagsField().createFilterOrErrorMessage('tags include #home');