From d10727b87ddc6bbd29e5bee9598c35ff3ca53d84 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 25 May 2026 12:46:18 -0400 Subject: [PATCH 1/2] Enhance due date parsing by validating word boundaries to prevent partial matches --- src/views/components/CustomParsers.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js index 0a45e2d..7d5d689 100644 --- a/src/views/components/CustomParsers.js +++ b/src/views/components/CustomParsers.js @@ -672,7 +672,29 @@ export const parseDueDate = (inputSentence, chrono) => { } } - const dueDateMatch = parsedDueDate[0] + let dueDateMatch = parsedDueDate[0] + + // Validate word boundaries for the matched date text + // This prevents matching partial words like "wed" in "wedding", fri in "friend", etc. + const matchStart = dueDateMatch.index + const matchEnd = matchStart + dueDateMatch.text.length + + const charBefore = matchStart > 0 ? inputSentence[matchStart - 1] : ' ' + const charAfter = + matchEnd < inputSentence.length ? inputSentence[matchEnd] : ' ' + + // Check if the match is part of a larger word + // If alphanumeric characters exist before/after, it's not a word boundary + const isValidWordBoundary = + !/[a-zA-Z0-9]/.test(charBefore) && !/[a-zA-Z0-9]/.test(charAfter) + + if (!isValidWordBoundary) { + return { + result: null, + highlight: [], + cleanedSentence: inputSentence, + } + } const dueDateText = dueDateMatch.text const dueDateStartIndex = dueDateMatch.index const dueDateEndIndex = dueDateStartIndex + dueDateText.length From eae67fd48207d40744cdbcd61e5ea17db7bb2997 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 25 May 2026 16:47:05 -0400 Subject: [PATCH 2/2] Refactor date parsing logic to validate word boundaries, preventing partial matches for days and months --- src/views/components/AddTaskModal.jsx | 9 ---- src/views/components/CustomParsers.js | 72 ++++++++++++++++++++------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index f886be5..806a897 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -416,15 +416,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => { dueDateHighlight = dueDateParsed.highlight[0] } - if (repeat.result) { - // if repeat has result the cleaned sentence will remove the date related info which mean - // we need to reparse the date again to get the correct due date: - const dueDateParsedAgain = parseDueDate(sentence, chrono) - if (dueDateParsedAgain.result) { - syncDueDateStates(dueDateParsedAgain.result) - } - } - // Create the cleaned sentence by sequentially applying all cleanups let cleanedSentence = sentence if (priority.result) cleanedSentence = priority.cleanedSentence diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js index 7d5d689..8e5cb4e 100644 --- a/src/views/components/CustomParsers.js +++ b/src/views/components/CustomParsers.js @@ -45,6 +45,20 @@ const ALL_MONTHS = Object.values(VALID_MONTHS).filter( (v, i, a) => a.indexOf(v) === i, ) +// Helper function to validate word boundaries for date/time matches +// Prevents matching partial words like 'wed' in 'wedding', 'fri' in 'friend', etc. +const isValidWordBoundary = (text, matchIndex, matchLength) => { + const charBefore = matchIndex > 0 ? text[matchIndex - 1] : ' ' + const charAfter = + matchIndex + matchLength < text.length + ? text[matchIndex + matchLength] + : ' ' + // Valid boundaries: spaces, punctuation, but NOT alphanumeric or word-forming characters + return !( + /[\p{L}\p{N}_]/u.test(charBefore) || /[\p{L}\p{N}_]/u.test(charAfter) + ) +} + export const parsePriority = inputSentence => { let sentence = inputSentence.toLowerCase() const priorityMap = { @@ -292,7 +306,22 @@ export const parseRepeatV2 = inputSentence => { .toLowerCase() .split(/ and |,|\s/) .map(day => day.trim()) - .filter(day => VALID_DAYS[day]) + .filter(day => { + // Validate that the day abbreviation is at proper word boundaries + // This prevents matches like 'wed' in 'wedding', 'fri' in 'friend' + if (!VALID_DAYS[day]) return false + // For short abbreviations (3 chars or less), validate word boundaries + if (day.length <= 3) { + const dayIndex = sentence.toLowerCase().indexOf(day) + if (dayIndex === -1) return false + return isValidWordBoundary( + sentence.toLowerCase(), + dayIndex, + day.length, + ) + } + return true + }) .map(day => VALID_DAYS[day]) if (!result.frequencyMetadata.days.length) return { result: null, name: null, cleanedSentence: inputSentence } @@ -318,7 +347,20 @@ export const parseRepeatV2 = inputSentence => { .toLowerCase() .split(/ and |,|\s/) .map(month => month.trim()) - .filter(month => VALID_MONTHS[month]) + .filter(month => { + if (!VALID_MONTHS[month]) return false + // For short abbreviations (3 chars or less), validate word boundaries + if (month.length <= 3) { + const monthIndex = sentence.toLowerCase().indexOf(month) + if (monthIndex === -1) return false + return isValidWordBoundary( + sentence.toLowerCase(), + monthIndex, + month.length, + ) + } + return true + }) .map(month => VALID_MONTHS[month]) result.frequencyMetadata.unit = 'days' return { @@ -664,7 +706,7 @@ export const parseDueDate = (inputSentence, chrono) => { forwardDate: true, }) - if (!parsedDueDate[0] || parsedDueDate[0].index === -1) { + if (!parsedDueDate.length) { return { result: null, highlight: [], @@ -672,23 +714,15 @@ export const parseDueDate = (inputSentence, chrono) => { } } - let dueDateMatch = parsedDueDate[0] + // Select the first valid word-bounded date match + // Prevents false positives like "wed" in "wedding" while still allowing later valid matches + const dueDateMatch = parsedDueDate.find( + match => + match.index !== -1 && + isValidWordBoundary(inputSentence, match.index, match.text.length), + ) - // Validate word boundaries for the matched date text - // This prevents matching partial words like "wed" in "wedding", fri in "friend", etc. - const matchStart = dueDateMatch.index - const matchEnd = matchStart + dueDateMatch.text.length - - const charBefore = matchStart > 0 ? inputSentence[matchStart - 1] : ' ' - const charAfter = - matchEnd < inputSentence.length ? inputSentence[matchEnd] : ' ' - - // Check if the match is part of a larger word - // If alphanumeric characters exist before/after, it's not a word boundary - const isValidWordBoundary = - !/[a-zA-Z0-9]/.test(charBefore) && !/[a-zA-Z0-9]/.test(charAfter) - - if (!isValidWordBoundary) { + if (!dueDateMatch) { return { result: null, highlight: [],