Skip to content

Commit

Permalink
Add a new 'limit' for queries
Browse files Browse the repository at this point in the history
See README for details.

Fixes #76
  • Loading branch information
schemar committed May 5, 2021
1 parent 15f3599 commit 8b89382
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 108 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ The following filters exist:
- `description (includes|does not include) <string>`
- `exclude sub-items`
- When this is set, the result list will only include tasks that are not indented in their file. It will only show tasks that are top level list items in their list.
- `limit to <number> tasks`
- Only lists the first `<number>` tasks of the result.
- Shorthand is `limit <number>`.

All filters of a query have to match in order for a task to be listed.
This means you cannot show tasks that have "GitHub in the path and have no due date or are due after 2021-04-04".
Expand Down Expand Up @@ -232,6 +235,15 @@ Show all tasks that were done before the 1st of December 2020:
done before 2020-12-01
```

Show one task that is due on the 5th of May and includes `#prio1` in its description:

```tasks
not done
due on 2021-05-05
description includes #prio1
limit to 1 tasks
````

### Tips

#### Daily Agenda
Expand Down
180 changes: 83 additions & 97 deletions src/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,70 @@ import moment from 'moment';
import { Status, Task } from './Task';

export class Query {
public readonly filters: ((task: Task) => boolean)[];
private _limit: number | undefined = undefined;
private _filters: ((task: Task) => boolean)[] = [];
private readonly noDueString = 'no due date';
private readonly dueRegexp = /due (before|after|on) (\d{4}-\d{2}-\d{2})/;
private readonly doneString = 'done';
private readonly notDoneString = 'not done';
private readonly doneRegexp = /done (before|after|on) (\d{4}-\d{2}-\d{2})/;
private readonly pathRegexp = /path (includes|does not include) (.*)/;
private readonly descriptionRegexp = /description (includes|does not include) (.*)/;
private readonly limitRegexp = /limit (to )?(\d+)( tasks?)?/;
private readonly excludeSubItemsString = 'exclude sub-items';

constructor({ source }: { source: string }) {
this.filters = this.parseFilters({ source });
source
.split('\n')
.map((line: string) => line.trim())
.forEach((line: string) => {
switch (true) {
case line === '':
break;
case line === this.doneString:
this._filters.push(
(task) => task.status === Status.Done,
);
break;
case line === this.notDoneString:
this._filters.push(
(task) => task.status !== Status.Done,
);
break;
case line === this.excludeSubItemsString:
this._filters.push((task) => task.indentation === '');
break;
case line === this.noDueString:
this._filters.push((task) => task.dueDate === null);
break;
case this.dueRegexp.test(line):
this.parseDueFilter({ line });
break;
case this.doneRegexp.test(line):
this.parseDoneFilter({ line });
break;
case this.pathRegexp.test(line):
this.parsePathFilter({ line });
break;
case this.descriptionRegexp.test(line):
this.parseDescriptionFilter({ line });
break;
case this.limitRegexp.test(line):
this.parseLimit({ line });
break;
}
});
}

private parseFilters({
source,
}: {
source: string;
}): ((task: Task) => boolean)[] {
const filters: ((task: Task) => boolean)[] = [];
const sourceLines = source.split('\n').map((line) => line.trim());

for (const sourceLine of sourceLines) {
if (sourceLine === '') {
continue;
}

if (sourceLine === this.doneString) {
filters.push((task) => task.status === Status.Done);
} else if (sourceLine === this.notDoneString) {
filters.push((task) => task.status !== Status.Done);
} else if (sourceLine === this.noDueString) {
filters.push((task) => task.dueDate === null);
} else if (this.dueRegexp.test(sourceLine)) {
const filter = this.parseDueFilter({ line: sourceLine });
if (filter !== null) {
filters.push(filter);
}
} else if (this.doneRegexp.test(sourceLine)) {
const filter = this.parseDoneFilter({ line: sourceLine });
if (filter !== null) {
filters.push(filter);
}
} else if (this.pathRegexp.test(sourceLine)) {
const filter = this.parsePathFilter({ line: sourceLine });
if (filter !== null) {
filters.push(filter);
}
} else if (this.descriptionRegexp.test(sourceLine)) {
const filter = this.parseDescriptionFilter({
line: sourceLine,
});
if (filter !== null) {
filters.push(filter);
}
} else if (sourceLine === this.excludeSubItemsString) {
filters.push((task) => task.indentation === '');
} else {
console.error('Tasks: unknown query filter:', sourceLine);
}
}
public get limit(): number | undefined {
return this._limit;
}

return filters;
public get filters(): ((task: Task) => boolean)[] {
return this._filters;
}

private parseDueFilter({
line,
}: {
line: string;
}): ((task: Task) => boolean) | null {
private parseDueFilter({ line }: { line: string }): void {
const dueMatch = line.match(this.dueRegexp);
if (dueMatch !== null) {
let filter;
Expand All @@ -88,18 +82,13 @@ export class Query {
task.dueDate ? task.dueDate.isSame(filterDate) : false;
}

return filter;
this._filters.push(filter);
} else {
console.error('Tasks: unknown query filter (due date):', line);
}

console.error('Tasks: unknown query filter (due date):', line);
return null;
}

private parseDoneFilter({
line,
}: {
line: string;
}): ((task: Task) => boolean) | null {
private parseDoneFilter({ line }: { line: string }): void {
const doneMatch = line.match(this.doneRegexp);
if (doneMatch !== null) {
let filter;
Expand All @@ -115,65 +104,62 @@ export class Query {
task.doneDate ? task.doneDate.isSame(filterDate) : false;
}

return filter;
this._filters.push(filter);
}

console.error('Tasks: unknown query filter (done date):', line);
return null;
}

private parsePathFilter({
line,
}: {
line: string;
}): ((task: Task) => boolean) | null {
private parsePathFilter({ line }: { line: string }): void {
const pathMatch = line.match(this.pathRegexp);
if (pathMatch !== null) {
let filter;
const filterMethod = pathMatch[1];
if (filterMethod === 'includes') {
filter = (task: Task) => task.path.includes(pathMatch[2]);
this._filters.push((task: Task) =>
task.path.includes(pathMatch[2]),
);
} else if (pathMatch[1] === 'does not include') {
filter = (task: Task) => !task.path.includes(pathMatch[2]);
this._filters.push(
(task: Task) => !task.path.includes(pathMatch[2]),
);
} else {
console.error('Tasks: unknown query filter (path):', line);
return null;
}

return filter;
} else {
console.error('Tasks: unknown query filter (path):', line);
}

console.error('Tasks: unknown query filter (path):', line);
return null;
}

private parseDescriptionFilter({
line,
}: {
line: string;
}): ((task: Task) => boolean) | null {
private parseDescriptionFilter({ line }: { line: string }): void {
const descriptionMatch = line.match(this.descriptionRegexp);
if (descriptionMatch !== null) {
let filter;
const filterMethod = descriptionMatch[1];
if (filterMethod === 'includes') {
filter = (task: Task) =>
task.description.includes(descriptionMatch[2]);
this._filters.push((task: Task) =>
task.description.includes(descriptionMatch[2]),
);
} else if (descriptionMatch[1] === 'does not include') {
filter = (task: Task) =>
!task.description.includes(descriptionMatch[2]);
this._filters.push(
(task: Task) =>
!task.description.includes(descriptionMatch[2]),
);
} else {
console.error(
'Tasks: unknown query filter (description):',
line,
);
return null;
}

return filter;
} else {
console.error('Tasks: unknown query filter (description):', line);
}
}

console.error('Tasks: unknown query filter (description):', line);
return null;
private parseLimit({ line }: { line: string }): void {
const limitMatch = line.match(this.limitRegexp);
if (limitMatch !== null) {
// limitMatch[2] is per regex always digits and therefore parsable.
const limit = Number.parseInt(limitMatch[2], 10);
this._limit = limit;
} else {
console.error('Tasks: unknown query limit:', line);
}
}
}
24 changes: 13 additions & 11 deletions src/QueryRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {

import { Cache, State } from './Cache';
import { Sort } from './Sort';
import type { Task } from './Task';
import { Query } from './Query';

export class QueryRenderer {
Expand All @@ -32,31 +31,31 @@ export class QueryRenderer {
new QueryRenderChild({
cache: this.cache,
container: element,
filters: query.filters,
query,
}),
);
}
}

class QueryRenderChild extends MarkdownRenderChild {
private readonly cache: Cache;
private readonly filters: ((task: Task) => boolean)[];
private readonly query: Query;

private cacheCallbackId: number | undefined;

constructor({
cache,
container,
filters,
query,
}: {
cache: Cache;
container: HTMLElement;
filters: ((task: Task) => boolean)[];
query: Query;
}) {
super(container);

this.cache = cache;
this.filters = filters;
this.query = query;
}

onload() {
Expand Down Expand Up @@ -92,17 +91,20 @@ class QueryRenderChild extends MarkdownRenderChild {
content: HTMLDivElement,
): Promise<{ taskList: HTMLUListElement; tasksCount: number }> {
let tasks = this.cache.getTasks();
for (const filter of this.filters) {
this.query.filters.forEach((filter) => {
tasks = tasks.filter(filter);
}
});

const tasksSorted = Sort.byDateThenPath(tasks);
const tasksCount = tasksSorted.length;
const tasksSortedLimited = Sort.byDateThenPath(tasks).slice(
0,
this.query.limit,
);
const tasksCount = tasksSortedLimited.length;

const taskList = content.createEl('ul');
taskList.addClass('contains-task-list');
for (let i = 0; i < tasksCount; i++) {
const task = tasksSorted[i];
const task = tasksSortedLimited[i];

let fileName: string | undefined;
const fileNameMatch = task.path.match(/([^/]+)\.md$/);
Expand Down

0 comments on commit 8b89382

Please sign in to comment.