From c45911fd36410b199039f0f61d2c63d87cca2a36 Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 25 Jul 2021 10:45:17 +0000 Subject: [PATCH] Fix date parsing parsing words with weekdays in them (#607) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/607 Co-authored-by: konrad Co-committed-by: konrad --- src/components/tasks/mixins/createTask.js | 2 +- ...arestHours.js => calculateNearestHours.ts} | 5 +- .../time/{parseDate.js => parseDate.ts} | 86 ++++++++++++------- src/models/priorities.json | 12 +-- .../parseTaskText.test.js | 17 +++- .../parseTaskText.ts} | 56 ++++++++---- tsconfig.json | 1 + 7 files changed, 119 insertions(+), 60 deletions(-) rename src/helpers/time/{calculateNearestHours.js => calculateNearestHours.ts} (66%) rename src/helpers/time/{parseDate.js => parseDate.ts} (71%) rename src/{helpers => modules}/parseTaskText.test.js (95%) rename src/{helpers/parseTaskText.js => modules/parseTaskText.ts} (54%) diff --git a/src/components/tasks/mixins/createTask.js b/src/components/tasks/mixins/createTask.js index 6bf139d18..2c68c5a4f 100644 --- a/src/components/tasks/mixins/createTask.js +++ b/src/components/tasks/mixins/createTask.js @@ -1,4 +1,4 @@ -import {parseTaskText} from '@/helpers/parseTaskText' +import {parseTaskText} from '@/modules/parseTaskText' import TaskModel from '@/models/task' import {formatISO} from 'date-fns' import LabelTask from '@/models/labelTask' diff --git a/src/helpers/time/calculateNearestHours.js b/src/helpers/time/calculateNearestHours.ts similarity index 66% rename from src/helpers/time/calculateNearestHours.js rename to src/helpers/time/calculateNearestHours.ts index e2f7f2dc5..14136691d 100644 --- a/src/helpers/time/calculateNearestHours.js +++ b/src/helpers/time/calculateNearestHours.ts @@ -1,4 +1,4 @@ -export function calculateNearestHours(currentDate = new Date()) { +export function calculateNearestHours(currentDate: Date = new Date()): number { if (currentDate.getHours() <= 9 || currentDate.getHours() > 21) { return 9 } @@ -18,4 +18,7 @@ export function calculateNearestHours(currentDate = new Date()) { if (currentDate.getHours() <= 21) { return 21 } + + // Same case as in the first if, will never be called + return 9 } \ No newline at end of file diff --git a/src/helpers/time/parseDate.js b/src/helpers/time/parseDate.ts similarity index 71% rename from src/helpers/time/parseDate.js rename to src/helpers/time/parseDate.ts index 77e7fb9ed..762f9f72b 100644 --- a/src/helpers/time/parseDate.js +++ b/src/helpers/time/parseDate.ts @@ -2,8 +2,18 @@ import {calculateDayInterval} from './calculateDayInterval' import {calculateNearestHours} from './calculateNearestHours' import {replaceAll} from '../replaceAll' -export const parseDate = text => { - const lowerText = text.toLowerCase() +interface dateParseResult { + newText: string, + date: Date | null, +} + +interface dateFoundResult { + foundText: string | null, + date: Date | null, +} + +export const parseDate = (text: string): dateParseResult => { + const lowerText: string = text.toLowerCase() if (lowerText.includes('today')) { return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today') @@ -27,7 +37,7 @@ export const parseDate = text => { return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week') } if (lowerText.includes('next month')) { - const date = new Date() + const date: Date = new Date() date.setDate(1) date.setMonth(date.getMonth() + 1) date.setHours(calculateNearestHours(date)) @@ -37,8 +47,8 @@ export const parseDate = text => { return addTimeToDate(text, date, 'next month') } if (lowerText.includes('end of month')) { - const curDate = new Date() - const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0) + const curDate: Date = new Date() + const date: Date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0) date.setHours(calculateNearestHours(date)) date.setMinutes(0) date.setSeconds(0) @@ -72,7 +82,14 @@ export const parseDate = text => { } } -const addTimeToDate = (text, date, match) => { +const addTimeToDate = (text: string, date: Date, match: string | null): dateParseResult => { + if (match === null) { + return { + newText: text, + date: null, + } + } + const matcher = new RegExp(`(${match} (at|@) )([0-9][0-9]?(:[0-9][0-9]?)?( ?(a|p)m)?)`, 'ig') const results = matcher.exec(text) @@ -100,17 +117,17 @@ const addTimeToDate = (text, date, match) => { } } -export const getDateFromText = (text, now = new Date()) => { - const fullDateRegex = /([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig +export const getDateFromText = (text: string, now: Date = new Date()) => { + const fullDateRegex: RegExp = /([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig // 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021 - let results = fullDateRegex.exec(text) - let result = results === null ? null : results[0] - let foundText = result - let containsYear = true + let results: string[] | null = fullDateRegex.exec(text) + let result: string | null = results === null ? null : results[0] + let foundText: string | null = result + let containsYear: boolean = true if (result === null) { // 2. Try parsing the date as something like "jan 21" or "21 jan" - const monthRegex = /((jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) [0-9][0-9]?|[0-9][0-9]? (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))/ig + const monthRegex: RegExp = /((jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) [0-9][0-9]?|[0-9][0-9]? (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))/ig results = monthRegex.exec(text) result = results === null ? null : `${results[0]} ${now.getFullYear()}` foundText = results === null ? '' : results[0] @@ -118,17 +135,24 @@ export const getDateFromText = (text, now = new Date()) => { if (result === null) { // 3. Try parsing the date as "27/01" or "01/27" - const monthNumericRegex = /([0-9][0-9]?\/[0-9][0-9]?)/ig + const monthNumericRegex:RegExp = /([0-9][0-9]?\/[0-9][0-9]?)/ig results = monthNumericRegex.exec(text) // Put the year before or after the date, depending on what works result = results === null ? null : `${now.getFullYear()}/${results[0]}` + if(result === null) { + return { + foundText, + date: null, + } + } + foundText = results === null ? '' : results[0] - if (isNaN(new Date(result))) { + if (result === null || isNaN(new Date(result).getTime())) { result = results === null ? null : `${results[0]}/${now.getFullYear()}` } - if (isNaN(new Date(result)) && results[0] !== null) { - const parts = results[0].split('/') + if (result === null || (isNaN(new Date(result).getTime()) && foundText !== '')) { + const parts = foundText.split('/') result = `${parts[1]}/${parts[0]}/${now.getFullYear()}` } } @@ -142,7 +166,7 @@ export const getDateFromText = (text, now = new Date()) => { } const date = new Date(result) - if (isNaN(date)) { + if (isNaN(date.getTime())) { return { foundText, date: null, @@ -159,7 +183,7 @@ export const getDateFromText = (text, now = new Date()) => { } } -export const getDateFromTextIn = (text, now = new Date()) => { +export const getDateFromTextIn = (text: string, now: Date = new Date()) => { const regex = /(in [0-9]+ (hours?|days?|weeks?|months?))/ig const results = regex.exec(text) if (results === null) { @@ -169,7 +193,7 @@ export const getDateFromTextIn = (text, now = new Date()) => { } } - let foundText = results[0] + const foundText: string = results[0] const date = new Date(now) const parts = foundText.split(' ') switch (parts[2]) { @@ -197,9 +221,9 @@ export const getDateFromTextIn = (text, now = new Date()) => { } } -const getDateFromWeekday = text => { - const matcher = /(mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)/ig - const results = matcher.exec(text) +const getDateFromWeekday = (text: string): dateFoundResult => { + const matcher: RegExp = / (mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)/ig + const results: string[] | null = matcher.exec(text) if (results === null) { return { foundText: null, @@ -207,11 +231,11 @@ const getDateFromWeekday = text => { } } - const date = new Date() - const currentDay = date.getDay() - let day = 0 + const date: Date = new Date() + const currentDay: number = date.getDay() + let day: number = 0 - switch (results[0]) { + switch (results[1]) { case 'mon': case 'monday': day = 1 @@ -247,16 +271,16 @@ const getDateFromWeekday = text => { } } - const distance = (day + 7 - currentDay) % 7 + const distance: number = (day + 7 - currentDay) % 7 date.setDate(date.getDate() + distance) return { - foundText: results[0], + foundText: results[1], date: date, } } -const getDayFromText = text => { +const getDayFromText = (text: string) => { const matcher = /(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)/ig const results = matcher.exec(text) if (results === null) { @@ -279,7 +303,7 @@ const getDayFromText = text => { } } -const getDateFromInterval = interval => { +const getDateFromInterval = (interval: number): Date => { const newDate = new Date() newDate.setDate(newDate.getDate() + interval) newDate.setHours(calculateNearestHours(newDate)) diff --git a/src/models/priorities.json b/src/models/priorities.json index 8a781e2ef..c38b195d9 100644 --- a/src/models/priorities.json +++ b/src/models/priorities.json @@ -1,8 +1,8 @@ { - "UNSET": 0, - "LOW": 1, - "MEDIUM": 2, - "HIGH": 3, - "URGENT": 4, - "DO_NOW": 5 +"UNSET": 0, +"LOW": 1, +"MEDIUM": 2, +"HIGH": 3, +"URGENT": 4, +"DO_NOW": 5 } \ No newline at end of file diff --git a/src/helpers/parseTaskText.test.js b/src/modules/parseTaskText.test.js similarity index 95% rename from src/helpers/parseTaskText.test.js rename to src/modules/parseTaskText.test.js index 94382558f..a3710df33 100644 --- a/src/helpers/parseTaskText.test.js +++ b/src/modules/parseTaskText.test.js @@ -1,6 +1,6 @@ import {parseTaskText} from './parseTaskText' -import {getDateFromText, getDateFromTextIn} from './time/parseDate' -import {calculateDayInterval} from './time/calculateDayInterval' +import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate' +import {calculateDayInterval} from '../helpers/time/calculateDayInterval' import priorities from '../models/priorities.json' describe('Parse Task Text', () => { @@ -194,6 +194,18 @@ describe('Parse Task Text', () => { expect(result.text).toBe('Lorem Ipsum') expect(result.date.getDate()).toBe(date.getDate() + 1) }) + it('should only recognize weekdays with a space before or after them 1', () => { + const result = parseTaskText('Lorem Ipsum renewed') + + expect(result.text).toBe('Lorem Ipsum renewed') + expect(result.date).toBeNull() + }) + it('should only recognize weekdays with a space before or after them 2', () => { + const result = parseTaskText('Lorem Ipsum github') + + expect(result.text).toBe('Lorem Ipsum github') + expect(result.date).toBeNull() + }) describe('Parse date from text', () => { const now = new Date() @@ -270,7 +282,6 @@ describe('Parse Task Text', () => { }) } }) - }) describe('Labels', () => { diff --git a/src/helpers/parseTaskText.js b/src/modules/parseTaskText.ts similarity index 54% rename from src/helpers/parseTaskText.js rename to src/modules/parseTaskText.ts index 2ad0e75d4..4896e63dc 100644 --- a/src/helpers/parseTaskText.js +++ b/src/modules/parseTaskText.ts @@ -1,18 +1,38 @@ -import {parseDate} from './time/parseDate' -import priorities from '../models/priorities.json' +import {parseDate} from '../helpers/time/parseDate' +import _priorities from '../models/priorities.json' -const LABEL_PREFIX = '@' -const LIST_PREFIX = '#' -const PRIORITY_PREFIX = '!' -const ASSIGNEE_PREFIX = '+' +const LABEL_PREFIX: string = '@' +const LIST_PREFIX: string = '#' +const PRIORITY_PREFIX: string = '!' +const ASSIGNEE_PREFIX: string = '+' + +const priorities: Priorites = _priorities + +interface Priorites { + UNSET: number, + LOW: number, + MEDIUM: number, + HIGH: number, + URGENT: number, + DO_NOW: number, +} + +interface ParsedTaskText { + text: string, + date: Date | null, + labels: string[], + list: string | null, + priority: number | null, + assignees: string[], +} /** * Parses task text for dates, assignees, labels, lists, priorities and returns an object with all found intents. * * @param text */ -export const parseTaskText = text => { - const result = { +export const parseTaskText = (text: string): ParsedTaskText => { + const result: ParsedTaskText = { text: text, date: null, labels: [], @@ -23,7 +43,7 @@ export const parseTaskText = text => { result.labels = getItemsFromPrefix(text, LABEL_PREFIX) - const lists = getItemsFromPrefix(text, LIST_PREFIX) + const lists: string[] = getItemsFromPrefix(text, LIST_PREFIX) result.list = lists.length > 0 ? lists[0] : null result.priority = getPriority(text) @@ -37,8 +57,8 @@ export const parseTaskText = text => { return cleanupResult(result) } -const getItemsFromPrefix = (text, prefix) => { - const items = [] +const getItemsFromPrefix = (text: string, prefix: string): string[] => { + const items: string[] = [] const itemParts = text.split(prefix) itemParts.forEach((p, index) => { @@ -62,15 +82,15 @@ const getItemsFromPrefix = (text, prefix) => { return Array.from(new Set(items)) } -const getPriority = text => { +const getPriority = (text: string): number | null => { const ps = getItemsFromPrefix(text, PRIORITY_PREFIX) if (ps.length === 0) { return null } for (const p of ps) { - for (const pi in priorities) { - if (priorities[pi] === parseInt(p)) { + for (const pi of Object.values(priorities)) { + if (pi === parseInt(p)) { return parseInt(p) } } @@ -79,7 +99,7 @@ const getPriority = text => { return null } -const cleanupItemText = (text, items, prefix) => { +const cleanupItemText = (text: string, items: string[], prefix: string): string => { items.forEach(l => { text = text .replace(`${prefix}'${l}' `, '') @@ -92,10 +112,10 @@ const cleanupItemText = (text, items, prefix) => { return text } -const cleanupResult = result => { +const cleanupResult = (result: ParsedTaskText): ParsedTaskText => { result.text = cleanupItemText(result.text, result.labels, LABEL_PREFIX) - result.text = cleanupItemText(result.text, [result.list], LIST_PREFIX) - result.text = cleanupItemText(result.text, [result.priority], PRIORITY_PREFIX) + result.text = result.list !== null ? cleanupItemText(result.text, [result.list], LIST_PREFIX) : result.text + result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], PRIORITY_PREFIX) : result.text result.text = cleanupItemText(result.text, result.assignees, ASSIGNEE_PREFIX) result.text = result.text.trim() diff --git a/tsconfig.json b/tsconfig.json index ab38a1919..dd9377416 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, "sourceMap": true, "baseUrl": ".", "types": [