Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1683 (2023-02-23 10:16:18)

feat: Teach DateParser to read date ranges
<!--- Provide a general summary of your changes in the Title above -->

# Description

Add `DateParser.parseDateRange()` to parse filters with two dates.

## Motivation and Context

This is a step on the way to implementing #1613.


## How has this been tested?

By TDD.

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

This is a new feature in code, not yet visible to users.

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

All changes done by pairing with @ilandikov 

## 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.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) 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://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [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)



Patch:
diff --git a/src/Query/DateParser.ts b/src/Query/DateParser.ts
index 2db22b07..2a64b595 100644
--- a/src/Query/DateParser.ts
+++ b/src/Query/DateParser.ts
@@ -1,4 +1,5 @@
 import * as chrono from 'chrono-node';
+import moment from 'moment';
 
 export class DateParser {
     public static parseDate(input: string, forwardDate: boolean = false): moment.Moment {
@@ -11,4 +12,29 @@ export class DateParser {
             )
             .startOf('day');
     }
+
+    /**
+     * Parse a line and extract a pair of dates, returned in a tuple, sorted by date.
+     * @param input - any pair of dates, separate by one or more spaces '17 August 2013 19 August 2013',
+     *                or a single date.
+     * @return - A Tuple of dates. If both input dates are invalid, then both ouput dates will be invalid.
+     */
+    public static parseDateRange(input: string): [moment.Moment, moment.Moment] {
+        const result = chrono.parse(input, undefined, {
+            forwardDate: true,
+        });
+
+        if (result.length === 0) {
+            return [moment.invalid(), moment.invalid()];
+        }
+
+        const startDate = result[0].start;
+        const endDate = result[1] && result[1].start ? result[1].start : startDate;
+        const start = window.moment(startDate.date());
+        const end = window.moment(endDate.date());
+        if (end.isBefore(start)) {
+            return [end, start];
+        }
+        return [start, end];
+    }
 }


Test patch:
diff --git a/tests/Query/DateParser.test.ts b/tests/Query/DateParser.test.ts
new file mode 100644
index 00000000..6023106a
--- /dev/null
+++ b/tests/Query/DateParser.test.ts
@@ -0,0 +1,71 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { DateParser } from '../../src/Query/DateParser';
+import { TaskRegularExpressions } from '../../src/Task';
+
+window.moment = moment;
+
+function testParsingeSingleDate(input: string, result: string) {
+    const moment = DateParser.parseDate(input);
+    expect(moment.format(TaskRegularExpressions.dateFormat)).toEqual(result);
+}
+
+function testParsingDateRange(input: string, expectedStart: string, expectedEnd: string) {
+    // Act
+    const result = DateParser.parseDateRange(input);
+
+    // Assert
+    const start = result[0];
+    const end = result[1];
+    expect(start).toBeDefined();
+    expect(end).toBeDefined();
+    const startFmt = start.format(TaskRegularExpressions.dateFormat);
+    const endFmt = end.format(TaskRegularExpressions.dateFormat);
+    expect([startFmt, endFmt]).toStrictEqual([expectedStart, expectedEnd]);
+}
+
+describe('DateParser - single dates', () => {
+    it('should parse a valid fixed date correctly', () => {
+        const input = '2021-03-17';
+        testParsingeSingleDate(input, input);
+    });
+
+    it('should recognise an invalid date correctly', () => {
+        testParsingeSingleDate('2021-13-17', 'Invalid date');
+    });
+});
+
+describe('DateParser - date ranges', () => {
+    it('should parse date range from natural dates', () => {
+        // Arrange
+        testParsingDateRange('17 August 2013 19 August 2013', '2013-08-17', '2013-08-19');
+    });
+
+    it('should parse date range with  multiple spaces', () => {
+        testParsingDateRange('2013-08-17   2014-08-19', '2013-08-17', '2014-08-19');
+    });
+
+    it('should parse date range with end before start', () => {
+        testParsingDateRange('2017-08-17 2014-08-19', '2014-08-19', '2017-08-17');
+    });
+
+    it('should parse single date as date range', () => {
+        // Arrange
+        const input = '2019-12-28';
+        testParsingDateRange(input, input, input);
+    });
+
+    it('should ignore invalid start date when parsing range', () => {
+        testParsingDateRange('2013-99-29 2014-08-19', '2014-08-19', '2014-08-19');
+    });
+
+    it('should ignore invalid end date when parsing range', () => {
+        testParsingDateRange('2014-08-19 2015-99-29', '2014-08-19', '2014-08-19');
+    });
+
+    it('should return 2 invalid dates when both dates are invalid', () => {
+        testParsingDateRange('2015-99-29 2015-99-29', 'Invalid date', 'Invalid date');
+    });
+});