Skip to content

Commit

Permalink
feat: Add text searching of task recurrence rules (#1449)
Browse files Browse the repository at this point in the history
* feat: Add 'recurrence' filter - partial implementation

* test: Extract helper function with_recurrence()

* test: Simplify a test

* test: Add more tests of recurrence filters.

* test: Add more tests of recurrence filters.

* feat: Teach Query to parse 'recurrence' text filters

* feat: Document 'recurrence' text text searches
  • Loading branch information
claremacrae authored Dec 31, 2022
1 parent d1479b8 commit dbee157
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 1 deletion.
9 changes: 9 additions & 0 deletions docs/queries/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ For more information, see [Priorities]({{ site.baseurl }}{% link getting-started

- `is recurring`
- `is not recurring`
- `recurrence (includes|does not include) <part of recurrence rule>`
- Matches case-insensitive (disregards capitalization).
- Note that the text searched is generated programmatically and standardised, and so may not exactly match the text in any manually typed tasks. For example, a task with `🔁 every Sunday` will be searched as `every week on Sunday`.
- The easiest way to see the standardised recurrence rule of your tasks is to use `group by recurrence`, and review the resulting group headings.
- `recurrence (regex matches|regex does not match) /<JavaScript-style Regex>/`
- Does regular expression match (case-sensitive by default).
- Essential reading: [Regular Expression Searches]({{ site.baseurl }}{% link queries/regular-expressions.md %}).

> `recurrence` text searching was introduced in Tasks 1.22.0.
### Status

Expand Down
2 changes: 1 addition & 1 deletion docs/quick-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This table summarizes the filters and other options available inside a `tasks` b
| `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` | |
| `is recurring`<br>`is not recurring` | | `group by recurring` | |
| | | `group by recurrence` | `hide recurrence rule` |
| `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` |
| | `sort by urgency` | | `show urgency` |
| `path (includes, does not include) <path>`<br>`path (regex matches, regex does not match) /regex/i` | `sort by path` | `group by path` | |
Expand Down
16 changes: 16 additions & 0 deletions src/Query/Filter/RecurrenceField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Task } from '../../Task';
import { TextField } from './TextField';

export class RecurrenceField extends TextField {
fieldName(): string {
return 'recurrence';
}

value(task: Task): string {
if (task.recurrence !== null) {
return task.recurrence!.toText();
} else {
return '';
}
}
}
2 changes: 2 additions & 0 deletions src/Query/FilterParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TagsField } from './Filter/TagsField';
import { BooleanField } from './Filter/BooleanField';
import { FilenameField } from './Filter/FilenameField';
import { UrgencyField } from './Filter/UrgencyField';
import { RecurrenceField } from './Filter/RecurrenceField';

import type { FilterOrErrorMessage } from './Filter/Filter';
import type { Sorter } from './Sorter';
Expand All @@ -35,6 +36,7 @@ const fieldCreators = [
() => new BooleanField(),
() => new FilenameField(),
() => new UrgencyField(),
() => new RecurrenceField(),
];

export function parseFilter(filterString: string): FilterOrErrorMessage | null {
Expand Down
2 changes: 2 additions & 0 deletions tests/Query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ describe('Query parsing', () => {
'priority is low',
'priority is medium',
'priority is none',
'recurrence does not include wednesday',
'recurrence includes wednesday',
'scheduled after 2021-12-27',
'scheduled before 2021-12-27',
'scheduled date is invalid',
Expand Down
82 changes: 82 additions & 0 deletions tests/Query/Filter/RecurrenceField.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @jest-environment jsdom
*/
import moment from 'moment';

import { RecurrenceField } from '../../../src/Query/Filter/RecurrenceField';
import { TaskBuilder } from '../../TestingTools/TaskBuilder';
import { toBeValid, toMatchTask } from '../../CustomMatchers/CustomMatchersForFilters';
import { RecurrenceBuilder } from '../../TestingTools/RecurrenceBuilder';

window.moment = moment;

expect.extend({
toBeValid,
toMatchTask,
});

describe('recurrence', () => {
// Note: We don't need to check all behaviours that are implemented in the base class.
// These are minimal tests to confirm that the filters are correctly wired up,
// to guard against possible future coding errors.

// Easy construction of Tasks with given rule text
function with_recurrence(ruleText: string) {
const recurrence = new RecurrenceBuilder().rule(ruleText).startDate('2022-07-14').build();
return new TaskBuilder().recurrence(recurrence).build();
}

it('value', () => {
const field = new RecurrenceField();
expect(field.value(new TaskBuilder().build())).toStrictEqual('');
expect(field.value(with_recurrence('every Sunday when done'))).toStrictEqual('every week on Sunday when done');
expect(field.value(with_recurrence('every 6 months on the 2nd Wednesday'))).toStrictEqual(
'every 6 months on the 2nd Wednesday',
);
});

it('by recurrence (includes)', () => {
// Arrange
const filter = new RecurrenceField().createFilterOrErrorMessage('recurrence includes wednesday');

// Assert
expect(filter).toBeValid();
expect(filter).toMatchTask(with_recurrence('every Wednesday'));
expect(filter).not.toMatchTask(new TaskBuilder().build());
});

it('by recurrence (does not include)', () => {
// Arrange
const filter = new RecurrenceField().createFilterOrErrorMessage('recurrence does not include when done');

// Assert
expect(filter).toBeValid();
expect(filter).toMatchTask(new TaskBuilder().build());
expect(filter).toMatchTask(with_recurrence('every week on Sunday'));
expect(filter).not.toMatchTask(with_recurrence('every 10 days when done'));
});

it('by recurrence (regex matches)', () => {
// Arrange
const filter = new RecurrenceField().createFilterOrErrorMessage(String.raw`recurrence regex matches /\d/`); // any digit present

// Assert
expect(filter).toBeValid();
expect(filter).toMatchTask(with_recurrence('every month on the 31st'));
expect(filter).not.toMatchTask(new TaskBuilder().build());
expect(filter).not.toMatchTask(with_recurrence('every month on the last'));
});

it('by recurrence (regex does not match)', () => {
// Arrange
const filter = new RecurrenceField().createFilterOrErrorMessage(
String.raw`recurrence regex does not match /\d/`, // no digit present
);

// Assert
expect(filter).toBeValid();
expect(filter).not.toMatchTask(with_recurrence('every month on the 31st'));
expect(filter).toMatchTask(new TaskBuilder().build());
expect(filter).toMatchTask(with_recurrence('every month on the last'));
});
});

0 comments on commit dbee157

Please sign in to comment.