Refactor calendar methods into util class (#3088)

Refactored some methods in calendar module:
- move methods into own file 
- dont call shorten method from titelTransform because why? just call
them after each other.
- added tests for util methods
- cleaned up other tests

---------

Co-authored-by: veeck <michael@veeck.de>
This commit is contained in:
Veeck 2023-04-09 12:49:50 +02:00 committed by GitHub
parent dee3cd3da7
commit 77f9c86774
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 787 additions and 757 deletions

View File

@ -11,7 +11,7 @@ _This release is scheduled to be released on 2023-07-01._
### Added ### Added
- Added tests for severonly - Added tests for serveronly
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests) - Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
### Removed ### Removed
@ -23,6 +23,7 @@ _This release is scheduled to be released on 2023-07-01._
- Update electron to v24 - Update electron to v24
- Use node v19 in github workflow (replacing v14) - Use node v19 in github workflow (replacing v14)
- Refactor formatTime into common util function for default modules - Refactor formatTime into common util function for default modules
- Refactor some calendar methods into own class and added tests for them
### Fixed ### Fixed

View File

@ -1,4 +1,4 @@
/* global cloneObject */ /* global CalendarUtils, cloneObject */
/* MagicMirror² /* MagicMirror²
* Module: Calendar * Module: Calendar
@ -79,7 +79,7 @@ Module.register("calendar", {
// Define required scripts. // Define required scripts.
getScripts: function () { getScripts: function () {
return ["moment.js"]; return ["calendarutils.js", "moment.js"];
}, },
// Define required translations. // Define required translations.
@ -108,7 +108,7 @@ Module.register("calendar", {
} }
// Set locale. // Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat)); moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
// clear data holder before start // clear data holder before start
this.calendarData = {}; 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); const titleClass = this.titleClassForUrl(event.url);
@ -362,7 +363,7 @@ Module.register("calendar", {
// Add endDate to dataheaders if showEnd is enabled // Add endDate to dataheaders if showEnd is enabled
if (this.config.showEnd) { 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); eventWrapper.appendChild(timeWrapper);
@ -378,20 +379,20 @@ Module.register("calendar", {
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
// Use dateFormat // 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 // Add end time if showEnd
if (this.config.showEnd) { if (this.config.showEnd) {
timeWrapper.innerHTML += "-"; 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 // For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) { if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day //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; 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) { } else if (this.config.getRelative > 0 && event.startDate < now) {
// Ongoing and getRelative is set // Ongoing and getRelative is set
timeWrapper.innerHTML = this.capFirst( timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", { this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true) 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) { } else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
// Within urgency days // 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) { if (event.fullDayEvent && this.config.nextDaysRelative) {
// Full days events within the next two days // Full days events within the next two days
if (event.today) { if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY")); timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.yesterday) { } 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) { } 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) { } else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { 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)) { if (event.startDate >= now || (event.fullDayEvent && event.today)) {
// Use relative time // Use relative time
if (!this.config.hideTime && !event.fullDayEvent) { 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 { } else {
timeWrapper.innerHTML = this.capFirst( timeWrapper.innerHTML = CalendarUtils.capFirst(
moment(event.startDate, "x").calendar(null, { moment(event.startDate, "x").calendar(null, {
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`, sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
nextDay: `[${this.translate("TOMORROW")}]`, nextDay: `[${this.translate("TOMORROW")}]`,
@ -434,27 +435,27 @@ Module.register("calendar", {
if (event.fullDayEvent) { if (event.fullDayEvent) {
// Full days events within the next two days // Full days events within the next two days
if (event.today) { if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY")); timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.dayBeforeYesterday) { } else if (event.dayBeforeYesterday) {
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") { if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYBEFOREYESTERDAY")); timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
} }
} else if (event.yesterday) { } 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) { } 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) { } else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") { 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) { } else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow() // 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 { } else {
// Ongoing event // Ongoing event
timeWrapper.innerHTML = this.capFirst( timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", { this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`, fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true) timeUntilEnd: moment(event.endDate, "x").fromNow(true)
@ -503,7 +504,9 @@ Module.register("calendar", {
const descCell = document.createElement("td"); const descCell = document.createElement("td");
descCell.className = "location"; descCell.className = "location";
descCell.colSpan = "2"; 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); locationRow.appendChild(descCell);
wrapper.appendChild(locationRow); wrapper.appendChild(locationRow);
@ -519,28 +522,6 @@ Module.register("calendar", {
return wrapper; 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. * Checks if this config contains the calendar url.
* *
@ -870,98 +851,6 @@ Module.register("calendar", {
return !!this.getCalendarProperty(url, property, undefined); 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}<br>${word} `;
} else {
temp += `${word}<br>`;
}
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. * Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate. * The all events available in one array, sorted on startdate.

View File

@ -11,7 +11,7 @@ const ical = require("node-ical");
const fetch = require("fetch"); const fetch = require("fetch");
const Log = require("logger"); const Log = require("logger");
const NodeHelper = require("node_helper"); 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 { try {
data = ical.parseICS(responseData); data = ical.parseICS(responseData);
Log.debug(`parsed data=${JSON.stringify(data)}`); Log.debug(`parsed data=${JSON.stringify(data)}`);
events = CalendarUtils.filterEvents(data, { events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents, excludedEvents,
includePastEvents, includePastEvents,
maximumEntries, maximumEntries,

View File

@ -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;
}

View File

@ -1,610 +1,117 @@
/* MagicMirror² /* MagicMirror²
* Calendar Util Methods * Calendar Util Methods
* *
* By Michael Teeuw https://michaelteeuw.nl * By Rejas
* MIT Licensed. * 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 = { const CalendarUtils = {
/** /**
* Calculate the time correction, either dst/std or full day in cases where * Capitalize the first letter of a string
* utc time is day before plus offset
* *
* @param {object} event the event which needs adjustement * @param {string} string The string to capitalize
* @param {Date} date the date on which this event happens * @returns {string} The capitalized string
* @returns {number} the necessary adjustment in hours
*/ */
calculateTimezoneAdjustment: function (event, date) { capFirst: function (string) {
let adjustHours = 0; return string.charAt(0).toUpperCase() + string.slice(1);
// 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;
}, },
/** /**
* 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 {number} timeFormat Specifies either 12 or 24-hour time format
* @param {object} config The configuration object * @returns {moment.LocaleSpecification} formatted time
* @returns {string[]} the filtered events
*/ */
filterEvents: function (data, config) { getLocaleSpecification: function (timeFormat) {
const newEvents = []; switch (timeFormat) {
case 12: {
// limitFunction doesn't do much limiting, see comment re: the dates return { longDateFormat: { LT: "h:mm A" } };
// 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();
} }
case 24: {
// FIXME: Ugly fix to solve the facebook birthday issue. return { longDateFormat: { LT: "HH:mm" } };
// Otherwise, the recurring events only show the birthday for next year. }
let isFacebookBirthday = false; default: {
if (typeof event.uid !== "undefined") { return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
} }
} }
},
if (event.type === "VEVENT") { /**
Log.debug(`Event:\n${JSON.stringify(event)}`); * Shortens a string if it's longer than maxLength and add an ellipsis to the end
let startDate = eventDate(event, "start"); *
let endDate; * @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") { if (wrapEvents === true) {
endDate = eventDate(event, "end"); const words = string.split(" ");
} else if (typeof event.duration !== "undefined") { let temp = "";
endDate = startDate.clone().add(moment.duration(event.duration)); 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 { } else {
if (!isFacebookBirthday) { line++;
// make copy of start date, separate storage area if (line > maxTitleLines - 1) {
endDate = moment(startDate.format("x"), "x"); if (i < words.length) {
} else { currentLine += "…";
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;
} }
break; break;
} }
}
if (excluded) { if (currentLine.length > 0) {
return; temp += `${currentLine}<br>${word} `;
}
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}`);
} else { } else {
// if we want past events temp += `${word}<br>`;
if (config.includePastEvents) { }
// use the calculated past time for the between from currentLine = "";
pastLocal = pastMoment.toDate(); }
}
return (temp + currentLine).trim();
} else { } else {
// otherwise use NOW.. cause we shouldn't use any before now if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
pastLocal = moment().toDate(); //now return `${string.trim().slice(0, maxLength)}`;
}
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 (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 { } else {
// if the timezones are the same, correct date if needed return string.trim();
//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
});
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
return newEvents;
}, },
/** /**
* Lookup iana tz from windows * 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} msTZName the timezone name to lookup * @param {string} title The title to transform.
* @returns {string|null} the iana name or null of none is found * @param {object} titleReplace Pairs of strings to be replaced in the title
* @returns {string} The transformed title.
*/ */
getIanaTZFromMS: function (msTZName) { titleTransform: function (title, titleReplace) {
// Get hash entry for (let needle in titleReplace) {
const he = zoneTable[msTZName]; const replacement = titleReplace[needle];
// If found return iana name, else null
return he ? he.iana[0] : null;
},
/** const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
* Gets the title from the event. if (regParts) {
* // the parsed pattern is a regexp.
* @param {object} event The event object to check. needle = new RegExp(regParts[1], regParts[2]);
* @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;
} }
title = title.replace(needle, replacement);
}
return title; 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);
}
} }
}; };

View File

@ -25,7 +25,7 @@ describe("Compliments module", () => {
await helpers.getDocument(); 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"]); await doTest(["Anytime here"]);
}); });
}); });
@ -36,7 +36,7 @@ describe("Compliments module", () => {
await helpers.getDocument(); await helpers.getDocument();
}); });
it("Show anytime compliments", async () => { it("shows anytime compliments", async () => {
await doTest(["Anytime here"]); await doTest(["Anytime here"]);
}); });
}); });

View File

@ -9,13 +9,13 @@ describe("Check configuration without modules", () => {
await helpers.stopApplication(); 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"); const elem = await helpers.waitForElement("#module_1_helloworld .module-content");
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
expect(elem.textContent).toContain("MagicMirror²"); 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"); const elem = await helpers.waitForElement("#module_5_helloworld .module-content");
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
expect(elem.textContent).toContain("www.michaelteeuw.nl"); expect(elem.textContent).toContain("www.michaelteeuw.nl");

View File

@ -36,7 +36,7 @@ describe("Compliments module", () => {
describe("Feature date in compliments module", () => { describe("Feature date in compliments module", () => {
describe("Set date and empty compliments for anytime, morning, evening and afternoon", () => { 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 helpers.startApplication("tests/configs/modules/compliments/compliments_date.js", "01 Jan 2022 10:00:00 GMT");
await doTest(["Happy new year!"]); await doTest(["Happy new year!"]);
}); });

View File

@ -1,18 +1,8 @@
global.moment = require("moment"); global.moment = require("moment");
describe("Functions into modules/default/calendar/calendar.js", () => { const CalendarUtils = require("../../../../../modules/default/calendar/calendarutils");
// 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");
});
describe("Calendar utils tests", () => {
describe("capFirst", () => { describe("capFirst", () => {
const words = { const words = {
rodrigo: "Rodrigo", rodrigo: "Rodrigo",
@ -24,68 +14,88 @@ describe("Functions into modules/default/calendar/calendar.js", () => {
Object.keys(words).forEach((word) => { Object.keys(words).forEach((word) => {
it(`for '${word}' should return '${words[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", () => { describe("getLocaleSpecification", () => {
it("should return a valid moment.LocaleSpecification for a 12-hour format", () => { 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", () => { 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", () => { 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", () => { it("should return a 12-hour longDateFormat when using the 'en' locale", () => {
const localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("en"); 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); moment.locale(localeBackup);
}); });
it("should return a 12-hour longDateFormat when using the 'au' locale", () => { it("should return a 12-hour longDateFormat when using the 'au' locale", () => {
const localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("au"); 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); moment.locale(localeBackup);
}); });
it("should return a 12-hour longDateFormat when using the 'eg' locale", () => { it("should return a 12-hour longDateFormat when using the 'eg' locale", () => {
const localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("eg"); 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); moment.locale(localeBackup);
}); });
it("should return a 24-hour longDateFormat when using the 'nl' locale", () => { it("should return a 24-hour longDateFormat when using the 'nl' locale", () => {
const localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("nl"); moment.locale("nl");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } }); expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 24-hour longDateFormat when using the 'fr' locale", () => { it("should return a 24-hour longDateFormat when using the 'fr' locale", () => {
const localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("fr"); moment.locale("fr");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } }); expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("should return a 24-hour longDateFormat when using the 'uk' locale", () => { it("should return a 24-hour longDateFormat when using the 'uk' locale", () => {
const localeBackup = moment.locale(); const localeBackup = moment.locale();
moment.locale("uk"); moment.locale("uk");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } }); expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
}); });
describe("shorten", () => { 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 <br>event at 12 o <br>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 = { const strings = {
" String with whitespace at the beginning that needs trimming": { length: 16, return: "String with whit…" }, " 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…" }, "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) => { Object.keys(strings).forEach((string) => {
it(`for '${string}' should return '${strings[string].return}'`, () => { 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", () => { 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", () => { 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)", () => { 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 <br>wrapEvent test. Should wrap <br>the string instead of <br>shorten it if called with <br>wrapEvent = true" "This is a <br>wrapEvent test. Should wrap <br>the string instead of <br>shorten it if called with <br>wrapEvent = true"
); );
}); });
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (without maxLength defined, default 25)", () => { 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 <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true" "This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true"
); );
}); });
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", () => { 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 <br>maxTitleLines test. Should wrap and …" "This is a wrapEvent and <br>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 <br>Teeuw");
});
});
}); });

View File

@ -1,5 +1,5 @@
const WeatherObject = require("../../../modules/default/weather/weatherobject"); const WeatherObject = require("../../../../../modules/default/weather/weatherobject");
const WeatherUtils = require("../../../modules/default/weather/weatherutils"); const WeatherUtils = require("../../../../../modules/default/weather/weatherutils");
global.moment = require("moment-timezone"); global.moment = require("moment-timezone");
global.SunCalc = require("suncalc"); global.SunCalc = require("suncalc");

View File

@ -2,7 +2,7 @@ const weather = require("../../../../../modules/default/weather/weatherutils");
describe("Weather utils tests", () => { describe("Weather utils tests", () => {
describe("convertPrecipitationUnit 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 values = [1, 2];
const units = ["mm", "cm"]; 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 values = [1, 2];
const units = ["mm", "cm"]; 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]; const values = [1, 2];
for (let i = 0; i < values.length; i++) { 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 values = [1, 2];
const units = ["mm", "cm"]; const units = ["mm", "cm"];
const expectedValues = [0.04, 0.79]; const expectedValues = [0.04, 0.79];