diff --git a/MMM-GoogleCalendar.js b/MMM-GoogleCalendar.js index cde39b6..0e38e1c 100644 --- a/MMM-GoogleCalendar.js +++ b/MMM-GoogleCalendar.js @@ -525,7 +525,7 @@ Module.register("MMM-GoogleCalendar", { // if those are common, as it avoids the listContainsEvent check for duplicates. // Filter based on `excludedEvents` config - if (this.config.excludedEvents?.length && this.config.excludedEvents.includes(event.summary)) { + if (this.config.excludedEvents?.length && this.isEventExcluded(event.summary)) { Log.debug(`Event ${event.id} ('${event.summary}') filtered due to excludedEvents settings.`); return true; } @@ -552,6 +552,54 @@ Module.register("MMM-GoogleCalendar", { return false; // Event should not be filtered out }, + /** + * Determines whether an event title matches any `excludedEvents` filter. + * + * Mirrors the default MagicMirror calendar module: each entry may be a plain + * string (case-insensitive substring match) or an object + * `{ filterBy, caseSensitive, regex }`. Previously this used an exact, + * case-sensitive full-title equality check, so partial filters such as + * "Birthday" never matched "John's Birthday" (issue #55). + * + * @param {string} title The event summary/title to test. + * @returns {boolean} True if the title matches an exclusion filter. + */ + isEventExcluded: function (title) { + if (typeof title !== "string") { + return false; + } + + for (const filter of this.config.excludedEvents) { + let pattern = filter; + let caseSensitive = false; + let useRegex = false; + + if (filter && typeof filter === "object") { + pattern = filter.filterBy; + caseSensitive = !!filter.caseSensitive; + useRegex = !!filter.regex; + } + + if (typeof pattern !== "string" || pattern === "") { + continue; + } + + if (useRegex) { + if (new RegExp(pattern, caseSensitive ? "" : "i").test(title)) { + return true; + } + } else if (caseSensitive) { + if (title.includes(pattern)) { + return true; + } + } else if (title.toLowerCase().includes(pattern.toLowerCase())) { + return true; + } + } + + return false; + }, + fetchCalendars: function () { this.config.calendars.forEach((calendar) => { if (!calendar.calendarID) { diff --git a/__tests__/MMM-GoogleCalendar.test.js b/__tests__/MMM-GoogleCalendar.test.js index b2fd0cd..01a0b19 100644 --- a/__tests__/MMM-GoogleCalendar.test.js +++ b/__tests__/MMM-GoogleCalendar.test.js @@ -116,5 +116,45 @@ describe('MMM-GoogleCalendar', () => { }); }); + describe('isEventExcluded', () => { + test('matches a case-insensitive substring (issue #55)', () => { + GCal.config.excludedEvents = ['Birthday']; + expect(GCal.isEventExcluded("John's Birthday")).toBe(true); + expect(GCal.isEventExcluded('my birthday party')).toBe(true); + }); + + test('does not match unrelated titles', () => { + GCal.config.excludedEvents = ['Birthday']; + expect(GCal.isEventExcluded('Team standup')).toBe(false); + }); + + test('still matches an exact full title (backwards compatible)', () => { + GCal.config.excludedEvents = ['Lunch']; + expect(GCal.isEventExcluded('Lunch')).toBe(true); + }); + + test('object form with caseSensitive only matches exact case', () => { + GCal.config.excludedEvents = [{ filterBy: 'Standup', caseSensitive: true }]; + expect(GCal.isEventExcluded('Daily Standup')).toBe(true); + expect(GCal.isEventExcluded('daily standup')).toBe(false); + }); + + test('object form with regex matches a pattern', () => { + GCal.config.excludedEvents = [{ filterBy: '^\\[private\\]', regex: true }]; + expect(GCal.isEventExcluded('[private] dentist')).toBe(true); + expect(GCal.isEventExcluded('not [private]')).toBe(false); + }); + + test('returns false for a private event with no summary', () => { + GCal.config.excludedEvents = ['Birthday']; + expect(GCal.isEventExcluded(undefined)).toBe(false); + }); + + test('ignores empty/invalid filter entries', () => { + GCal.config.excludedEvents = ['', { filterBy: '' }, {}]; + expect(GCal.isEventExcluded('anything')).toBe(false); + }); + }); + // Add more describe blocks for other pure functions if identified });