mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
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:
parent
dee3cd3da7
commit
77f9c86774
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
613
modules/default/calendar/calendarfetcherutils.js
Normal file
613
modules/default/calendar/calendarfetcherutils.js
Normal 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;
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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");
|
||||||
|
@ -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!"]);
|
||||||
});
|
});
|
||||||
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
@ -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");
|
@ -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];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user