Skip to content

Commit 201b828

Browse files
authored
refactor: Tags search now implemented with SubstringMatcher (#1056)
* refactor: Add SubstringMatcher.matchesAnyOf() * refactor: Make IStringMatcher an abstract class In preparation for moving a method down from SubstringMatcher * refactor: Move matchesAnyOf() down to IStringMatcher * refactor: Rework 'tags include' & co to use SubstringMatcher * refactor: Rework 'do not include' & co to use SubstringMatcher * refactor: Rework TagsField to use TextField.maybeNegate() * refactor: Simplify TagsField, using idea from TextField
1 parent 19d984e commit 201b828

File tree

6 files changed

+34
-19
lines changed

6 files changed

+34
-19
lines changed

src/Query/Filter/TagsField.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Task } from '../../Task';
2+
import { SubstringMatcher } from '../Matchers/SubstringMatcher';
23
import { Field } from './Field';
34
import { FilterOrErrorMessage } from './Filter';
45
import { TextField } from './TextField';
@@ -22,19 +23,13 @@ export class TagsField extends Field {
2223
// Search is done sans the hash. If it is provided then strip it off.
2324
const search = tagMatch[3].replace(/^#/, '');
2425

25-
if (filterMethod === 'include' || filterMethod === 'includes') {
26+
if (filterMethod.includes('include')) {
27+
const matcher = new SubstringMatcher(search);
2628
result.filter = (task: Task) =>
27-
task.tags.find((tag) =>
28-
TextField.stringIncludesCaseInsensitive(tag, search),
29-
) !== undefined;
30-
} else if (
31-
tagMatch[2] === 'do not include' ||
32-
tagMatch[2] === 'does not include'
33-
) {
34-
result.filter = (task: Task) =>
35-
task.tags.find((tag) =>
36-
TextField.stringIncludesCaseInsensitive(tag, search),
37-
) == undefined;
29+
TextField.maybeNegate(
30+
matcher.matchesAnyOf(task.tags),
31+
filterMethod,
32+
);
3833
} else {
3934
result.error = 'do not understand query filter (tag/tags)';
4035
}

src/Query/Filter/TextField.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export abstract class TextField extends Field {
6868
*/
6969
protected abstract value(task: Task): string;
7070

71-
private static maybeNegate(match: boolean, filterMethod: String) {
71+
public static maybeNegate(match: boolean, filterMethod: String) {
7272
return filterMethod.match(/not/) ? !match : match;
7373
}
7474
}

src/Query/Matchers/IStringMatcher.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44
* This is used to hide away the details of various text searches, such as the
55
* simple inclusion of a sub-string, or the more complex regular expression searches.
66
*/
7-
export interface IStringMatcher {
7+
export abstract class IStringMatcher {
88
/**
99
* Return whether the given string matches this condition.
1010
* @param stringToSearch
1111
*/
12-
matches(stringToSearch: string): boolean;
12+
public abstract matches(stringToSearch: string): boolean;
13+
14+
/**
15+
* Return whether any of the given strings matches this condition.
16+
* @param stringsToSearch
17+
*/
18+
public matchesAnyOf(stringsToSearch: string[]) {
19+
return stringsToSearch.some((s) => this.matches(s));
20+
}
1321
}

src/Query/Matchers/RegexMatcher.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { IStringMatcher } from './IStringMatcher';
1+
import { IStringMatcher } from './IStringMatcher';
22

33
/**
44
* Regular-expression-based implementation of IStringMatcher.
55
*/
6-
export class RegexMatcher implements IStringMatcher {
6+
export class RegexMatcher extends IStringMatcher {
77
private readonly regex: RegExp;
88

99
/**
@@ -12,6 +12,7 @@ export class RegexMatcher implements IStringMatcher {
1212
* @param regex {RegExp} - A valid regular expression
1313
*/
1414
public constructor(regex: RegExp) {
15+
super();
1516
this.regex = regex;
1617
}
1718

src/Query/Matchers/SubstringMatcher.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { IStringMatcher } from './IStringMatcher';
1+
import { IStringMatcher } from './IStringMatcher';
22

33
/**
44
* Substring-based implementation of IStringMatcher.
55
*
66
* This does a case-insensitive search for the given string.
77
*/
8-
export class SubstringMatcher implements IStringMatcher {
8+
export class SubstringMatcher extends IStringMatcher {
99
private readonly stringToFind: string;
1010

1111
/**
@@ -15,6 +15,7 @@ export class SubstringMatcher implements IStringMatcher {
1515
* Searches will be case-insensitive.
1616
*/
1717
public constructor(stringToFind: string) {
18+
super();
1819
this.stringToFind = stringToFind;
1920
}
2021

tests/Query/Matchers/SubstringMatcher.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,14 @@ describe('SubstringMatcher', () => {
77
expect(matcher.matches('prefix find me suffix')).toStrictEqual(true);
88
expect(matcher.matches('FIND ME')).toStrictEqual(true);
99
});
10+
11+
it('should match any values in array of text', () => {
12+
const matcher = new SubstringMatcher('find me');
13+
expect(
14+
matcher.matchesAnyOf(['wibble', 'stuff', 'FIND ME']),
15+
).toStrictEqual(true);
16+
expect(
17+
matcher.matchesAnyOf(['wibble', 'stuff', 'LOSE ME']),
18+
).not.toStrictEqual(true);
19+
});
1020
});

0 commit comments

Comments
 (0)