Fix date parsing parsing words with weekdays in them (#607)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/607
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2021-07-25 10:45:17 +00:00
parent 2a6649d9dc
commit c45911fd36
7 changed files with 119 additions and 60 deletions

View file

@ -1,4 +1,4 @@
import {parseTaskText} from '@/helpers/parseTaskText' import {parseTaskText} from '@/modules/parseTaskText'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import LabelTask from '@/models/labelTask' import LabelTask from '@/models/labelTask'

View file

@ -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) { if (currentDate.getHours() <= 9 || currentDate.getHours() > 21) {
return 9 return 9
} }
@ -18,4 +18,7 @@ export function calculateNearestHours(currentDate = new Date()) {
if (currentDate.getHours() <= 21) { if (currentDate.getHours() <= 21) {
return 21 return 21
} }
// Same case as in the first if, will never be called
return 9
} }

View file

@ -2,8 +2,18 @@ import {calculateDayInterval} from './calculateDayInterval'
import {calculateNearestHours} from './calculateNearestHours' import {calculateNearestHours} from './calculateNearestHours'
import {replaceAll} from '../replaceAll' import {replaceAll} from '../replaceAll'
export const parseDate = text => { interface dateParseResult {
const lowerText = text.toLowerCase() 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')) { if (lowerText.includes('today')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today') return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
@ -27,7 +37,7 @@ export const parseDate = text => {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week') return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week')
} }
if (lowerText.includes('next month')) { if (lowerText.includes('next month')) {
const date = new Date() const date: Date = new Date()
date.setDate(1) date.setDate(1)
date.setMonth(date.getMonth() + 1) date.setMonth(date.getMonth() + 1)
date.setHours(calculateNearestHours(date)) date.setHours(calculateNearestHours(date))
@ -37,8 +47,8 @@ export const parseDate = text => {
return addTimeToDate(text, date, 'next month') return addTimeToDate(text, date, 'next month')
} }
if (lowerText.includes('end of month')) { if (lowerText.includes('end of month')) {
const curDate = new Date() const curDate: Date = new Date()
const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0) const date: Date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
date.setHours(calculateNearestHours(date)) date.setHours(calculateNearestHours(date))
date.setMinutes(0) date.setMinutes(0)
date.setSeconds(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 matcher = new RegExp(`(${match} (at|@) )([0-9][0-9]?(:[0-9][0-9]?)?( ?(a|p)m)?)`, 'ig')
const results = matcher.exec(text) const results = matcher.exec(text)
@ -100,17 +117,17 @@ const addTimeToDate = (text, date, match) => {
} }
} }
export const getDateFromText = (text, now = new Date()) => { export const getDateFromText = (text: string, now: Date = 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 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 // 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021
let results = fullDateRegex.exec(text) let results: string[] | null = fullDateRegex.exec(text)
let result = results === null ? null : results[0] let result: string | null = results === null ? null : results[0]
let foundText = result let foundText: string | null = result
let containsYear = true let containsYear: boolean = true
if (result === null) { if (result === null) {
// 2. Try parsing the date as something like "jan 21" or "21 jan" // 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) results = monthRegex.exec(text)
result = results === null ? null : `${results[0]} ${now.getFullYear()}` result = results === null ? null : `${results[0]} ${now.getFullYear()}`
foundText = results === null ? '' : results[0] foundText = results === null ? '' : results[0]
@ -118,17 +135,24 @@ export const getDateFromText = (text, now = new Date()) => {
if (result === null) { if (result === null) {
// 3. Try parsing the date as "27/01" or "01/27" // 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) results = monthNumericRegex.exec(text)
// Put the year before or after the date, depending on what works // Put the year before or after the date, depending on what works
result = results === null ? null : `${now.getFullYear()}/${results[0]}` result = results === null ? null : `${now.getFullYear()}/${results[0]}`
if(result === null) {
return {
foundText,
date: null,
}
}
foundText = results === null ? '' : results[0] 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()}` result = results === null ? null : `${results[0]}/${now.getFullYear()}`
} }
if (isNaN(new Date(result)) && results[0] !== null) { if (result === null || (isNaN(new Date(result).getTime()) && foundText !== '')) {
const parts = results[0].split('/') const parts = foundText.split('/')
result = `${parts[1]}/${parts[0]}/${now.getFullYear()}` result = `${parts[1]}/${parts[0]}/${now.getFullYear()}`
} }
} }
@ -142,7 +166,7 @@ export const getDateFromText = (text, now = new Date()) => {
} }
const date = new Date(result) const date = new Date(result)
if (isNaN(date)) { if (isNaN(date.getTime())) {
return { return {
foundText, foundText,
date: null, 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 regex = /(in [0-9]+ (hours?|days?|weeks?|months?))/ig
const results = regex.exec(text) const results = regex.exec(text)
if (results === null) { 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 date = new Date(now)
const parts = foundText.split(' ') const parts = foundText.split(' ')
switch (parts[2]) { switch (parts[2]) {
@ -197,9 +221,9 @@ export const getDateFromTextIn = (text, now = new Date()) => {
} }
} }
const getDateFromWeekday = text => { const getDateFromWeekday = (text: string): dateFoundResult => {
const matcher = /(mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)/ig const matcher: RegExp = / (mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)/ig
const results = matcher.exec(text) const results: string[] | null = matcher.exec(text)
if (results === null) { if (results === null) {
return { return {
foundText: null, foundText: null,
@ -207,11 +231,11 @@ const getDateFromWeekday = text => {
} }
} }
const date = new Date() const date: Date = new Date()
const currentDay = date.getDay() const currentDay: number = date.getDay()
let day = 0 let day: number = 0
switch (results[0]) { switch (results[1]) {
case 'mon': case 'mon':
case 'monday': case 'monday':
day = 1 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) date.setDate(date.getDate() + distance)
return { return {
foundText: results[0], foundText: results[1],
date: date, 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 matcher = /(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)/ig
const results = matcher.exec(text) const results = matcher.exec(text)
if (results === null) { if (results === null) {
@ -279,7 +303,7 @@ const getDayFromText = text => {
} }
} }
const getDateFromInterval = interval => { const getDateFromInterval = (interval: number): Date => {
const newDate = new Date() const newDate = new Date()
newDate.setDate(newDate.getDate() + interval) newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate)) newDate.setHours(calculateNearestHours(newDate))

View file

@ -1,6 +1,6 @@
import {parseTaskText} from './parseTaskText' import {parseTaskText} from './parseTaskText'
import {getDateFromText, getDateFromTextIn} from './time/parseDate' import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
import {calculateDayInterval} from './time/calculateDayInterval' import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import priorities from '../models/priorities.json' import priorities from '../models/priorities.json'
describe('Parse Task Text', () => { describe('Parse Task Text', () => {
@ -194,6 +194,18 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(date.getDate() + 1) 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', () => { describe('Parse date from text', () => {
const now = new Date() const now = new Date()
@ -270,7 +282,6 @@ describe('Parse Task Text', () => {
}) })
} }
}) })
}) })
describe('Labels', () => { describe('Labels', () => {

View file

@ -1,18 +1,38 @@
import {parseDate} from './time/parseDate' import {parseDate} from '../helpers/time/parseDate'
import priorities from '../models/priorities.json' import _priorities from '../models/priorities.json'
const LABEL_PREFIX = '@' const LABEL_PREFIX: string = '@'
const LIST_PREFIX = '#' const LIST_PREFIX: string = '#'
const PRIORITY_PREFIX = '!' const PRIORITY_PREFIX: string = '!'
const ASSIGNEE_PREFIX = '+' 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. * Parses task text for dates, assignees, labels, lists, priorities and returns an object with all found intents.
* *
* @param text * @param text
*/ */
export const parseTaskText = text => { export const parseTaskText = (text: string): ParsedTaskText => {
const result = { const result: ParsedTaskText = {
text: text, text: text,
date: null, date: null,
labels: [], labels: [],
@ -23,7 +43,7 @@ export const parseTaskText = text => {
result.labels = getItemsFromPrefix(text, LABEL_PREFIX) 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.list = lists.length > 0 ? lists[0] : null
result.priority = getPriority(text) result.priority = getPriority(text)
@ -37,8 +57,8 @@ export const parseTaskText = text => {
return cleanupResult(result) return cleanupResult(result)
} }
const getItemsFromPrefix = (text, prefix) => { const getItemsFromPrefix = (text: string, prefix: string): string[] => {
const items = [] const items: string[] = []
const itemParts = text.split(prefix) const itemParts = text.split(prefix)
itemParts.forEach((p, index) => { itemParts.forEach((p, index) => {
@ -62,15 +82,15 @@ const getItemsFromPrefix = (text, prefix) => {
return Array.from(new Set(items)) return Array.from(new Set(items))
} }
const getPriority = text => { const getPriority = (text: string): number | null => {
const ps = getItemsFromPrefix(text, PRIORITY_PREFIX) const ps = getItemsFromPrefix(text, PRIORITY_PREFIX)
if (ps.length === 0) { if (ps.length === 0) {
return null return null
} }
for (const p of ps) { for (const p of ps) {
for (const pi in priorities) { for (const pi of Object.values(priorities)) {
if (priorities[pi] === parseInt(p)) { if (pi === parseInt(p)) {
return parseInt(p) return parseInt(p)
} }
} }
@ -79,7 +99,7 @@ const getPriority = text => {
return null return null
} }
const cleanupItemText = (text, items, prefix) => { const cleanupItemText = (text: string, items: string[], prefix: string): string => {
items.forEach(l => { items.forEach(l => {
text = text text = text
.replace(`${prefix}'${l}' `, '') .replace(`${prefix}'${l}' `, '')
@ -92,10 +112,10 @@ const cleanupItemText = (text, items, prefix) => {
return text 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.labels, LABEL_PREFIX)
result.text = cleanupItemText(result.text, [result.list], LIST_PREFIX) result.text = result.list !== null ? cleanupItemText(result.text, [result.list], LIST_PREFIX) : result.text
result.text = cleanupItemText(result.text, [result.priority], PRIORITY_PREFIX) 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 = cleanupItemText(result.text, result.assignees, ASSIGNEE_PREFIX)
result.text = result.text.trim() result.text = result.text.trim()

View file

@ -10,6 +10,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"types": [ "types": [