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: [],