diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebbaf01e..1f8cf3ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,7 @@ _This release is scheduled to be released on 2023-07-01._
### Added
-- Added tests for severonly
+- Added tests for serveronly
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
### Removed
@@ -23,6 +23,7 @@ _This release is scheduled to be released on 2023-07-01._
- Update electron to v24
- Use node v19 in github workflow (replacing v14)
- Refactor formatTime into common util function for default modules
+- Refactor some calendar methods into own class and added tests for them
### Fixed
diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js
index 23b94134..f78df757 100644
--- a/modules/default/calendar/calendar.js
+++ b/modules/default/calendar/calendar.js
@@ -1,4 +1,4 @@
-/* global cloneObject */
+/* global CalendarUtils, cloneObject */
/* MagicMirror²
* Module: Calendar
@@ -79,7 +79,7 @@ Module.register("calendar", {
// Define required scripts.
getScripts: function () {
- return ["moment.js"];
+ return ["calendarutils.js", "moment.js"];
},
// Define required translations.
@@ -108,7 +108,7 @@ Module.register("calendar", {
}
// Set locale.
- moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
+ moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
// clear data holder before start
this.calendarData = {};
@@ -337,7 +337,8 @@ Module.register("calendar", {
}
}
- titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
+ const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
+ titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
const titleClass = this.titleClassForUrl(event.url);
@@ -362,7 +363,7 @@ Module.register("calendar", {
// Add endDate to dataheaders if showEnd is enabled
if (this.config.showEnd) {
- timeWrapper.innerHTML += ` - ${this.capFirst(moment(event.endDate, "x").format("LT"))}`;
+ timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
}
eventWrapper.appendChild(timeWrapper);
@@ -378,20 +379,20 @@ Module.register("calendar", {
if (this.config.timeFormat === "absolute") {
// Use dateFormat
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
// Add end time if showEnd
if (this.config.showEnd) {
timeWrapper.innerHTML += "-";
- timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
+ timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
}
// For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
event.endDate -= ONE_SECOND;
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
} else if (this.config.getRelative > 0 && event.startDate < now) {
// Ongoing and getRelative is set
- timeWrapper.innerHTML = this.capFirst(
+ timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
@@ -399,19 +400,19 @@ Module.register("calendar", {
);
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
// Within urgency days
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
}
if (event.fullDayEvent && this.config.nextDaysRelative) {
// Full days events within the next two days
if (event.today) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.yesterday) {
- timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
- timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
}
@@ -420,9 +421,9 @@ Module.register("calendar", {
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
// Use relative time
if (!this.config.hideTime && !event.fullDayEvent) {
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
} else {
- timeWrapper.innerHTML = this.capFirst(
+ timeWrapper.innerHTML = CalendarUtils.capFirst(
moment(event.startDate, "x").calendar(null, {
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
nextDay: `[${this.translate("TOMORROW")}]`,
@@ -434,27 +435,27 @@ Module.register("calendar", {
if (event.fullDayEvent) {
// Full days events within the next two days
if (event.today) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.dayBeforeYesterday) {
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
- timeWrapper.innerHTML = this.capFirst(this.translate("DAYBEFOREYESTERDAY"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
}
} else if (event.yesterday) {
- timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
- timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
- timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
+ timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
- timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
+ timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
}
} else {
// Ongoing event
- timeWrapper.innerHTML = this.capFirst(
+ timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
@@ -503,7 +504,9 @@ Module.register("calendar", {
const descCell = document.createElement("td");
descCell.className = "location";
descCell.colSpan = "2";
- descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
+
+ const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
+ descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
locationRow.appendChild(descCell);
wrapper.appendChild(locationRow);
@@ -519,28 +522,6 @@ Module.register("calendar", {
return wrapper;
},
- /**
- * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
- * corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
- * it will a localeSpecification object with the system locale time format.
- *
- * @param {number} timeFormat Specifies either 12 or 24 hour time format
- * @returns {moment.LocaleSpecification} formatted time
- */
- getLocaleSpecification: function (timeFormat) {
- switch (timeFormat) {
- case 12: {
- return { longDateFormat: { LT: "h:mm A" } };
- }
- case 24: {
- return { longDateFormat: { LT: "HH:mm" } };
- }
- default: {
- return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
- }
- }
- },
-
/**
* Checks if this config contains the calendar url.
*
@@ -870,98 +851,6 @@ Module.register("calendar", {
return !!this.getCalendarProperty(url, property, undefined);
},
- /**
- * Shortens a string if it's longer than maxLength and add a ellipsis to the end
- *
- * @param {string} string Text string to shorten
- * @param {number} maxLength The max length of the string
- * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
- * @param {number} maxTitleLines The max number of vertical lines before cutting event title
- * @returns {string} The shortened string
- */
- shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
- if (typeof string !== "string") {
- return "";
- }
-
- if (wrapEvents === true) {
- const words = string.split(" ");
- let temp = "";
- let currentLine = "";
- let line = 0;
-
- for (let i = 0; i < words.length; i++) {
- const word = words[i];
- if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
- // max - 1 to account for a space
- currentLine += `${word} `;
- } else {
- line++;
- if (line > maxTitleLines - 1) {
- if (i < words.length) {
- currentLine += "…";
- }
- break;
- }
-
- if (currentLine.length > 0) {
- temp += `${currentLine}
${word} `;
- } else {
- temp += `${word}
`;
- }
- currentLine = "";
- }
- }
-
- return (temp + currentLine).trim();
- } else {
- if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
- return `${string.trim().slice(0, maxLength)}…`;
- } else {
- return string.trim();
- }
- }
- },
-
- /**
- * Capitalize the first letter of a string
- *
- * @param {string} string The string to capitalize
- * @returns {string} The capitalized string
- */
- capFirst: function (string) {
- return string.charAt(0).toUpperCase() + string.slice(1);
- },
-
- /**
- * Transforms the title of an event for usage.
- * Replaces parts of the text as defined in config.titleReplace.
- * Shortens title based on config.maxTitleLength and config.wrapEvents
- *
- * @param {string} title The title to transform.
- * @param {object} titleReplace Pairs of strings to be replaced in the title
- * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
- * @param {number} maxTitleLength The max length of the string
- * @param {number} maxTitleLines The max number of vertical lines before cutting event title
- * @returns {string} The transformed title.
- */
- titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
- for (let needle in titleReplace) {
- const replacement = titleReplace[needle];
-
- const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
- if (regParts) {
- // the parsed pattern is a regexp.
- needle = new RegExp(regParts[1], regParts[2]);
- }
-
- title = title.replace(needle, replacement);
- }
-
- title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
- return title;
- },
-
/**
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js
index 00688ee2..3c7629e1 100644
--- a/modules/default/calendar/calendarfetcher.js
+++ b/modules/default/calendar/calendarfetcher.js
@@ -11,7 +11,7 @@ const ical = require("node-ical");
const fetch = require("fetch");
const Log = require("logger");
const NodeHelper = require("node_helper");
-const CalendarUtils = require("./calendarutils");
+const CalendarFetcherUtils = require("./calendarfetcherutils");
/**
*
@@ -72,7 +72,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try {
data = ical.parseICS(responseData);
Log.debug(`parsed data=${JSON.stringify(data)}`);
- events = CalendarUtils.filterEvents(data, {
+ events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
maximumEntries,
diff --git a/modules/default/calendar/calendarfetcherutils.js b/modules/default/calendar/calendarfetcherutils.js
new file mode 100644
index 00000000..e47b6bba
--- /dev/null
+++ b/modules/default/calendar/calendarfetcherutils.js
@@ -0,0 +1,613 @@
+/* MagicMirror²
+ * Calendar Fetcher Util Methods
+ *
+ * By Michael Teeuw https://michaelteeuw.nl
+ * MIT Licensed.
+ */
+
+/**
+ * @external Moment
+ */
+const path = require("path");
+const moment = require("moment");
+const zoneTable = require(path.join(__dirname, "windowsZones.json"));
+const Log = require("../../../js/logger");
+
+const CalendarFetcherUtils = {
+ /**
+ * Calculate the time correction, either dst/std or full day in cases where
+ * utc time is day before plus offset
+ *
+ * @param {object} event the event which needs adjustment
+ * @param {Date} date the date on which this event happens
+ * @returns {number} the necessary adjustment in hours
+ */
+ calculateTimezoneAdjustment: function (event, date) {
+ let adjustHours = 0;
+ // if a timezone was specified
+ if (!event.start.tz) {
+ Log.debug(" if no tz, guess based on now");
+ event.start.tz = moment.tz.guess();
+ }
+ Log.debug(`initial tz=${event.start.tz}`);
+
+ // if there is a start date specified
+ if (event.start.tz) {
+ // if this is a windows timezone
+ if (event.start.tz.includes(" ")) {
+ // use the lookup table to get theIANA name as moment and date don't know MS timezones
+ let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz);
+ Log.debug(`corrected TZ=${tz}`);
+ // watch out for unregistered windows timezone names
+ // if we had a successful lookup
+ if (tz) {
+ // change the timezone to the IANA name
+ event.start.tz = tz;
+ // Log.debug("corrected timezone="+event.start.tz)
+ }
+ }
+ Log.debug(`corrected tz=${event.start.tz}`);
+ let current_offset = 0; // offset from TZ string or calculated
+ let mm = 0; // date with tz or offset
+ let start_offset = 0; // utc offset of created with tz
+ // if there is still an offset, lookup failed, use it
+ if (event.start.tz.startsWith("(")) {
+ const regex = /[+|-]\d*:\d*/;
+ const start_offsetString = event.start.tz.match(regex).toString().split(":");
+ let start_offset = parseInt(start_offsetString[0]);
+ start_offset *= event.start.tz[1] === "-" ? -1 : 1;
+ adjustHours = start_offset;
+ Log.debug(`defined offset=${start_offset} hours`);
+ current_offset = start_offset;
+ event.start.tz = "";
+ Log.debug(`ical offset=${current_offset} date=${date}`);
+ mm = moment(date);
+ let x = parseInt(moment(new Date()).utcOffset());
+ Log.debug(`net mins=${current_offset * 60 - x}`);
+
+ mm = mm.add(x - current_offset * 60, "minutes");
+ adjustHours = (current_offset * 60 - x) / 60;
+ event.start = mm.toDate();
+ Log.debug(`adjusted date=${event.start}`);
+ } else {
+ // get the start time in that timezone
+ let es = moment(event.start);
+ // check for start date prior to start of daylight changing date
+ if (es.format("YYYY") < 2007) {
+ es.set("year", 2013); // if so, use a closer date
+ }
+ Log.debug(`start date/time=${es.toDate()}`);
+ start_offset = moment.tz(es, event.start.tz).utcOffset();
+ Log.debug(`start offset=${start_offset}`);
+
+ Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
+
+ // get the specified date in that timezone
+ mm = moment.tz(moment(date), event.start.tz);
+ Log.debug(`event date=${mm.toDate()}`);
+ current_offset = mm.utcOffset();
+ }
+ Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
+
+ // if the offset is greater than 0, east of london
+ if (current_offset !== start_offset) {
+ // big offset
+ Log.debug("offset");
+ let h = parseInt(mm.format("H"));
+ // check if the event time is less than the offset
+ if (h > 0 && h < Math.abs(current_offset) / 60) {
+ // if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
+ // we need to fix that
+ //adjustHours = 24;
+ // Log.debug("adjusting date")
+ }
+ //-300 > -240
+ //if (Math.abs(current_offset) > Math.abs(start_offset)){
+ if (current_offset > start_offset) {
+ adjustHours -= 1;
+ Log.debug("adjust down 1 hour dst change");
+ //} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
+ } else if (current_offset < start_offset) {
+ adjustHours += 1;
+ Log.debug("adjust up 1 hour dst change");
+ }
+ }
+ }
+ Log.debug(`adjustHours=${adjustHours}`);
+ return adjustHours;
+ },
+
+ /**
+ * Filter the events from ical according to the given config
+ *
+ * @param {object} data the calendar data from ical
+ * @param {object} config The configuration object
+ * @returns {string[]} the filtered events
+ */
+ filterEvents: function (data, config) {
+ const newEvents = [];
+
+ // limitFunction doesn't do much limiting, see comment re: the dates
+ // array in rrule section below as to why we need to do the filtering
+ // ourselves
+ const limitFunction = function (date, i) {
+ return true;
+ };
+
+ const eventDate = function (event, time) {
+ return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
+ };
+
+ Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
+ Object.entries(data).forEach(([key, event]) => {
+ Log.debug("Processing entry...");
+ const now = new Date();
+ const today = moment().startOf("day").toDate();
+ const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
+ let past = today;
+
+ if (config.includePastEvents) {
+ past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
+ }
+
+ // FIXME: Ugly fix to solve the facebook birthday issue.
+ // Otherwise, the recurring events only show the birthday for next year.
+ let isFacebookBirthday = false;
+ if (typeof event.uid !== "undefined") {
+ if (event.uid.indexOf("@facebook.com") !== -1) {
+ isFacebookBirthday = true;
+ }
+ }
+
+ if (event.type === "VEVENT") {
+ Log.debug(`Event:\n${JSON.stringify(event)}`);
+ let startDate = eventDate(event, "start");
+ let endDate;
+
+ if (typeof event.end !== "undefined") {
+ endDate = eventDate(event, "end");
+ } else if (typeof event.duration !== "undefined") {
+ endDate = startDate.clone().add(moment.duration(event.duration));
+ } else {
+ if (!isFacebookBirthday) {
+ // make copy of start date, separate storage area
+ endDate = moment(startDate.format("x"), "x");
+ } else {
+ endDate = moment(startDate).add(1, "days");
+ }
+ }
+
+ Log.debug(`start: ${startDate.toDate()}`);
+ Log.debug(`end:: ${endDate.toDate()}`);
+
+ // Calculate the duration of the event for use with recurring events.
+ let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
+ Log.debug(`duration: ${duration}`);
+
+ // FIXME: Since the parsed json object from node-ical comes with time information
+ // this check could be removed (?)
+ if (event.start.length === 8) {
+ startDate = startDate.startOf("day");
+ }
+
+ const title = CalendarFetcherUtils.getTitleFromEvent(event);
+ Log.debug(`title: ${title}`);
+
+ let excluded = false,
+ dateFilter = null;
+
+ for (let f in config.excludedEvents) {
+ let filter = config.excludedEvents[f],
+ testTitle = title.toLowerCase(),
+ until = null,
+ useRegex = false,
+ regexFlags = "g";
+
+ if (filter instanceof Object) {
+ if (typeof filter.until !== "undefined") {
+ until = filter.until;
+ }
+
+ if (typeof filter.regex !== "undefined") {
+ useRegex = filter.regex;
+ }
+
+ // If additional advanced filtering is added in, this section
+ // must remain last as we overwrite the filter object with the
+ // filterBy string
+ if (filter.caseSensitive) {
+ filter = filter.filterBy;
+ testTitle = title;
+ } else if (useRegex) {
+ filter = filter.filterBy;
+ testTitle = title;
+ regexFlags += "i";
+ } else {
+ filter = filter.filterBy.toLowerCase();
+ }
+ } else {
+ filter = filter.toLowerCase();
+ }
+
+ if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
+ if (until) {
+ dateFilter = until;
+ } else {
+ excluded = true;
+ }
+ break;
+ }
+ }
+
+ if (excluded) {
+ return;
+ }
+
+ const location = event.location || false;
+ const geo = event.geo || false;
+ const description = event.description || false;
+
+ if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
+ const rule = event.rrule;
+ let addedEvents = 0;
+
+ const pastMoment = moment(past);
+ const futureMoment = moment(future);
+
+ // can cause problems with e.g. birthdays before 1900
+ if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
+ rule.origOptions.dtstart.setYear(1900);
+ rule.options.dtstart.setYear(1900);
+ }
+
+ // For recurring events, get the set of start dates that fall within the range
+ // of dates we're looking for.
+ // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
+ let pastLocal = 0;
+ let futureLocal = 0;
+ if (CalendarFetcherUtils.isFullDayEvent(event)) {
+ Log.debug("fullday");
+ // if full day event, only use the date part of the ranges
+ pastLocal = pastMoment.toDate();
+ futureLocal = futureMoment.toDate();
+
+ Log.debug(`pastLocal: ${pastLocal}`);
+ Log.debug(`futureLocal: ${futureLocal}`);
+ } else {
+ // if we want past events
+ if (config.includePastEvents) {
+ // use the calculated past time for the between from
+ pastLocal = pastMoment.toDate();
+ } else {
+ // otherwise use NOW.. cause we shouldn't use any before now
+ pastLocal = moment().toDate(); //now
+ }
+ futureLocal = futureMoment.toDate(); // future
+ }
+ Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
+ const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
+ Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
+ // The "dates" array contains the set of dates within our desired date range range that are valid
+ // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
+ // had its date changed from outside the range to inside the range. For the time being,
+ // we'll handle this by adding *all* recurrence entries into the set of dates that we check,
+ // because the logic below will filter out any recurrences that don't actually belong within
+ // our display range.
+ // Would be great if there was a better way to handle this.
+ Log.debug(`event.recurrences: ${event.recurrences}`);
+ if (event.recurrences !== undefined) {
+ for (let r in event.recurrences) {
+ // Only add dates that weren't already in the range we added from the rrule so that
+ // we don"t double-add those events.
+ if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
+ dates.push(new Date(r));
+ }
+ }
+ }
+ // Loop through the set of date entries to see which recurrences should be added to our event list.
+ for (let d in dates) {
+ let date = dates[d];
+ // Remove the time information of each date by using its substring, using the following method:
+ // .toISOString().substring(0,10).
+ // since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
+ // (see https://momentjs.com/docs/#/displaying/as-iso-string/).
+ const dateKey = date.toISOString().substring(0, 10);
+ let curEvent = event;
+ let showRecurrence = true;
+
+ // Get the offset of today where we are processing
+ // This will be the correction, we need to apply.
+ let nowOffset = new Date().getTimezoneOffset();
+ // For full day events, the time might be off from RRULE/Luxon problem
+ // Get time zone offset of the rule calculated event
+ let dateoffset = date.getTimezoneOffset();
+
+ // Reduce the time by the following offset.
+ Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
+
+ let dh = moment(date).format("HH");
+ Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
+
+ if (CalendarFetcherUtils.isFullDayEvent(event)) {
+ Log.debug("Fullday");
+ // If the offset is negative (east of GMT), where the problem is
+ if (dateoffset < 0) {
+ if (dh < Math.abs(dateoffset / 60)) {
+ // if the rrule byweekday WAS explicitly set , correct it
+ // reduce the time by the offset
+ if (curEvent.rrule.origOptions.byweekday !== undefined) {
+ // Apply the correction to the date/time to get it UTC relative
+ date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
+ }
+ // the duration was calculated way back at the top before we could correct the start time..
+ // fix it for this event entry
+ //duration = 24 * 60 * 60 * 1000;
+ Log.debug(`new recurring date1 fulldate is ${date}`);
+ }
+ } else {
+ // if the timezones are the same, correct date if needed
+ //if (event.start.tz === moment.tz.guess()) {
+ // if the date hour is less than the offset
+ if (24 - dh <= Math.abs(dateoffset / 60)) {
+ // if the rrule byweekday WAS explicitly set , correct it
+ if (curEvent.rrule.origOptions.byweekday !== undefined) {
+ // apply the correction to the date/time back to right day
+ date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
+ }
+ // the duration was calculated way back at the top before we could correct the start time..
+ // fix it for this event entry
+ //duration = 24 * 60 * 60 * 1000;
+ Log.debug(`new recurring date2 fulldate is ${date}`);
+ }
+ //}
+ }
+ } else {
+ // not full day, but luxon can still screw up the date on the rule processing
+ // we need to correct the date to get back to the right event for
+ if (dateoffset < 0) {
+ // if the date hour is less than the offset
+ if (dh <= Math.abs(dateoffset / 60)) {
+ // if the rrule byweekday WAS explicitly set , correct it
+ if (curEvent.rrule.origOptions.byweekday !== undefined) {
+ // Reduce the time by t:
+ // Apply the correction to the date/time to get it UTC relative
+ date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
+ }
+ // the duration was calculated way back at the top before we could correct the start time..
+ // fix it for this event entry
+ //duration = 24 * 60 * 60 * 1000;
+ Log.debug(`new recurring date1 is ${date}`);
+ }
+ } else {
+ // if the timezones are the same, correct date if needed
+ //if (event.start.tz === moment.tz.guess()) {
+ // if the date hour is less than the offset
+ if (24 - dh <= Math.abs(dateoffset / 60)) {
+ // if the rrule byweekday WAS explicitly set , correct it
+ if (curEvent.rrule.origOptions.byweekday !== undefined) {
+ // apply the correction to the date/time back to right day
+ date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
+ }
+ // the duration was calculated way back at the top before we could correct the start time..
+ // fix it for this event entry
+ //duration = 24 * 60 * 60 * 1000;
+ Log.debug(`new recurring date2 is ${date}`);
+ }
+ //}
+ }
+ }
+ startDate = moment(date);
+ Log.debug(`Corrected startDate: ${startDate.toDate()}`);
+
+ let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
+
+ // For each date that we're checking, it's possible that there is a recurrence override for that one day.
+ if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
+ // We found an override, so for this recurrence, use a potentially different title, start date, and duration.
+ curEvent = curEvent.recurrences[dateKey];
+ startDate = moment(curEvent.start);
+ duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
+ }
+ // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
+ else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
+ // This date is an exception date, which means we should skip it in the recurrence pattern.
+ showRecurrence = false;
+ }
+ Log.debug(`duration: ${duration}`);
+
+ endDate = moment(parseInt(startDate.format("x")) + duration, "x");
+ if (startDate.format("x") === endDate.format("x")) {
+ endDate = endDate.endOf("day");
+ }
+
+ const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
+
+ // If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
+ // it to the event list.
+ if (endDate.isBefore(past) || startDate.isAfter(future)) {
+ showRecurrence = false;
+ }
+
+ if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
+ showRecurrence = false;
+ }
+
+ if (showRecurrence === true) {
+ Log.debug(`saving event: ${description}`);
+ addedEvents++;
+ newEvents.push({
+ title: recurrenceTitle,
+ startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
+ endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
+ fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
+ recurringEvent: true,
+ class: event.class,
+ firstYear: event.start.getFullYear(),
+ location: location,
+ geo: geo,
+ description: description
+ });
+ }
+ }
+ // End recurring event parsing.
+ } else {
+ // Single event.
+ const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
+ // Log.debug("full day event")
+
+ if (config.includePastEvents) {
+ // Past event is too far in the past, so skip.
+ if (endDate < past) {
+ return;
+ }
+ } else {
+ // It's not a fullday event, and it is in the past, so skip.
+ if (!fullDayEvent && endDate < new Date()) {
+ return;
+ }
+
+ // It's a fullday event, and it is before today, So skip.
+ if (fullDayEvent && endDate <= today) {
+ return;
+ }
+ }
+
+ // It exceeds the maximumNumberOfDays limit, so skip.
+ if (startDate > future) {
+ return;
+ }
+
+ if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
+ return;
+ }
+
+ // if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
+ if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
+ endDate = endDate.endOf("day");
+ }
+ // get correction for date saving and dst change between now and then
+ let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
+ // Every thing is good. Add it to the list.
+ newEvents.push({
+ title: title,
+ startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
+ endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
+ fullDayEvent: fullDayEvent,
+ class: event.class,
+ location: location,
+ geo: geo,
+ description: description
+ });
+ }
+ }
+ });
+
+ newEvents.sort(function (a, b) {
+ return a.startDate - b.startDate;
+ });
+
+ return newEvents;
+ },
+
+ /**
+ * Lookup iana tz from windows
+ *
+ * @param {string} msTZName the timezone name to lookup
+ * @returns {string|null} the iana name or null of none is found
+ */
+ getIanaTZFromMS: function (msTZName) {
+ // Get hash entry
+ const he = zoneTable[msTZName];
+ // If found return iana name, else null
+ return he ? he.iana[0] : null;
+ },
+
+ /**
+ * Gets the title from the event.
+ *
+ * @param {object} event The event object to check.
+ * @returns {string} The title of the event, or "Event" if no title is found.
+ */
+ getTitleFromEvent: function (event) {
+ let title = "Event";
+ if (event.summary) {
+ title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
+ } else if (event.description) {
+ title = event.description;
+ }
+
+ return title;
+ },
+
+ /**
+ * Checks if an event is a fullday event.
+ *
+ * @param {object} event The event object to check.
+ * @returns {boolean} True if the event is a fullday event, false otherwise
+ */
+ isFullDayEvent: function (event) {
+ if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
+ return true;
+ }
+
+ const start = event.start || 0;
+ const startDate = new Date(start);
+ const end = event.end || 0;
+ if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
+ // Is 24 hours, and starts on the middle of the night.
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Determines if the user defined time filter should apply
+ *
+ * @param {Date} now Date object using previously created object for consistency
+ * @param {Moment} endDate Moment object representing the event end date
+ * @param {string} filter The time to subtract from the end date to determine if an event should be shown
+ * @returns {boolean} True if the event should be filtered out, false otherwise
+ */
+ timeFilterApplies: function (now, endDate, filter) {
+ if (filter) {
+ const until = filter.split(" "),
+ value = parseInt(until[0]),
+ increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
+ filterUntil = moment(endDate.format()).subtract(value, increment);
+
+ return now < filterUntil.format("x");
+ }
+
+ return false;
+ },
+
+ /**
+ * Determines if the user defined title filter should apply
+ *
+ * @param {string} title the title of the event
+ * @param {string} filter the string to look for, can be a regex also
+ * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
+ * @param {string} regexFlags flags that should be applied to the regex
+ * @returns {boolean} True if the title should be filtered out, false otherwise
+ */
+ titleFilterApplies: function (title, filter, useRegex, regexFlags) {
+ if (useRegex) {
+ // Assume if leading slash, there is also trailing slash
+ if (filter[0] === "/") {
+ // Strip leading and trailing slashes
+ filter = filter.substr(1).slice(0, -1);
+ }
+
+ filter = new RegExp(filter, regexFlags);
+
+ return filter.test(title);
+ } else {
+ return title.includes(filter);
+ }
+ }
+};
+
+if (typeof module !== "undefined") {
+ module.exports = CalendarFetcherUtils;
+}
diff --git a/modules/default/calendar/calendarutils.js b/modules/default/calendar/calendarutils.js
index 64a7b9f4..601ccdf0 100644
--- a/modules/default/calendar/calendarutils.js
+++ b/modules/default/calendar/calendarutils.js
@@ -1,610 +1,117 @@
/* MagicMirror²
* Calendar Util Methods
*
- * By Michael Teeuw https://michaelteeuw.nl
+ * By Rejas
* MIT Licensed.
*/
-
-/**
- * @external Moment
- */
-const path = require("path");
-const moment = require("moment");
-const zoneTable = require(path.join(__dirname, "windowsZones.json"));
-const Log = require("../../../js/logger");
-
const CalendarUtils = {
/**
- * Calculate the time correction, either dst/std or full day in cases where
- * utc time is day before plus offset
+ * Capitalize the first letter of a string
*
- * @param {object} event the event which needs adjustement
- * @param {Date} date the date on which this event happens
- * @returns {number} the necessary adjustment in hours
+ * @param {string} string The string to capitalize
+ * @returns {string} The capitalized string
*/
- calculateTimezoneAdjustment: function (event, date) {
- let adjustHours = 0;
- // if a timezone was specified
- if (!event.start.tz) {
- Log.debug(" if no tz, guess based on now");
- event.start.tz = moment.tz.guess();
- }
- Log.debug(`initial tz=${event.start.tz}`);
-
- // if there is a start date specified
- if (event.start.tz) {
- // if this is a windows timezone
- if (event.start.tz.includes(" ")) {
- // use the lookup table to get theIANA name as moment and date don't know MS timezones
- let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
- Log.debug(`corrected TZ=${tz}`);
- // watch out for unregistered windows timezone names
- // if we had a successful lookup
- if (tz) {
- // change the timezone to the IANA name
- event.start.tz = tz;
- // Log.debug("corrected timezone="+event.start.tz)
- }
- }
- Log.debug(`corrected tz=${event.start.tz}`);
- let current_offset = 0; // offset from TZ string or calculated
- let mm = 0; // date with tz or offset
- let start_offset = 0; // utc offset of created with tz
- // if there is still an offset, lookup failed, use it
- if (event.start.tz.startsWith("(")) {
- const regex = /[+|-]\d*:\d*/;
- const start_offsetString = event.start.tz.match(regex).toString().split(":");
- let start_offset = parseInt(start_offsetString[0]);
- start_offset *= event.start.tz[1] === "-" ? -1 : 1;
- adjustHours = start_offset;
- Log.debug(`defined offset=${start_offset} hours`);
- current_offset = start_offset;
- event.start.tz = "";
- Log.debug(`ical offset=${current_offset} date=${date}`);
- mm = moment(date);
- let x = parseInt(moment(new Date()).utcOffset());
- Log.debug(`net mins=${current_offset * 60 - x}`);
-
- mm = mm.add(x - current_offset * 60, "minutes");
- adjustHours = (current_offset * 60 - x) / 60;
- event.start = mm.toDate();
- Log.debug(`adjusted date=${event.start}`);
- } else {
- // get the start time in that timezone
- let es = moment(event.start);
- // check for start date prior to start of daylight changing date
- if (es.format("YYYY") < 2007) {
- es.set("year", 2013); // if so, use a closer date
- }
- Log.debug(`start date/time=${es.toDate()}`);
- start_offset = moment.tz(es, event.start.tz).utcOffset();
- Log.debug(`start offset=${start_offset}`);
-
- Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
-
- // get the specified date in that timezone
- mm = moment.tz(moment(date), event.start.tz);
- Log.debug(`event date=${mm.toDate()}`);
- current_offset = mm.utcOffset();
- }
- Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
-
- // if the offset is greater than 0, east of london
- if (current_offset !== start_offset) {
- // big offset
- Log.debug("offset");
- let h = parseInt(mm.format("H"));
- // check if the event time is less than the offset
- if (h > 0 && h < Math.abs(current_offset) / 60) {
- // if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
- // we need to fix that
- //adjustHours = 24;
- // Log.debug("adjusting date")
- }
- //-300 > -240
- //if (Math.abs(current_offset) > Math.abs(start_offset)){
- if (current_offset > start_offset) {
- adjustHours -= 1;
- Log.debug("adjust down 1 hour dst change");
- //} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
- } else if (current_offset < start_offset) {
- adjustHours += 1;
- Log.debug("adjust up 1 hour dst change");
- }
- }
- }
- Log.debug(`adjustHours=${adjustHours}`);
- return adjustHours;
+ capFirst: function (string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
},
/**
- * Filter the events from ical according to the given config
+ * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
+ * corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input)
+ * it will a localeSpecification object with the system locale time format.
*
- * @param {object} data the calendar data from ical
- * @param {object} config The configuration object
- * @returns {string[]} the filtered events
+ * @param {number} timeFormat Specifies either 12 or 24-hour time format
+ * @returns {moment.LocaleSpecification} formatted time
*/
- filterEvents: function (data, config) {
- const newEvents = [];
-
- // limitFunction doesn't do much limiting, see comment re: the dates
- // array in rrule section below as to why we need to do the filtering
- // ourselves
- const limitFunction = function (date, i) {
- return true;
- };
-
- const eventDate = function (event, time) {
- return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
- };
-
- Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
- Object.entries(data).forEach(([key, event]) => {
- Log.debug("Processing entry...");
- const now = new Date();
- const today = moment().startOf("day").toDate();
- const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
- let past = today;
-
- if (config.includePastEvents) {
- past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
+ getLocaleSpecification: function (timeFormat) {
+ switch (timeFormat) {
+ case 12: {
+ return { longDateFormat: { LT: "h:mm A" } };
}
-
- // FIXME: Ugly fix to solve the facebook birthday issue.
- // Otherwise, the recurring events only show the birthday for next year.
- let isFacebookBirthday = false;
- if (typeof event.uid !== "undefined") {
- if (event.uid.indexOf("@facebook.com") !== -1) {
- isFacebookBirthday = true;
- }
+ case 24: {
+ return { longDateFormat: { LT: "HH:mm" } };
}
+ default: {
+ return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
+ }
+ }
+ },
- if (event.type === "VEVENT") {
- Log.debug(`Event:\n${JSON.stringify(event)}`);
- let startDate = eventDate(event, "start");
- let endDate;
+ /**
+ * Shortens a string if it's longer than maxLength and add an ellipsis to the end
+ *
+ * @param {string} string Text string to shorten
+ * @param {number} maxLength The max length of the string
+ * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
+ * @param {number} maxTitleLines The max number of vertical lines before cutting event title
+ * @returns {string} The shortened string
+ */
+ shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
+ if (typeof string !== "string") {
+ return "";
+ }
- if (typeof event.end !== "undefined") {
- endDate = eventDate(event, "end");
- } else if (typeof event.duration !== "undefined") {
- endDate = startDate.clone().add(moment.duration(event.duration));
+ if (wrapEvents === true) {
+ const words = string.split(" ");
+ let temp = "";
+ let currentLine = "";
+ let line = 0;
+
+ for (let i = 0; i < words.length; i++) {
+ const word = words[i];
+ if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
+ // max - 1 to account for a space
+ currentLine += `${word} `;
} else {
- if (!isFacebookBirthday) {
- // make copy of start date, separate storage area
- endDate = moment(startDate.format("x"), "x");
- } else {
- endDate = moment(startDate).add(1, "days");
- }
- }
-
- Log.debug(`start: ${startDate.toDate()}`);
- Log.debug(`end:: ${endDate.toDate()}`);
-
- // Calculate the duration of the event for use with recurring events.
- let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
- Log.debug(`duration: ${duration}`);
-
- // FIXME: Since the parsed json object from node-ical comes with time information
- // this check could be removed (?)
- if (event.start.length === 8) {
- startDate = startDate.startOf("day");
- }
-
- const title = CalendarUtils.getTitleFromEvent(event);
- Log.debug(`title: ${title}`);
-
- let excluded = false,
- dateFilter = null;
-
- for (let f in config.excludedEvents) {
- let filter = config.excludedEvents[f],
- testTitle = title.toLowerCase(),
- until = null,
- useRegex = false,
- regexFlags = "g";
-
- if (filter instanceof Object) {
- if (typeof filter.until !== "undefined") {
- until = filter.until;
- }
-
- if (typeof filter.regex !== "undefined") {
- useRegex = filter.regex;
- }
-
- // If additional advanced filtering is added in, this section
- // must remain last as we overwrite the filter object with the
- // filterBy string
- if (filter.caseSensitive) {
- filter = filter.filterBy;
- testTitle = title;
- } else if (useRegex) {
- filter = filter.filterBy;
- testTitle = title;
- regexFlags += "i";
- } else {
- filter = filter.filterBy.toLowerCase();
- }
- } else {
- filter = filter.toLowerCase();
- }
-
- if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
- if (until) {
- dateFilter = until;
- } else {
- excluded = true;
+ line++;
+ if (line > maxTitleLines - 1) {
+ if (i < words.length) {
+ currentLine += "…";
}
break;
}
- }
- if (excluded) {
- return;
- }
-
- const location = event.location || false;
- const geo = event.geo || false;
- const description = event.description || false;
-
- if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
- const rule = event.rrule;
- let addedEvents = 0;
-
- const pastMoment = moment(past);
- const futureMoment = moment(future);
-
- // can cause problems with e.g. birthdays before 1900
- if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
- rule.origOptions.dtstart.setYear(1900);
- rule.options.dtstart.setYear(1900);
- }
-
- // For recurring events, get the set of start dates that fall within the range
- // of dates we're looking for.
- // kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
- let pastLocal = 0;
- let futureLocal = 0;
- if (CalendarUtils.isFullDayEvent(event)) {
- Log.debug("fullday");
- // if full day event, only use the date part of the ranges
- pastLocal = pastMoment.toDate();
- futureLocal = futureMoment.toDate();
-
- Log.debug(`pastLocal: ${pastLocal}`);
- Log.debug(`futureLocal: ${futureLocal}`);
+ if (currentLine.length > 0) {
+ temp += `${currentLine}
${word} `;
} else {
- // if we want past events
- if (config.includePastEvents) {
- // use the calculated past time for the between from
- pastLocal = pastMoment.toDate();
- } else {
- // otherwise use NOW.. cause we shouldn't use any before now
- pastLocal = moment().toDate(); //now
- }
- futureLocal = futureMoment.toDate(); // future
+ temp += `${word}
`;
}
- Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
- const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
- Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
- // The "dates" array contains the set of dates within our desired date range range that are valid
- // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
- // had its date changed from outside the range to inside the range. For the time being,
- // we'll handle this by adding *all* recurrence entries into the set of dates that we check,
- // because the logic below will filter out any recurrences that don't actually belong within
- // our display range.
- // Would be great if there was a better way to handle this.
- Log.debug(`event.recurrences: ${event.recurrences}`);
- if (event.recurrences !== undefined) {
- for (let r in event.recurrences) {
- // Only add dates that weren't already in the range we added from the rrule so that
- // we don"t double-add those events.
- if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
- dates.push(new Date(r));
- }
- }
- }
- // Loop through the set of date entries to see which recurrences should be added to our event list.
- for (let d in dates) {
- let date = dates[d];
- // Remove the time information of each date by using its substring, using the following method:
- // .toISOString().substring(0,10).
- // since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
- // (see https://momentjs.com/docs/#/displaying/as-iso-string/).
- const dateKey = date.toISOString().substring(0, 10);
- let curEvent = event;
- let showRecurrence = true;
-
- // Get the offset of today where we are processing
- // This will be the correction, we need to apply.
- let nowOffset = new Date().getTimezoneOffset();
- // For full day events, the time might be off from RRULE/Luxon problem
- // Get time zone offset of the rule calculated event
- let dateoffset = date.getTimezoneOffset();
-
- // Reduce the time by the following offset.
- Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
-
- let dh = moment(date).format("HH");
- Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
-
- if (CalendarUtils.isFullDayEvent(event)) {
- Log.debug("Fullday");
- // If the offset is negative (east of GMT), where the problem is
- if (dateoffset < 0) {
- if (dh < Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- // reduce the time by the offset
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // Apply the correction to the date/time to get it UTC relative
- date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug(`new recurring date1 fulldate is ${date}`);
- }
- } else {
- // if the timezones are the same, correct date if needed
- //if (event.start.tz === moment.tz.guess()) {
- // if the date hour is less than the offset
- if (24 - dh <= Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // apply the correction to the date/time back to right day
- date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug(`new recurring date2 fulldate is ${date}`);
- }
- //}
- }
- } else {
- // not full day, but luxon can still screw up the date on the rule processing
- // we need to correct the date to get back to the right event for
- if (dateoffset < 0) {
- // if the date hour is less than the offset
- if (dh <= Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // Reduce the time by t:
- // Apply the correction to the date/time to get it UTC relative
- date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug(`new recurring date1 is ${date}`);
- }
- } else {
- // if the timezones are the same, correct date if needed
- //if (event.start.tz === moment.tz.guess()) {
- // if the date hour is less than the offset
- if (24 - dh <= Math.abs(dateoffset / 60)) {
- // if the rrule byweekday WAS explicitly set , correct it
- if (curEvent.rrule.origOptions.byweekday !== undefined) {
- // apply the correction to the date/time back to right day
- date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
- }
- // the duration was calculated way back at the top before we could correct the start time..
- // fix it for this event entry
- //duration = 24 * 60 * 60 * 1000;
- Log.debug(`new recurring date2 is ${date}`);
- }
- //}
- }
- }
- startDate = moment(date);
- Log.debug(`Corrected startDate: ${startDate.toDate()}`);
-
- let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
-
- // For each date that we're checking, it's possible that there is a recurrence override for that one day.
- if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
- // We found an override, so for this recurrence, use a potentially different title, start date, and duration.
- curEvent = curEvent.recurrences[dateKey];
- startDate = moment(curEvent.start);
- duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
- }
- // If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
- else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
- // This date is an exception date, which means we should skip it in the recurrence pattern.
- showRecurrence = false;
- }
- Log.debug(`duration: ${duration}`);
-
- endDate = moment(parseInt(startDate.format("x")) + duration, "x");
- if (startDate.format("x") === endDate.format("x")) {
- endDate = endDate.endOf("day");
- }
-
- const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
-
- // If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
- // it to the event list.
- if (endDate.isBefore(past) || startDate.isAfter(future)) {
- showRecurrence = false;
- }
-
- if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
- showRecurrence = false;
- }
-
- if (showRecurrence === true) {
- Log.debug(`saving event: ${description}`);
- addedEvents++;
- newEvents.push({
- title: recurrenceTitle,
- startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
- endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
- fullDayEvent: CalendarUtils.isFullDayEvent(event),
- recurringEvent: true,
- class: event.class,
- firstYear: event.start.getFullYear(),
- location: location,
- geo: geo,
- description: description
- });
- }
- }
- // End recurring event parsing.
- } else {
- // Single event.
- const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
- // Log.debug("full day event")
-
- if (config.includePastEvents) {
- // Past event is too far in the past, so skip.
- if (endDate < past) {
- return;
- }
- } else {
- // It's not a fullday event, and it is in the past, so skip.
- if (!fullDayEvent && endDate < new Date()) {
- return;
- }
-
- // It's a fullday event, and it is before today, So skip.
- if (fullDayEvent && endDate <= today) {
- return;
- }
- }
-
- // It exceeds the maximumNumberOfDays limit, so skip.
- if (startDate > future) {
- return;
- }
-
- if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
- return;
- }
-
- // if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
- if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
- endDate = endDate.endOf("day");
- }
- // get correction for date saving and dst change between now and then
- let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
- // Every thing is good. Add it to the list.
- newEvents.push({
- title: title,
- startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
- endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
- fullDayEvent: fullDayEvent,
- class: event.class,
- location: location,
- geo: geo,
- description: description
- });
+ currentLine = "";
}
}
- });
- newEvents.sort(function (a, b) {
- return a.startDate - b.startDate;
- });
-
- return newEvents;
- },
-
- /**
- * Lookup iana tz from windows
- *
- * @param {string} msTZName the timezone name to lookup
- * @returns {string|null} the iana name or null of none is found
- */
- getIanaTZFromMS: function (msTZName) {
- // Get hash entry
- const he = zoneTable[msTZName];
- // If found return iana name, else null
- return he ? he.iana[0] : null;
- },
-
- /**
- * Gets the title from the event.
- *
- * @param {object} event The event object to check.
- * @returns {string} The title of the event, or "Event" if no title is found.
- */
- getTitleFromEvent: function (event) {
- let title = "Event";
- if (event.summary) {
- title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
- } else if (event.description) {
- title = event.description;
- }
-
- return title;
- },
-
- /**
- * Checks if an event is a fullday event.
- *
- * @param {object} event The event object to check.
- * @returns {boolean} True if the event is a fullday event, false otherwise
- */
- isFullDayEvent: function (event) {
- if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
- return true;
- }
-
- const start = event.start || 0;
- const startDate = new Date(start);
- const end = event.end || 0;
- if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
- // Is 24 hours, and starts on the middle of the night.
- return true;
- }
-
- return false;
- },
-
- /**
- * Determines if the user defined time filter should apply
- *
- * @param {Date} now Date object using previously created object for consistency
- * @param {Moment} endDate Moment object representing the event end date
- * @param {string} filter The time to subtract from the end date to determine if an event should be shown
- * @returns {boolean} True if the event should be filtered out, false otherwise
- */
- timeFilterApplies: function (now, endDate, filter) {
- if (filter) {
- const until = filter.split(" "),
- value = parseInt(until[0]),
- increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
- filterUntil = moment(endDate.format()).subtract(value, increment);
-
- return now < filterUntil.format("x");
- }
-
- return false;
- },
-
- /**
- * Determines if the user defined title filter should apply
- *
- * @param {string} title the title of the event
- * @param {string} filter the string to look for, can be a regex also
- * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
- * @param {string} regexFlags flags that should be applied to the regex
- * @returns {boolean} True if the title should be filtered out, false otherwise
- */
- titleFilterApplies: function (title, filter, useRegex, regexFlags) {
- if (useRegex) {
- // Assume if leading slash, there is also trailing slash
- if (filter[0] === "/") {
- // Strip leading and trailing slashes
- filter = filter.substr(1).slice(0, -1);
- }
-
- filter = new RegExp(filter, regexFlags);
-
- return filter.test(title);
+ return (temp + currentLine).trim();
} else {
- return title.includes(filter);
+ if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
+ return `${string.trim().slice(0, maxLength)}…`;
+ } else {
+ return string.trim();
+ }
}
+ },
+
+ /**
+ * Transforms the title of an event for usage.
+ * Replaces parts of the text as defined in config.titleReplace.
+ * Shortens title based on config.maxTitleLength and config.wrapEvents
+ *
+ * @param {string} title The title to transform.
+ * @param {object} titleReplace Pairs of strings to be replaced in the title
+ * @returns {string} The transformed title.
+ */
+ titleTransform: function (title, titleReplace) {
+ for (let needle in titleReplace) {
+ const replacement = titleReplace[needle];
+
+ const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
+ if (regParts) {
+ // the parsed pattern is a regexp.
+ needle = new RegExp(regParts[1], regParts[2]);
+ }
+
+ title = title.replace(needle, replacement);
+ }
+ return title;
}
};
diff --git a/tests/e2e/modules/compliments_spec.js b/tests/e2e/modules/compliments_spec.js
index 52d232e5..c4fcf686 100644
--- a/tests/e2e/modules/compliments_spec.js
+++ b/tests/e2e/modules/compliments_spec.js
@@ -25,7 +25,7 @@ describe("Compliments module", () => {
await helpers.getDocument();
});
- it("Show anytime because if configure empty parts of day compliments and set anytime compliments", async () => {
+ it("shows anytime because if configure empty parts of day compliments and set anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});
@@ -36,7 +36,7 @@ describe("Compliments module", () => {
await helpers.getDocument();
});
- it("Show anytime compliments", async () => {
+ it("shows anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});
diff --git a/tests/e2e/modules_empty_spec.js b/tests/e2e/modules_empty_spec.js
index ddd08e82..dfe6c081 100644
--- a/tests/e2e/modules_empty_spec.js
+++ b/tests/e2e/modules_empty_spec.js
@@ -9,13 +9,13 @@ describe("Check configuration without modules", () => {
await helpers.stopApplication();
});
- it("Show the message MagicMirror² title", async () => {
+ it("shows the message MagicMirror² title", async () => {
const elem = await helpers.waitForElement("#module_1_helloworld .module-content");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("MagicMirror²");
});
- it("Show the url of michael's website", async () => {
+ it("shows the url of michael's website", async () => {
const elem = await helpers.waitForElement("#module_5_helloworld .module-content");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("www.michaelteeuw.nl");
diff --git a/tests/electron/modules/compliments_spec.js b/tests/electron/modules/compliments_spec.js
index 3afa83de..60a8b639 100644
--- a/tests/electron/modules/compliments_spec.js
+++ b/tests/electron/modules/compliments_spec.js
@@ -36,7 +36,7 @@ describe("Compliments module", () => {
describe("Feature date in compliments module", () => {
describe("Set date and empty compliments for anytime, morning, evening and afternoon", () => {
- it("Show happy new year compliment on new years day", async () => {
+ it("shows happy new year compliment on new years day", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_date.js", "01 Jan 2022 10:00:00 GMT");
await doTest(["Happy new year!"]);
});
diff --git a/tests/unit/functions/calendar_spec.js b/tests/unit/modules/default/calendar/calendar_utils_spec.js
similarity index 52%
rename from tests/unit/functions/calendar_spec.js
rename to tests/unit/modules/default/calendar/calendar_utils_spec.js
index 34e16dd3..7cf896c1 100644
--- a/tests/unit/functions/calendar_spec.js
+++ b/tests/unit/modules/default/calendar/calendar_utils_spec.js
@@ -1,18 +1,8 @@
global.moment = require("moment");
-describe("Functions into modules/default/calendar/calendar.js", () => {
- // Fake for use by calendar.js
- Module = {};
- Module.definitions = {};
- Module.register = (name, moduleDefinition) => {
- Module.definitions[name] = moduleDefinition;
- };
-
- beforeAll(() => {
- // load calendar.js
- require("../../../modules/default/calendar/calendar");
- });
+const CalendarUtils = require("../../../../../modules/default/calendar/calendarutils");
+describe("Calendar utils tests", () => {
describe("capFirst", () => {
const words = {
rodrigo: "Rodrigo",
@@ -24,68 +14,88 @@ describe("Functions into modules/default/calendar/calendar.js", () => {
Object.keys(words).forEach((word) => {
it(`for '${word}' should return '${words[word]}'`, () => {
- expect(Module.definitions.calendar.capFirst(word)).toBe(words[word]);
+ expect(CalendarUtils.capFirst(word)).toBe(words[word]);
});
});
+
+ it("should not capitalize other letters", () => {
+ expect(CalendarUtils.capFirst("event")).not.toBe("EVent");
+ });
});
describe("getLocaleSpecification", () => {
it("should return a valid moment.LocaleSpecification for a 12-hour format", () => {
- expect(Module.definitions.calendar.getLocaleSpecification(12)).toEqual({ longDateFormat: { LT: "h:mm A" } });
+ expect(CalendarUtils.getLocaleSpecification(12)).toEqual({ longDateFormat: { LT: "h:mm A" } });
});
it("should return a valid moment.LocaleSpecification for a 24-hour format", () => {
- expect(Module.definitions.calendar.getLocaleSpecification(24)).toEqual({ longDateFormat: { LT: "HH:mm" } });
+ expect(CalendarUtils.getLocaleSpecification(24)).toEqual({ longDateFormat: { LT: "HH:mm" } });
});
it("should return the current system locale when called without timeFormat number", () => {
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
});
it("should return a 12-hour longDateFormat when using the 'en' locale", () => {
const localeBackup = moment.locale();
moment.locale("en");
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup);
});
it("should return a 12-hour longDateFormat when using the 'au' locale", () => {
const localeBackup = moment.locale();
moment.locale("au");
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup);
});
it("should return a 12-hour longDateFormat when using the 'eg' locale", () => {
const localeBackup = moment.locale();
moment.locale("eg");
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup);
});
it("should return a 24-hour longDateFormat when using the 'nl' locale", () => {
const localeBackup = moment.locale();
moment.locale("nl");
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup);
});
it("should return a 24-hour longDateFormat when using the 'fr' locale", () => {
const localeBackup = moment.locale();
moment.locale("fr");
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup);
});
it("should return a 24-hour longDateFormat when using the 'uk' locale", () => {
const localeBackup = moment.locale();
moment.locale("uk");
- expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
+ expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup);
});
});
describe("shorten", () => {
+ it("should not shorten if short enough", () => {
+ expect(CalendarUtils.shorten("Event 1", 10, false, 1)).toBe("Event 1");
+ });
+
+ it("should shorten into one line", () => {
+ expect(CalendarUtils.shorten("Example event at 12 o clock", 10, true, 1)).toBe("Example …");
+ });
+
+ it("should shorten into three lines", () => {
+ expect(CalendarUtils.shorten("Example event at 12 o clock", 10, true, 3)).toBe("Example
event at 12 o
clock");
+ });
+
+ it("should not shorten into three lines if wrap is false", () => {
+ expect(CalendarUtils.shorten("Example event at 12 o clock", 10, false, 3)).toBe("Example ev…");
+ });
+
const strings = {
" String with whitespace at the beginning that needs trimming": { length: 16, return: "String with whit…" },
"long string that needs shortening": { length: 16, return: "long string that…" },
@@ -95,34 +105,44 @@ describe("Functions into modules/default/calendar/calendar.js", () => {
Object.keys(strings).forEach((string) => {
it(`for '${string}' should return '${strings[string].return}'`, () => {
- expect(Module.definitions.calendar.shorten(string, strings[string].length)).toBe(strings[string].return);
+ expect(CalendarUtils.shorten(string, strings[string].length)).toBe(strings[string].return);
});
});
it("should return an empty string if shorten is called with a non-string", () => {
- expect(Module.definitions.calendar.shorten(100)).toBe("");
+ expect(CalendarUtils.shorten(100)).toBe("");
});
it("should not shorten the string if shorten is called with a non-number maxLength", () => {
- expect(Module.definitions.calendar.shorten("This is a test string", "This is not a number")).toBe("This is a test string");
+ expect(CalendarUtils.shorten("This is a test string", "This is not a number")).toBe("This is a test string");
});
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (with maxLength defined as 20)", () => {
- expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", 20, true)).toBe(
+ expect(CalendarUtils.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", 20, true)).toBe(
"This is a
wrapEvent test. Should wrap
the string instead of
shorten it if called with
wrapEvent = true"
);
});
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (without maxLength defined, default 25)", () => {
- expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", undefined, true)).toBe(
+ expect(CalendarUtils.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", undefined, true)).toBe(
"This is a wrapEvent
test. Should wrap the string
instead of shorten it if called
with wrapEvent = true"
);
});
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", () => {
- expect(Module.definitions.calendar.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).toBe(
+ expect(CalendarUtils.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).toBe(
"This is a wrapEvent and
maxTitleLines test. Should wrap and …"
);
});
});
+
+ describe("titleTransform and shorten combined", () => {
+ it("should replace the birthday and wrap nicely", () => {
+ const transformedTitle = CalendarUtils.titleTransform("Michael Teeuw's birthday", {
+ "De verjaardag van ": "",
+ "'s birthday": ""
+ });
+ expect(CalendarUtils.shorten(transformedTitle, 10, true, 2)).toBe("Michael
Teeuw");
+ });
+ });
});
diff --git a/tests/unit/functions/weather_object_spec.js b/tests/unit/modules/default/weather/weather_object_spec.js
similarity index 94%
rename from tests/unit/functions/weather_object_spec.js
rename to tests/unit/modules/default/weather/weather_object_spec.js
index 5f9444e4..f681b357 100644
--- a/tests/unit/functions/weather_object_spec.js
+++ b/tests/unit/modules/default/weather/weather_object_spec.js
@@ -1,5 +1,5 @@
-const WeatherObject = require("../../../modules/default/weather/weatherobject");
-const WeatherUtils = require("../../../modules/default/weather/weatherutils");
+const WeatherObject = require("../../../../../modules/default/weather/weatherobject");
+const WeatherUtils = require("../../../../../modules/default/weather/weatherutils");
global.moment = require("moment-timezone");
global.SunCalc = require("suncalc");
diff --git a/tests/unit/modules/default/weather/weather_utils_spec.js b/tests/unit/modules/default/weather/weather_utils_spec.js
index a7c8d75c..482212e0 100644
--- a/tests/unit/modules/default/weather/weather_utils_spec.js
+++ b/tests/unit/modules/default/weather/weather_utils_spec.js
@@ -2,7 +2,7 @@ const weather = require("../../../../../modules/default/weather/weatherutils");
describe("Weather utils tests", () => {
describe("convertPrecipitationUnit tests", () => {
- it("Should keep value and unit if outputUnit is undefined", () => {
+ it("should keep value and unit if outputUnit is undefined", () => {
const values = [1, 2];
const units = ["mm", "cm"];
@@ -12,7 +12,7 @@ describe("Weather utils tests", () => {
}
});
- it("Should keep value and unit if outputUnit is metric", () => {
+ it("should keep value and unit if outputUnit is metric", () => {
const values = [1, 2];
const units = ["mm", "cm"];
@@ -22,7 +22,7 @@ describe("Weather utils tests", () => {
}
});
- it("Should use mm unit if input unit is undefined", () => {
+ it("should use mm unit if input unit is undefined", () => {
const values = [1, 2];
for (let i = 0; i < values.length; i++) {
@@ -31,7 +31,7 @@ describe("Weather utils tests", () => {
}
});
- it("Should convert value and unit if outputUnit is imperial", () => {
+ it("should convert value and unit if outputUnit is imperial", () => {
const values = [1, 2];
const units = ["mm", "cm"];
const expectedValues = [0.04, 0.79];