Skip to content

Commit

Permalink
Merge pull request #1665 from ilandikov/refactor-and-approval-datefield
Browse files Browse the repository at this point in the history
refactor: DateField, HappensDateField and approval tests on DueDateField
  • Loading branch information
claremacrae committed Feb 16, 2023
2 parents 0bd2d9a + 526611f commit c560387
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 120 deletions.
87 changes: 53 additions & 34 deletions src/Query/Filter/DateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Explanation } from '../Explain/Explanation';
import type { Comparator } from '../Sorter';
import { compareByDate } from '../../lib/DateTools';
import { Field } from './Field';
import { Filter, FilterOrErrorMessage } from './Filter';
import { Filter, type FilterFunction, FilterOrErrorMessage } from './Filter';
import { FilterInstructions } from './FilterInstructions';

/**
Expand Down Expand Up @@ -43,38 +43,20 @@ export abstract class DateField extends Field {

const result = new FilterOrErrorMessage(line);

const match = Field.getMatch(this.filterRegExp(), line);
let filterFunction;
if (match !== null) {
const filterDate = DateParser.parseDate(match[2]);
if (!filterDate.isValid()) {
const fieldNameKeywordDate = Field.getMatch(this.filterRegExp(), line);
if (fieldNameKeywordDate !== null) {
const fieldKeyword = fieldNameKeywordDate[1];
const fieldDate = DateParser.parseDate(fieldNameKeywordDate[2]);
if (!fieldDate.isValid()) {
result.error = 'do not understand ' + this.fieldName() + ' date';
} else {
let relative;
if (match[1] === 'before') {
filterFunction = (task: Task) => {
const date = this.date(task);
return date ? date.isBefore(filterDate) : this.filterResultIfFieldMissing();
};
relative = ' ' + match[1];
} else if (match[1] === 'after') {
filterFunction = (task: Task) => {
const date = this.date(task);
return date ? date.isAfter(filterDate) : this.filterResultIfFieldMissing();
};
relative = ' ' + match[1];
} else {
filterFunction = (task: Task) => {
const date = this.date(task);
return date ? date.isSame(filterDate) : this.filterResultIfFieldMissing();
};
relative = ' on';
}
const explanation = DateField.getExplanationString(
const filterFunction = this.buildFilterFunction(fieldKeyword, fieldDate);

const explanation = DateField.buildExplanation(
this.fieldName(),
relative,
fieldKeyword,
this.filterResultIfFieldMissing(),
filterDate,
fieldDate,
);
result.filter = new Filter(line, filterFunction, new Explanation(explanation));
}
Expand All @@ -84,6 +66,33 @@ export abstract class DateField extends Field {
return result;
}

/**
* Builds function that actually filters the tasks depending on the date
* @param fieldKeyword relationship to be held with the date 'before', 'after'
* @param fieldDate the date to be used by the filter function
* @returns the function that filters the tasks
*/
private buildFilterFunction(fieldKeyword: string, fieldDate: moment.Moment): FilterFunction {
let filterFunction;
if (fieldKeyword === 'before') {
filterFunction = (task: Task) => {
const date = this.date(task);
return date ? date.isBefore(fieldDate) : this.filterResultIfFieldMissing();
};
} else if (fieldKeyword === 'after') {
filterFunction = (task: Task) => {
const date = this.date(task);
return date ? date.isAfter(fieldDate) : this.filterResultIfFieldMissing();
};
} else {
filterFunction = (task: Task) => {
const date = this.date(task);
return date ? date.isSame(fieldDate) : this.filterResultIfFieldMissing();
};
}
return filterFunction;
}

