Refactor date parsing logic to validate word boundaries, preventing partial matches for days and months

This commit is contained in:
Mo Tarbin
2026-05-25 16:47:05 -04:00
parent aad300bd9c
commit bbe68974b2
2 changed files with 53 additions and 28 deletions

View File

@@ -416,15 +416,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
dueDateHighlight = dueDateParsed.highlight[0] 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 // Create the cleaned sentence by sequentially applying all cleanups
let cleanedSentence = sentence let cleanedSentence = sentence
if (priority.result) cleanedSentence = priority.cleanedSentence if (priority.result) cleanedSentence = priority.cleanedSentence

View File

@@ -45,6 +45,20 @@ const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
(v, i, a) => a.indexOf(v) === i, (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 => { export const parsePriority = inputSentence => {
let sentence = inputSentence.toLowerCase() let sentence = inputSentence.toLowerCase()
const priorityMap = { const priorityMap = {
@@ -292,7 +306,22 @@ export const parseRepeatV2 = inputSentence => {
.toLowerCase() .toLowerCase()
.split(/ and |,|\s/) .split(/ and |,|\s/)
.map(day => day.trim()) .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]) .map(day => VALID_DAYS[day])
if (!result.frequencyMetadata.days.length) if (!result.frequencyMetadata.days.length)
return { result: null, name: null, cleanedSentence: inputSentence } return { result: null, name: null, cleanedSentence: inputSentence }
@@ -318,7 +347,20 @@ export const parseRepeatV2 = inputSentence => {
.toLowerCase() .toLowerCase()
.split(/ and |,|\s/) .split(/ and |,|\s/)
.map(month => month.trim()) .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]) .map(month => VALID_MONTHS[month])
result.frequencyMetadata.unit = 'days' result.frequencyMetadata.unit = 'days'
return { return {
@@ -664,7 +706,7 @@ export const parseDueDate = (inputSentence, chrono) => {
forwardDate: true, forwardDate: true,
}) })
if (!parsedDueDate[0] || parsedDueDate[0].index === -1) { if (!parsedDueDate.length) {
return { return {
result: null, result: null,
highlight: [], 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 if (!dueDateMatch) {
// 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 { return {
result: null, result: null,
highlight: [], highlight: [],