chore: Minor fix

This commit is contained in:
iamsivin
2026-02-19 21:25:20 +05:30
parent 2f21009609
commit bde3368a9f
3 changed files with 240 additions and 14 deletions
@@ -190,7 +190,7 @@ const WORD_NUMBERS = Object.keys(WORD_NUMBER_MAP).join('|');
const RELATIVE_DAYS = Object.keys(RELATIVE_DAY_MAP).join('|');
const TIME_OF_DAY_NAMES = 'morning|afternoon|evening|night|noon|midnight';
const NUM_RE = `(\\d+|${WORD_NUMBERS})`;
const NUM_RE = `(\\d+(?:\\.5)?|${WORD_NUMBERS})`;
const UNIT_RE = `(${UNIT_NAMES})`;
const TIME_SUFFIX_RE =
'(?:\\s+(?:at\\s+)?(\\d{1,2}(?::\\d{2})?\\s*(?:am|pm|a\\.m\\.?|p\\.m\\.?)?|\\d{1,2}:\\d{2}))?';
@@ -297,12 +297,38 @@ const resolveTimeOfDay = (text, now) => {
// ─── Pattern Matchers ────────────────────────────────────────────────────────
const FRACTIONAL_CONVERT = {
hours: { unit: 'minutes', factor: 60 },
days: { unit: 'hours', factor: 24 },
weeks: { unit: 'days', factor: 7 },
months: { unit: 'days', factor: 30 },
years: { unit: 'months', factor: 12 },
minutes: { unit: 'seconds', factor: 60 },
};
const addFractionalSafe = (date, unit, amount) => {
if (Number.isInteger(amount)) return add(date, { [unit]: amount });
if (amount % 1 !== 0.5) return null;
const conv = FRACTIONAL_CONVERT[unit];
if (conv) return add(date, { [conv.unit]: Math.round(amount * conv.factor) });
return add(date, { [unit]: Math.round(amount) });
};
const HALF_UNIT_DURATIONS = {
hour: { minutes: 30 },
day: { hours: 12 },
week: { days: 3, hours: 12 },
month: { days: 15 },
year: { months: 6 },
};
const matchRelativeDuration = (text, now) => {
if (text.match(/^(?:in\s+)?half\s+(?:an?\s+)?hour$/)) {
return add(now, { minutes: 30 });
}
if (text.match(/^(?:in\s+)?half\s+(?:an?\s+)?day$/)) {
return add(now, { hours: 12 });
const halfMatch = text.match(
/^(?:in\s+)?half\s+(?:an?\s+)?(hour|day|week|month|year)$/
);
if (halfMatch) {
const duration = HALF_UNIT_DURATIONS[halfMatch[1]];
return duration ? add(now, duration) : null;
}
const match = text.match(new RegExp(`^(?:in\\s+)?${NUM_RE}\\s+${UNIT_RE}$`));
@@ -312,7 +338,7 @@ const matchRelativeDuration = (text, now) => {
const unit = UNIT_MAP[match[2]];
if (amount == null || !unit) return null;
return add(now, { [unit]: amount });
return addFractionalSafe(now, unit, amount);
};
const matchDurationFromNow = (text, now) => {
@@ -325,7 +351,7 @@ const matchDurationFromNow = (text, now) => {
const unit = UNIT_MAP[match[2]];
if (amount == null || !unit) return null;
return add(now, { [unit]: amount });
return addFractionalSafe(now, unit, amount);
};
const matchRelativeDay = (text, now) => {
@@ -792,7 +818,9 @@ export const parseDateFromText = (text, referenceDate = new Date()) => {
// ─── Suggestion Candidates (uses maps already defined above) ─────────────────
const CANONICAL_UNITS = [...new Set(Object.values(UNIT_MAP))];
const SUGGESTION_UNITS = [...new Set(Object.values(UNIT_MAP))].filter(
u => u !== 'seconds'
);
const MONTH_NAMES_LONG = Object.keys(MONTH_MAP).filter(k => k.length > 3);
@@ -827,9 +855,19 @@ const matchesPrefix = (candidate, text) => {
const buildSuggestionCandidates = text => {
if (/^\d/.test(text)) {
const num = text.match(/^\d+/)[0];
return CANONICAL_UNITS.map(u => `${num} ${u}`);
const num = text.match(/^\d+(?:\.5)?/)[0];
return SUGGESTION_UNITS.map(u => `${num} ${u}`);
}
if ('half'.startsWith(text)) {
return Object.keys(HALF_UNIT_DURATIONS).map(u => `half ${u}`);
}
const wordNum = WORD_NUMBER_MAP[text];
if (wordNum != null && wordNum >= 1) {
return SUGGESTION_UNITS.map(u => `${wordNum} ${u}`);
}
return PHRASE_CANDIDATES.filter(c => matchesPrefix(c, text));
};
@@ -1,4 +1,7 @@
import { parseDateFromText } from '../snoozeDateParser';
import {
parseDateFromText,
generateDateSuggestions,
} from '../snoozeDateParser';
const now = new Date('2023-06-16T10:00:00');
@@ -730,3 +733,188 @@ describe('regression: contradictory time-of-day + time rejected', () => {
expect(result.date.getHours()).toEqual(14);
});
});
describe('generateDateSuggestions', () => {
describe('half suggestions', () => {
it('"half" returns half hour/day/week/month/year suggestions', () => {
const results = generateDateSuggestions('half', now);
const labels = results.map(r => r.label);
expect(labels).toContain('half hour');
expect(labels).toContain('half day');
expect(labels).toContain('half week');
expect(labels).toContain('half month');
expect(labels).toContain('half year');
});
it('"ha" returns half suggestions (partial match)', () => {
const results = generateDateSuggestions('ha', now);
const labels = results.map(r => r.label);
expect(labels).toContain('half hour');
expect(labels).toContain('half day');
});
it('"hal" returns half suggestions (partial match)', () => {
const results = generateDateSuggestions('hal', now);
expect(results.length).toBeGreaterThan(0);
expect(results[0].label).toMatch(/^half /);
});
});
describe('word number suggestions', () => {
it('"two" returns duration suggestions', () => {
const results = generateDateSuggestions('two', now);
const labels = results.map(r => r.label);
expect(labels).toContain('2 minutes');
expect(labels).toContain('2 hours');
expect(labels).toContain('2 days');
});
it('"ten" returns duration suggestions', () => {
const results = generateDateSuggestions('ten', now);
const labels = results.map(r => r.label);
expect(labels).toContain('10 minutes');
expect(labels).toContain('10 hours');
});
it('"five" returns duration suggestions', () => {
const results = generateDateSuggestions('five', now);
const labels = results.map(r => r.label);
expect(labels).toContain('5 minutes');
expect(labels).toContain('5 hours');
expect(labels).toContain('5 days');
});
});
describe('no seconds in suggestions', () => {
it('"2" does not suggest seconds', () => {
const results = generateDateSuggestions('2', now);
const labels = results.map(r => r.label);
expect(labels).not.toContain('2 seconds');
expect(labels).toContain('2 minutes');
});
it('"100" does not suggest seconds', () => {
const results = generateDateSuggestions('100', now);
const labels = results.map(r => r.label);
const hasSeconds = labels.some(l => l.includes('seconds'));
expect(hasSeconds).toBe(false);
});
});
describe('decimal number suggestions', () => {
it('"1.5" returns duration suggestions', () => {
const results = generateDateSuggestions('1.5', now);
const labels = results.map(r => r.label);
expect(labels).toContain('1.5 hours');
expect(labels).toContain('1.5 days');
});
});
describe('caps at MAX_SUGGESTIONS', () => {
it('returns at most 5 results', () => {
const results = generateDateSuggestions('2', now);
expect(results.length).toBeLessThanOrEqual(5);
});
});
});
describe('dot-delimited dates', () => {
it('"12.12.2034" parses to Dec 12 2034', () => {
const result = parseDateFromText('12.12.2034', now);
expect(result).not.toBeNull();
expect(result.date.getFullYear()).toEqual(2034);
expect(result.date.getMonth()).toEqual(11);
expect(result.date.getDate()).toEqual(12);
});
it('"01.06.2025" parses correctly', () => {
const result = parseDateFromText('01.06.2025', now);
expect(result).not.toBeNull();
expect(result.date.getFullYear()).toEqual(2025);
});
});
describe('noise word stripping', () => {
it('"snooze this for 5 minutes" parses', () => {
const result = parseDateFromText('snooze this for 5 minutes', now);
expect(result).not.toBeNull();
});
it('"please snooze this for half a day" parses', () => {
const result = parseDateFromText('please snooze this for half a day', now);
expect(result).not.toBeNull();
});
it('"snooze this until tomorrow" parses', () => {
const result = parseDateFromText('snooze this until tomorrow', now);
expect(result).not.toBeNull();
});
it('"schedule this for 2025-01-15" parses', () => {
const result = parseDateFromText('schedule this for 2025-01-15', now);
expect(result).not.toBeNull();
expect(result.date.getFullYear()).toEqual(2025);
expect(result.date.getMonth()).toEqual(0);
expect(result.date.getDate()).toEqual(15);
});
});
describe('half unit parsing', () => {
it('"half hour" adds 30 minutes', () => {
const result = parseDateFromText('half hour', now);
expect(result).not.toBeNull();
expect(result.date.getMinutes()).toEqual(30);
});
it('"half day" adds 12 hours', () => {
const result = parseDateFromText('half day', now);
expect(result).not.toBeNull();
expect(result.date.getHours()).toEqual(22);
});
it('"half week" parses to a future date', () => {
const result = parseDateFromText('half week', now);
expect(result).not.toBeNull();
expect(result.date > now).toBe(true);
});
it('"half month" parses to a future date', () => {
const result = parseDateFromText('half month', now);
expect(result).not.toBeNull();
expect(result.date > now).toBe(true);
});
it('"half year" parses to ~6 months ahead', () => {
const result = parseDateFromText('half year', now);
expect(result).not.toBeNull();
expect(result.date.getMonth()).toEqual(11);
});
});
describe('decimal duration parsing (only .5 allowed)', () => {
it('"1.5 hours" parses correctly', () => {
const result = parseDateFromText('1.5 hours', now);
expect(result).not.toBeNull();
expect(result.date > now).toBe(true);
});
it('"1.5 days" parses correctly', () => {
const result = parseDateFromText('1.5 days', now);
expect(result).not.toBeNull();
expect(result.date > now).toBe(true);
});
it('"0.5 hours" parses correctly', () => {
const result = parseDateFromText('0.5 hours', now);
expect(result).not.toBeNull();
expect(result.date > now).toBe(true);
});
it('"1.3 hours" returns null (only .5 allowed)', () => {
expect(parseDateFromText('1.3 hours', now)).toBeNull();
});
it('"2.7 days" returns null (only .5 allowed)', () => {
expect(parseDateFromText('2.7 days', now)).toBeNull();
});
});
@@ -53,12 +53,12 @@ const placeholder = computed(() =>
);
const hotKeys = computed(() => [
...dynamicSnoozeActions.value,
...inboxHotKeys.value,
...goToCommandHotKeys.value,
...goToAppearanceHotKeys.value,
...bulkActionsHotKeys.value,
...conversationHotKeys.value,
...dynamicSnoozeActions.value,
]);
const setCommandBarData = () => {
@@ -156,7 +156,7 @@ const onCommandBarChange = item => {
];
if (uniqueParents.length === 1) {
currentCommandRoot.value = uniqueParents[0];
} else if (uniqueParents.length > 1) {
} else {
currentCommandRoot.value = null;
}
}