/**
* Return the task's value for this date field, if any.
* @param task - a Task object
Expand All @@ -94,19 +103,29 @@ export abstract class DateField extends Field {
/**
* Construct a string used to explain a date-based filter
* @param fieldName - for example, 'due'
* @param relationshipPrefixedWithSpace - for example ' before' or ''
* @param fieldKeyword - one of the keywords like 'before' or 'after'
* @param filterResultIfFieldMissing - whether the search matches tasks without the requested date value
* @param filterDate - the date used in the filter
*/
public static getExplanationString(
public static buildExplanation(
fieldName: string,
relationshipPrefixedWithSpace: string,
fieldKeyword: string,
filterResultIfFieldMissing: boolean,
filterDate: moment.Moment,
) {
): string {
let relationship;
switch (fieldKeyword) {
case 'before':
case 'after':
relationship = fieldKeyword;
break;
default:
relationship = 'on';
break;
}
// Example of formatted date: '2024-01-02 (Tuesday 2nd January 2024)'
const actualDate = filterDate.format('YYYY-MM-DD (dddd Do MMMM YYYY)');
let result = `${fieldName} date is${relationshipPrefixedWithSpace} ${actualDate}`;
let result = `${fieldName} date is ${relationship} ${actualDate}`;
if (filterResultIfFieldMissing) {
result += ` OR no ${fieldName} date`;
}
Expand Down
79 changes: 44 additions & 35 deletions src/Query/Filter/HappensDateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Explanation } from '../Explain/Explanation';
import type { Comparator } from '../Sorter';
import { compareByDate } from '../../lib/DateTools';
import { Field } from './Field';
import { Filter, FilterOrErrorMessage } from './Filter';
import { Filter, type FilterFunction, FilterOrErrorMessage } from './Filter';
import { FilterInstructions } from './FilterInstructions';
import { DateField } from './DateField';

Expand Down Expand Up @@ -46,35 +46,20 @@ export class HappensDateField extends Field {

const result = new FilterOrErrorMessage(line);

const happensMatch = Field.getMatch(this.filterRegExp(), line);
if (happensMatch !== null) {
const filterDate = DateParser.parseDate(happensMatch[2]);
if (!filterDate.isValid()) {
const fieldNameKeywordDate = Field.getMatch(this.filterRegExp(), line);
if (fieldNameKeywordDate !== null) {
const fieldKeyword = fieldNameKeywordDate[1];
const fieldDate = DateParser.parseDate(fieldNameKeywordDate[2]);
if (!fieldDate.isValid()) {
result.error = 'do not understand happens date';
} else {
let filterFunction;
let relative;
if (happensMatch[1] === 'before') {
filterFunction = (task: Task) => {
return this.dates(task).some((date) => date && date.isBefore(filterDate));
};
relative = ' ' + happensMatch[1];
} else if (happensMatch[1] === 'after') {
filterFunction = (task: Task) => {
return this.dates(task).some((date) => date && date.isAfter(filterDate));
};
relative = ' ' + happensMatch[1];
} else {
filterFunction = (task: Task) => {
return this.dates(task).some((date) => date && date.isSame(filterDate));
};
relative = ' on';
}
const explanation = DateField.getExplanationString(
const filterFunction = this.buildFilterFunction(fieldKeyword, fieldDate);

const explanation = DateField.buildExplanation(
'due, start or scheduled',
relative,
fieldKeyword,
false,
filterDate,
fieldDate,
);
result.filter = new Filter(line, filterFunction, new Explanation(explanation));
}
Expand All @@ -85,16 +70,27 @@ export class HappensDateField extends Field {
}

/**
* Return the earliest of the dates used by 'happens' in the given task, or null if none set.
*
* Generally speaking, the earliest date is considered to be the highest priority,
* as it is the first point at which the user might wish to act on the task.
* @param task
* Builds function that actually filters the tasks depending on the date
* @param fieldKeyword relationship to be held with the date 'before', 'after'
* @param fieldDate the date to be used by the filter function
* @returns the function that filters the tasks
*/
public earliestDate(task: Task): Moment | null {
const happensDates = new HappensDateField().dates(task);
const sortedHappensDates = happensDates.sort(compareByDate);
return sortedHappensDates[0];
private buildFilterFunction(fieldKeyword: string, fieldDate: moment.Moment): FilterFunction {
let filterFunction;
if (fieldKeyword === 'before') {
filterFunction = (task: Task) => {
return this.dates(task).some((date) => date && date.isBefore(fieldDate));
};
} else if (fieldKeyword === 'after') {
filterFunction = (task: Task) => {
return this.dates(task).some((date) => date && date.isAfter(fieldDate));
};
} else {
filterFunction = (task: Task) => {
return this.dates(task).some((date) => date && date.isSame(fieldDate));
};
}
return filterFunction;
}

protected filterRegExp(): RegExp {
Expand Down Expand Up @@ -124,4 +120,17 @@ export class HappensDateField extends Field {
return compareByDate(this.earliestDate(a), this.earliestDate(b));
};
}

/**
* Return the earliest of the dates used by 'happens' in the given task, or null if none set.
*
* Generally speaking, the earliest date is considered to be the highest priority,
* as it is the first point at which the user might wish to act on the task.
* @param task
*/
public earliestDate(task: Task): Moment | null {
const happensDates = new HappensDateField().dates(task);
const sortedHappensDates = happensDates.sort(compareByDate);
return sortedHappensDates[0];
}
}
55 changes: 4 additions & 51 deletions tests/DocsSamplesForStatuses.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Options } from 'approvals/lib/Core/Options';
import { verify } from 'approvals/lib/Providers/Jest/JestApprovals';

import { Status } from '../src/Status';
Expand All @@ -13,53 +12,7 @@ import type { StatusCollection, StatusCollectionEntry } from '../src/StatusColle
import * as Themes from '../src/Config/Themes';
import { StatusValidator } from '../src/StatusValidator';
import { TaskBuilder } from './TestingTools/TaskBuilder';

function verifyMarkdown(markdown: string) {
let output = '<!-- placeholder to force blank line before included text -->\n\n';
output += markdown;
output += '\n\n<!-- placeholder to force blank line after included text -->\n';
let options = new Options();
options = options.forFile().withFileExtention('md');
verify(output, options);
}

class MarkdownTable {
private columnNames: string[];
private _markdown = '';

constructor(columnNames: string[]) {
this.columnNames = columnNames;
this.addTitleRow();
}

get markdown(): string {
return this._markdown;
}

private addTitleRow() {
let titles = '|';
let divider = '|';
this.columnNames.forEach((s) => {
titles += ` ${s} |`;
divider += ' ----- |';
});

this._markdown += `${titles}\n`;
this._markdown += `${divider}\n`;
}

public addRow(cells: string[]) {
let row = '|';
cells.forEach((s) => {
row += ` ${s} |`;
});
this._markdown += `${row}\n`;
}

public verify() {
verifyMarkdown(this.markdown);
}
}
import { MarkdownTable, verifyMarkdownForDocs } from './TestingTools/VerifyMarkdownTable';

function getPrintableSymbol(symbol: string) {
const result = symbol !== ' ' ? symbol : 'space';
Expand Down Expand Up @@ -88,7 +41,7 @@ function verifyStatusesAsMarkdownTable(statuses: Status[], showQueryInstructions
const needsCustomStyling = status.symbol !== ' ' && status.symbol !== 'x' ? 'Yes' : 'No';
table.addRow([statusCharacter, nextStatusCharacter, status.name, type, needsCustomStyling]);
}
table.verify();
table.verifyForDocs();
}

function verifyStatusesAsTasksList(statuses: Status[]) {
Expand All @@ -97,7 +50,7 @@ function verifyStatusesAsTasksList(statuses: Status[]) {
const statusCharacter = getPrintableSymbol(status.symbol);
markdown += `- [${status.symbol}] #task ${statusCharacter} ${status.name}\n`;
}
verifyMarkdown(markdown);
verifyMarkdownForDocs(markdown);
}

function verifyStatusesAsTasksText(statuses: Status[]) {
Expand Down Expand Up @@ -254,7 +207,7 @@ function verifyTransitionsAsMarkdownTable(statuses: Status[]) {
showGroupNamesForAllTasks('status.type', new StatusTypeField().createGrouper().grouper);
showGroupNamesForAllTasks('status.name', new StatusNameField().createGrouper().grouper);

table.verify();
table.verifyForDocs();
}

describe('Status Transitions', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
| date / keyword | last week | this week | next week | 2023-02-09 | 2023-02-07 2023-02-11 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| before | due before last week =><br> due date is before 2023-02-03 (Friday 3rd February 2023)<br> | due before this week =><br> due date is before 2023-02-05 (Sunday 5th February 2023)<br> | due before next week =><br> due date is before 2023-02-17 (Friday 17th February 2023)<br> | due before 2023-02-09 =><br> due date is before 2023-02-09 (Thursday 9th February 2023)<br> | due before 2023-02-07 2023-02-11 =><br> due date is before 2023-02-07 (Tuesday 7th February 2023)<br> |
| on | due on last week =><br> due date is on 2023-02-03 (Friday 3rd February 2023)<br> | due on this week =><br> due date is on 2023-02-05 (Sunday 5th February 2023)<br> | due on next week =><br> due date is on 2023-02-17 (Friday 17th February 2023)<br> | due on 2023-02-09 =><br> due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due on 2023-02-07 2023-02-11 =><br> due date is on 2023-02-07 (Tuesday 7th February 2023)<br> |
| after | due after last week =><br> due date is after 2023-02-03 (Friday 3rd February 2023)<br> | due after this week =><br> due date is after 2023-02-05 (Sunday 5th February 2023)<br> | due after next week =><br> due date is after 2023-02-17 (Friday 17th February 2023)<br> | due after 2023-02-09 =><br> due date is after 2023-02-09 (Thursday 9th February 2023)<br> | due after 2023-02-07 2023-02-11 =><br> due date is after 2023-02-07 (Tuesday 7th February 2023)<br> |
| in | due in last week =><br> due date is on 2023-02-03 (Friday 3rd February 2023)<br> | due in this week =><br> due date is on 2023-02-05 (Sunday 5th February 2023)<br> | due in next week =><br> due date is on 2023-02-17 (Friday 17th February 2023)<br> | due in 2023-02-09 =><br> due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due in 2023-02-07 2023-02-11 =><br> due date is on 2023-02-07 (Tuesday 7th February 2023)<br> |
| | due last week =><br> due date is on 2023-02-03 (Friday 3rd February 2023)<br> | due this week =><br> due date is on 2023-02-05 (Sunday 5th February 2023)<br> | due next week =><br> due date is on 2023-02-17 (Friday 17th February 2023)<br> | due 2023-02-09 =><br> due date is on 2023-02-09 (Thursday 9th February 2023)<br> | due 2023-02-07 2023-02-11 =><br> due date is on 2023-02-07 (Tuesday 7th February 2023)<br> |
34 changes: 34 additions & 0 deletions tests/Query/Filter/DueDateField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
expectTaskComparesBefore,
expectTaskComparesEqual,
} from '../../CustomMatchers/CustomMatchersForSorting';
import { Query } from '../../../src/Query/Query';
import { MarkdownTable } from '../../TestingTools/VerifyMarkdownTable';

window.moment = moment;

Expand Down Expand Up @@ -90,3 +92,35 @@ describe('sorting by due', () => {
expectTaskComparesEqual(sorter, date2, date2);
});
});

describe('due date', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date(2023, 1, 10)); // 2023-02-10
});

afterAll(() => {
jest.useRealTimers();
});

it('approval tests', () => {
const dates = ['last week', 'this week', 'next week', '2023-02-09', '2023-02-07 2023-02-11'];
const keywords = ['before ', 'on ', 'after ', 'in ', ''];

const table = new MarkdownTable(['date / keyword'].concat(dates));

keywords.forEach((keyword) => {
const newRow = [keyword];
dates.forEach((date) => {
const query = new Query({ source: `due ${keyword}${date}` });
expect(query.error).toBeUndefined();

newRow.push(query.explainQueryWithoutIntroduction().replace(/(\n)/g, '<br>'));
});

table.addRow(newRow);
});

table.verify();
});
});
Loading

0 comments on commit c560387

Please sign in to comment.