UNPKG

node-red-contrib-sun-position

Version:
1,375 lines (1,287 loc) 94 kB
// @ts-check /* * This code is licensed under the Apache License Version 2.0. * * Copyright (c) 2022 Robert Gester * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * */ /******************************************** * dateTimeHelper.js: *********************************************/ 'use strict'; /** * support timeData * @typedef {Object} ITimeObject Data * @param {Date} now * @param {number} nowNr * @param {number} dayNr * @param {number} monthNr * @param {number} dateNr * @param {number} yearNr * @param {number} dayId */ /** * @typedef {Object} ILimitationsObj * @property {boolean} [next] - if __true__ the next date will be delivered starting from now, otherwise the matching date of the date from now * @property {Array.<number>|string} [days] - days for which should be calculated the sun time * @property {Array.<number>|string} [months] - months for which should be calculated the sun time * @property {boolean} [onlyOddDays] - if true only odd days will be used * @property {boolean} [onlyEvenDays] - if true only even days will be used * @property {boolean} [onlyOddWeeks] - if true only odd weeks will be used * @property {boolean} [onlyEvenWeeks] - if true only even weeks will be used * @property {Date} [dateStart] - Date for start range * @property {Date} [dateEnd] - Date for end range * dateStart */ /** * @typedef {Object} ILimitedDate * @property {Date} date - The limited Date * @property {boolean} hasChanged - indicator if the input Date has changed * @property {string} error - if an error occurs the string is not empty */ const util = require('util'); const TIME_WEEK = 604800000; const TIME_5d = 432000000; const TIME_4d = 345600000; const TIME_3d = 259200000; const TIME_36h = 129600000; const TIME_24h = 86400000; const TIME_12h = 43200000; const TIME_1h = 3600000; const TIME_20min = 1200000; const TIME_1min = 60000; const TIME_1s = 1000; module.exports = { TIME_WEEK, TIME_5d, TIME_4d, TIME_3d, TIME_36h, TIME_24h, TIME_12h, TIME_1h, TIME_20min, TIME_1min, TIME_1s, isBool, isTrue, isFalse, XOR, XAND, pad2, pad, angleNorm, angleNormRad, toDec, toRad, clipStrLength, countDecimals, handleError, chkValueFilled, checkLimits, getMsgBoolValue, getMsgBoolValue2, getMsgTopicContains, getMsgNumberValue, getMsgNumberValue2, getSpecialDayOfMonth, getNthWeekdayOfMonth, getLastDayOfMonth, getStdTimezoneOffset, isDSTObserved, addOffset, calcDayOffset, calcMonthOffset, convertDateTimeZone, isValidDate, isoStringToDate, roundToHour, normalizeDate, limitDate, getTimeOfText, getDateOfText, getDayOfYear, getWeekOfYear, getUTCDayId, getDayId, getTimeNumberUTC, getTimeOut, getNodeId, initializeParser, getFormattedDateOut, parseDateFromFormat, textReplace, getNowTimeStamp, getNowObject, getDeepValue }; /*******************************************************************************************************/ /* Date.prototype.addDays = function (days) { var date = new Date(this.valueOf()); date.setUTCDate(date.getUTCDate() + days); return date; } // Returns the ISO week of the date. Date.prototype.getWeek = function() { const date = new Date(this.getTime()); date.setHours(0, 0, 0, 0); // Thursday in current week decides the year. date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); // January 4 is always in week 1. const week1 = new Date(date.getFullYear(), 0, 4); // Adjust to Thursday in week 1 and count number of weeks from date to week1. return 1 + Math.round(((date.getTime() - week1.getTime()) / TIME_24h - 3 + (week1.getDay() + 6) % 7) / 7); }; // Returns the four-digit year corresponding to the ISO week of the date. Date.prototype.getWeekYear = function() { const date = new Date(this.getTime()); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); return date.getFullYear(); }; */ /*******************************************************************************************************/ /* simple functions */ /*******************************************************************************************************/ /** * returns **true** if the parameter value is a valid boolean value for **false** or **true** * @param {*} val a parameter which should be checked if it is a valid false boolean * @returns {boolean} true if the parameter value is a valid boolean value for for **false** or **true** */ function isBool(val) { val = (val+'').toLowerCase(); return (['true', 'yes', 'on', 'ja', 'false', 'no', 'off', 'nein'].includes(val) || !isNaN(val)); } /** * returns **true** if the parameter value is a valid boolean value for **true** * @param {*} val a parameter which should be checked if it is a valid true boolean * @returns {boolean} true if the parameter value is a valid boolean value for **true** */ function isTrue(val) { val = (val+'').toLowerCase(); return (['true', 'yes', 'on', 'ja'].includes(val) || (!isNaN(val) && (Number(val) > 0))); } /** * returns **true** if the parameter value is a valid boolean value for **false** * @param {*} val a parameter which should be checked if it is a valid false boolean * @returns {boolean} true if the parameter value is a valid boolean value for **false** */ function isFalse(val) { val = (val+'').toLowerCase(); return (['false', 'no', 'off', 'nein'].includes(val) || (!isNaN(val) && (Number(val) <= 0))); } /** * Exclusive OR * @param {*} a - operand one * @param {*} b - operand two * @returns {boolean} - **true** if the a expression or b expression is **true** (like ||), but not if both are **true** */ function XOR(a,b) { return (!a !== !b); // (a || b) && !(a && b); // ( a && !b ) || ( !a && b ) } /** * Exclusive AND * @param {*} a - operand one * @param {*} b - operand two * @returns {boolean} - **true** if the a expression and b expression is **true** (like &&) or if both are **false** */ function XAND(a, b) { return (!a === !b); // (a && b) || !(a || b); // (a && b) || (!a && !b); } /** * count the number of decimals of a number * @param {*} value number to check */ function countDecimals(value) { return value === value>> 0 ? 0 : value.toString().split('.')[1].length || 0; } /** * normalizes an angle * @param {number} angle to normalize */ function angleNorm(angle) { while (angle < 0) { angle += 360; } while (angle > 360) { angle -= 360; } return angle; } /** * normalizes an angle * @param {number} angle to normalize */ function angleNormRad(angle) { const dr = (2 * Math.PI); while (angle < 0) { angle += dr; } while (angle > dr) { angle -= dr; } return angle; } /** * radians to decimal grad * @param {number} rad angle in radians * @return {number} angle in decimal grad */ function toDec(rad) { return rad * ( 180 / Math.PI ); } /** * decimal grad to radians * @param {number} dec angle in decimal grad * @return {number} angle in radians */ function toRad(dec) { return dec * ( Math.PI / 180 ); } /*******************************************************************************************************/ /** * gives a ID of a node * @param {any} node a node * @returns {string} id of the given node */ function getNodeId(node) { return '[' + node.type + ((node.name) ? '/' + node.name + ':' : ':') + (node._path || node.id) + ']'; } /*******************************************************************************************************/ /** * creates a string with two digits * @param {number} n number to format * @returns {string} number with minimum two digits */ function pad2(n) { // always returns a string return (n < 0 || n > 9 ? '' : '0') + n; } /** * creates a string from a number with leading zeros * @param {any} val number to format * @param {number} [len] length of number (default 2) * @returns {string} number with minimum digits as defined in length */ function pad(val, len) { val = String(val); len = len || 2; while (val.length < len) { val = '0' + val; } return val; } /*******************************************************************************************************/ // obj.timer = setTimeoutPromise(60500).then(() // const setTimeoutPromise = util.promisify(setTimeout); // const setIntervalPromise = util.promisify(setTimeout); /*******************************************************************************************************/ /* Node-Red Helper functions */ /*******************************************************************************************************/ /** * generic function for handle a error in a node * @param {any} node the node where the error occurs * @param {String} messageText the message text * @param {Error} [err] the error object * @param {string} [stateText] the state text which should be set to the node */ function handleError(node, messageText, err, stateText) { if (!err) { err = new Error(messageText); } else { if (messageText && err.message) { messageText += ':' + err.message; } else if (err.message) { messageText = err.message; } } if (node && messageText) { node.error(messageText); node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', text: (stateText) ? stateText : messageText }); } else if (console) { /* eslint-disable no-console */ console.error(messageText); console.log(util.inspect(err)); console.trace(); // eslint-disable-line /* eslint-enable no-console */ } } /*******************************************************************************************************/ /** * check if a value is filled or returns default value * @param {any} val to check for undefined, null, empty * @param {any} defaultVal default value to use * @returns {any} result to use if value is undefined, null or empty string */ function chkValueFilled(val, defaultVal) { return ((typeof val === 'undefined') || (val === '') || (val === null)) ? defaultVal : val; } /*******************************************************************************************************/ /** * clip a text to a maximum length * @param {string} v text to clip * @param {number} [l] length to clip the text * @returns {string} string not longer than the given length */ function clipStrLength(v, l) { l = l || 15; v = String(v); if (v.length > l) { return v.slice(0, (l - 3)) + '...'; } return v; } /*******************************************************************************************************/ /** * get a date for the first day of week in the given month * @param {number} year year to check * @param {number} month month to check * @param {number} [dayOfWeek] Day of week, where 0 is Sunday, 1 Monday ... 6 Saturday * @returns {Date} first day of given month */ /* function _getFirstDayOfMonth(year, month, dayOfWeek) { const d = new Date(year, month, 1); dayOfWeek = dayOfWeek || 1; // Monday while (d.getDay() !== dayOfWeek) { d.setDate(d.getDate() + 1); } return d; } */ /** * get a date for the specific day of week in the given month * @param {number} year year to check * @param {number} month month to check * @param {number} [dayOfWeek] day of week 0=Sunday, 1=Monday, ..., 6=Saturday * @param {number} [n] the nTh Numer of the day of week - 0 based * @returns {Date} weekday of given month */ function getNthWeekdayOfMonth(year, month, dayOfWeek, n) { const date = new Date(year, month, 1); dayOfWeek = dayOfWeek || 1; // Monday n = n || 0; // sunday const add = (dayOfWeek - date.getDay() + 7) % 7 + n * 7; date.setDate(1 + add); return date; } /** * get a date for the last day of week in the given month * @param {number} year year to check * @param {number} month month to check * @param {number} [dayOfWeek] Day of week, where 0 is Sunday, 1 Monday ... 6 Saturday * @returns {Date} last day of given month */ function getLastDayOfMonth(year, month, dayOfWeek) { const d = new Date(year, month+1, 0); dayOfWeek = dayOfWeek || 1; // Monday while (d.getDay() !== dayOfWeek) { d.setDate(d.getDate() - 1); } return d; } /** * get a date for the special day in the given month * @param {number} year year to check * @param {number} month month to check * @param {string} dayName Name of the special day * @returns {Date|null} last day of given month or null */ function getSpecialDayOfMonth(year, month, dayName) { switch (dayName) { case 'fMon': return getNthWeekdayOfMonth(year, month, 1, 0); case 'fTue': return getNthWeekdayOfMonth(year, month, 2, 0); case 'fWed': return getNthWeekdayOfMonth(year, month, 3, 0); case 'fThu': return getNthWeekdayOfMonth(year, month, 4, 0); case 'fFri': return getNthWeekdayOfMonth(year, month, 5, 0); case 'fSat': return getNthWeekdayOfMonth(year, month, 6, 0); case 'fSun': return getNthWeekdayOfMonth(year, month, 0, 0); case 'lMon': return getLastDayOfMonth(year, month, 1); case 'lTue': return getLastDayOfMonth(year, month, 2); case 'lWed': return getLastDayOfMonth(year, month, 3); case 'lThu': return getLastDayOfMonth(year, month, 4); case 'lFri': return getLastDayOfMonth(year, month, 5); case 'lSat': return getLastDayOfMonth(year, month, 6); case 'lSun': return getLastDayOfMonth(year, month, 0); } return null; } /** * For a given date, get the ISO week number * * Based on information at: * * http://www.merlyn.demon.co.uk/weekcalc.htm#WNR * * Algorithm is to find nearest thursday, it's year * is the year of the week number. Then get weeks * between that date and the first day of that year. * * Note that dates in one year can be weeks of previous * or next year, overlap is up to 3 days. * * e.g. 2014/12/29 is Monday in week 1 of 2015 * 2012/1/1 is Sunday in week 52 of 2011 * * @param {Date} date date to get week number * @returns {Array} ISO week number, [UTCFullYear, weekNumber] */ function getWeekOfYear(date) { // Copy date so don't modify original date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); // Set to nearest Thursday: current date + 4 - current day number // Make Sunday's day number 7 date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay()||7)); // Get first day of year const yearStart = new Date(Date.UTC(date.getUTCFullYear(),0,1)); // Calculate full weeks to nearest Thursday const weekNo = Math.ceil(( ( (date.getTime() - yearStart.getTime()) / TIME_24h) + 1)/7); // Return array of year and week number return [date.getUTCFullYear(), weekNo]; } /** * For a given date, get the day number * @param {Date} date date to get day number * @returns {Array} day number, [UTCFullYear, dayNumber] */ function getDayOfYear(date) { const start = new Date(date.getFullYear(), 0, 0); const diff = (date.getTime() - start.getTime()) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * TIME_1min); return [date.getUTCFullYear(), Math.floor(diff / TIME_24h)]; } /** * gets a day id from a date * @param {Date} d date to get day id from */ function getUTCDayId(d) { return d.getUTCDay() + (d.getUTCMonth() * 31) + (d.getUTCFullYear() * 372); } /** * gets a day id from a date * @param {Date} d date to get day id from */ function getDayId(d) { return d.getDay() + (d.getMonth() * 31) + (d.getFullYear() * 372); } /******************************************************************************************/ /** * the definition of the time to compare * @param {*} node the current node object * @param {*} msg the message object * @returns {Date} Date object of given Date or now */ function getNowTimeStamp(node, msg) { let value = ''; if (typeof msg.time === 'number') { value = msg.time; node.debug(`compare time to msg.time = "${value}"`); } else if (typeof msg.ts === 'number') { value = msg.ts; node.debug(`compare time to msg.ts = "${value}"`); } else { return new Date(); } const dto = new Date(value); if (isValidDate(dto)) { // node.debug(dto.toISOString()); return dto; } node.error(`Error can not get a valid timestamp from "${value}"! Will use current timestamp!`); return new Date(); } /** * the definition of the time to compare * @param {*} node the current node object * @param {*} msg the message object * @returns {ITimeObject} Date object of given Date or now */ function getNowObject(node, msg) { const dNow = getNowTimeStamp(node, msg); return { now : dNow, nowNr : dNow.getTime(), dayNr : dNow.getDay(), dateNr : dNow.getDate(), weekNr : getWeekOfYear(dNow), monthNr : dNow.getMonth(), yearNr : dNow.getFullYear(), dayId : getDayId(dNow) }; } /*******************************************************************************************************/ /* date-time functions */ /*******************************************************************************************************/ /** * convert the time part of a date into a comparable number * @param {Date} date - date to convert * @return {number} numeric representation of the time part of the date */ function getTimeNumberUTC(date) { return date.getUTCMilliseconds() + date.getUTCSeconds() * TIME_1s + date.getUTCMinutes() * TIME_1min + date.getUTCHours() * TIME_1h; } /*******************************************************************************************************/ /** * get the timeout time * @param {Date} base - base time (tyically new Date()) * @param {Date} time - time to schedule the timeout * @returns {number} milliseconds until the defined Date */ function getTimeOut(base, time) { let millisec = (time.valueOf() - base.valueOf()); // - 5; while (millisec < 10) { millisec += TIME_24h; // 24h } return millisec; } /*******************************************************************************************************/ /** * check if a given number is in given limits * @param {number} num number angle to compare * @param {number} [low] low limit * @param {number} [high] high limit * @return {boolean} **true** if the number is inside given limits, at least one limit must be validate, otherwise returns **false** */ function checkLimits(num, low, high) { // console.debug('checkLimits num=' + num + ' low=' + low + ' high=' + high); // eslint-disable-line if (typeof low === 'number' && !isNaN(low)) { if (typeof high === 'number' && !isNaN(high)) { if (high > low) { return (num > low) && (num < high); } return (num > low) || (num < high); } return (num > low); } if (typeof high === 'number' && !isNaN(high)) { return (num < high); } return false; } /** * @callback IIsFoundNumberFunc * @param {number} res - Value of the found value */ /** * @callback INotFoundFunc * @param {*} msg - message * @return {any} */ /** * check the type of the message * @param {*} msg message * @param {string|Array.<string>} ids property names to check * @param {string|Array.<string>} [names] topic names to check * @param {IIsFoundNumberFunc} [isFound] if the topic is found this function will be called with the found value * @param {INotFoundFunc} [notFound] topic names to check * @return {number|any} */ function getMsgNumberValue(msg, ids, names, isFound, notFound) { if (ids && msg) { if (!Array.isArray(ids)) { ids = [ids]; } for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (msg.payload && (typeof msg.payload[id] !== 'undefined') && (msg.payload[id] !== '')) { const res = Number(msg.payload[id]); // Number() instead of parseFloat() to also parse boolean if (!isNaN(res)) { if (typeof isFound === 'function') { return isFound(res); } return res; } } if ((typeof msg[id] !== 'undefined') && (msg[id] !== '')) { const res = Number(msg[id]); if (!isNaN(res)) { if (typeof isFound === 'function') { return isFound(res); } return res; } } } } if (names && msg && msg.topic) { const res = Number(msg.payload); if (!isNaN(res)) { if (!Array.isArray(names)) { names = [names]; } for (let i = 0; i < names.length; i++) { if (String(msg.topic).includes(String(names[i]))) { if (typeof isFound === 'function') { return isFound(res); } return res; } } } } if (typeof notFound === 'function') { return notFound(msg); } else if (typeof notFound === 'undefined') { return NaN; } return notFound; } /** * check the type of the message * @param {*} msg message * @param {string|Array.<string>} ids property names to check * @param {IIsFoundNumberFunc} [isFound] if the topic is found this function will be called with the found value * @param {INotFoundFunc} [notFound] topic names to check * @return {number|any} */ function getMsgNumberValue2(msg, ids, isFound, notFound) { if (ids && msg) { if (!Array.isArray(ids)) { ids = [ids]; } for (let i = 0; i < ids.length; i++) { const id = ids[i]; if ((typeof msg[id] !== 'undefined') && (msg[id] !== '')) { const res = Number(msg[id]); if (!isNaN(res)) { if (typeof isFound === 'function') { return isFound(res); } return res; } } } } if (typeof notFound === 'function') { return notFound(msg); } else if (typeof notFound === 'undefined') { return NaN; } return notFound; } /** * @callback IIsFoundBoolFunc * @param {boolean} result - found value converted to boolean * @param {*} realResult - real value which was found or the complete payload * @param {string} topic - topic of the message */ /** * check if the msg or msg.property contains a property with value true or the topic contains the given name * @param {*} msg message * @param {string|Array.<string>} ids property names to check * @param {string|Array.<string>} [names] topic names to check * @param {IIsFoundBoolFunc} [isFound] if the topic is found this function will be called with the found value * @param {INotFoundFunc} [notFound] topic names to check * @return {boolean|any} */ function getMsgBoolValue(msg, ids, names, isFound, notFound) { if (ids && msg) { if (!Array.isArray(ids)) { ids = [ids]; } for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (msg.payload && (typeof msg.payload[id] !== 'undefined') && (msg.payload[id] !== null) && (msg.payload[id] !== '')) { if (typeof isFound === 'function') { return isFound(isTrue(msg.payload[id]), msg.payload[id], msg.topic); } return isTrue(msg.payload[id]); } if ((typeof msg[id] !== 'undefined') && (msg[id] !== null) && (msg[id] !== '')) { if (typeof isFound === 'function') { return isFound(isTrue(msg[id]), msg[id], msg.topic); } return isTrue(msg[id]); } } } return getMsgTopicContains(msg, names, isFound, notFound); } /** * check if the msg contains a property with value true (no payload check) * @param {*} msg message * @param {string|Array.<string>} ids property names to check * @param {IIsFoundBoolFunc} [isFound] if the topic is found this function will be called with the found value * @param {INotFoundFunc} [notFound] topic names to check * @return {boolean|any} */ function getMsgBoolValue2(msg, ids, isFound, notFound) { if (ids && msg) { if (!Array.isArray(ids)) { ids = [ids]; } for (let i = 0; i < ids.length; i++) { const id = ids[i]; if ((typeof msg[id] !== 'undefined') && (msg[id] !== null) && (msg[id] !== '')) { if (typeof isFound === 'function') { return isFound(isTrue(msg[id]), msg[id], msg.topic); } return isTrue(msg[id]); } } } if (typeof notFound === 'function') { return notFound(msg); } else if (typeof notFound === 'undefined') { return false; } return notFound; } /** * check if thetopic contains one of the given names * @param {*} msg message * @param {string|Array.<string>} [names] topic names to check * @param {IIsFoundBoolFunc} [isFound] if the topic is found this function will be called with the found value * @param {INotFoundFunc} [notFound] topic names to check * @return {boolean} */ function getMsgTopicContains(msg, names, isFound, notFound) { if (msg && names) { if (!Array.isArray(names)) { names = [names]; } const topic = String(msg.topic); for (let i = 0; i < names.length; i++) { if (topic.includes(String(names[i]))) { if (typeof isFound === 'function') { return isFound(true, msg.payload, topic); } return true; } } } if (typeof notFound === 'function') { return notFound(msg); } else if (typeof notFound === 'undefined') { return false; } return notFound; } /*******************************************************************************************************/ /** * get the standard timezone offset without DST * @param {Date} d - Date to check * @returns {number} minutes of the timezone offset */ function getStdTimezoneOffset(d) { d = d || new Date(); const jan = new Date(d.getFullYear(),0,1); const jul = new Date(d.getFullYear(), 6, 1); return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); } /** * check mif a given Date is DST * @param {Date} d - Date to check * @returns {boolean} _true_ if the given Date has DST */ function isDSTObserved(d) { d = d || new Date(); return d.getTimezoneOffset() < getStdTimezoneOffset(d); } /** * changes the time based on a timezone * @param {Date} date Javascript Date object * @param {number} timeZoneOffset Offset in Minutes * @return {date} new date object with changed timezone to use with .toLocaleString() */ function convertDateTimeZone(date, timeZoneOffset) { const localTime = date.getTime(); const localOffset = date.getTimezoneOffset() * TIME_1min; const utc = localTime + localOffset; const destTime = utc + (TIME_1min * timeZoneOffset); return new Date(destTime); } /** * checks if a value is a valid Date object * @param {*} d - a value to check * @returns {boolean} returns __true__ if it is a valid Date, otherwhise __false__ */ function isValidDate(d) { // @ts-ignore return d instanceof Date && !isNaN(d); // d !== 'Invalid Date' && !isNaN(d) } /** * Parse an ISO date string (i.e. "2019-01-18T00:00:00.000Z", * "2019-01-17T17:00:00.000-07:00", or "2019-01-18T07:00:00.000+07:00", * which are the same time) and return a JavaScript Date object with the * value represented by the string. * @param {string} isoString - a ISO 8601 format string * @returns {Date} returns Date represtntation of the string */ function isoStringToDate( isoString ) { // Split the string into an array based on the digit groups. const dateParts = isoString.split( /\D+/ ); // Set up a date object with the current time. const returnDate = new Date(); // Manually parse the parts of the string and set each part for the // date. Note: Using the UTC versions of these functions is necessary // because we're manually adjusting for time zones stored in the // string. returnDate.setUTCFullYear( parseInt( dateParts[ 0 ] ) ); // The month numbers are one "off" from what normal humans would expect // because January == 0. returnDate.setUTCMonth( parseInt( dateParts[ 1 ] ) - 1 ); returnDate.setUTCDate( parseInt( dateParts[ 2 ] ) ); // Set the time parts of the date object. returnDate.setUTCHours( parseInt( dateParts[ 3 ] ) ); returnDate.setUTCMinutes( parseInt( dateParts[ 4 ] ) ); returnDate.setUTCSeconds( parseInt( dateParts[ 5 ] ) ); returnDate.setUTCMilliseconds( parseInt( dateParts[ 6 ] ) ); // Track the number of hours we need to adjust the date by based // on the timezone. let timezoneOffsetHours = 0; // If there's a value for either the hours or minutes offset. if ( dateParts[ 7 ] || dateParts[ 8 ] ) { // Track the number of minutes we need to adjust the date by // based on the timezone. let timezoneOffsetMinutes = 0; // If there's a value for the minutes offset. if ( dateParts[ 8 ] ) { // Convert the minutes value into an hours value. timezoneOffsetMinutes = parseInt( dateParts[ 8 ] ) / 60; } // Add the hours and minutes values to get the total offset in // hours. timezoneOffsetHours = parseInt( dateParts[ 7 ] ) + timezoneOffsetMinutes; // If the sign for the timezone is a plus to indicate the // timezone is ahead of UTC time. if ( isoString.substr( -6, 1 ) === '+' ) { // Make the offset negative since the hours will need to be // subtracted from the date. timezoneOffsetHours *= -1; } } // Get the current hours for the date and add the offset to get the // correct time adjusted for timezone. returnDate.setHours( returnDate.getHours() + timezoneOffsetHours ); // Return the Date object calculated from the string. return returnDate; } /* like previous isoStringToDate, but smaller const parseDate = dateString => { const b = dateString.split(/\D+/); const offsetMult = dateString.indexOf('+') !== -1 ? -1 : 1; const hrOffset = offsetMult * (+b[7] || 0); const minOffset = offsetMult * (+b[8] || 0); return new Date(Date.UTC(+b[0], +b[1] - 1, +b[2], +b[3] + hrOffset, +b[4] + minOffset, +b[5], +b[6] || 0)); }; */ /** * Round a date to the nearest full Hour * @param {Date} date Date to round * @returns {Date} Date round to next full Hour */ function roundToHour(date) { return new Date(Math.round(date.getTime() / TIME_1h ) * TIME_1h); } /*******************************************************************************************************/ /** * adds an offset to a given Date object (Warning: No copy of Date Object will be created and original Date Object could be changed!!) * @param {Date} d Date object where the offset should be added * @param {number} offset the offset (positive or negative) which should be added to the date. If no multiplier is given, the offset must be in milliseconds. * @param {number} [multiplier] additional multiplier for the offset. Should be a positive Number. Special value -1 if offset is in month and -2 if offset is in years * @return {Date|undefined|null} Date with added offset */ function addOffset(d, offset, multiplier) { if (offset && !isNaN(offset) && offset !== 0) { if (multiplier > 0) { return new Date(d.getTime() + Math.floor(offset * multiplier)); } else if (multiplier === -1) { d.setMonth(Math.floor(d.getMonth() + offset)); } else if (multiplier === -2) { d.setFullYear(Math.floor(d.getFullYear() + offset)); } else { return new Date(d.getTime() + Math.floor(offset)); // if multiplier is not a valid value } } return d; } /*******************************************************************************************************/ /** * calculates the number of days to get a positive date object * @param {Array.<number>} days array of allowed days * @param {number} daystart start day (0=Sunday) * @return {number} number of days for the next valid day as offset to the daystart */ function calcDayOffset(days, daystart) { let dayx = 0; while (!days.includes(daystart + dayx)) { dayx++; if ((daystart + dayx) > 6) { daystart = (dayx * -1); } if (dayx > 7) { dayx = -1; break; } } return dayx; } /** * calculates the number of month to get a positive date object * @param {Array.<number>} months array of allowed months * @param {number} monthstart start month (0=January) * @return {number} number of months for the next valid day as offset to the monthstart */ function calcMonthOffset(months, monthstart) { let monthx = 0; while (!months.includes(monthstart + monthx)) { monthx++; if ((monthstart + monthx) > 11) { monthstart = (monthx * -1); } if (monthx > 12) { monthx = -1; break; } } return monthx; } /*******************************************************************************************************/ /** * normalize date by adding offset, get only the next valid date, etc... * @param {Date|number|object} d input Date to normalize * @param {number} offset offset to add tot he Date object * @param {number} multiplier multiplier for the offset * @param {ILimitationsObj} [limit] additional limitations for the calculation * @return {Date} a normalized date moved tot the future to fulfill all conditions */ function normalizeDate(d, offset, multiplier, limit) { // console.debug(`normalizeDate d=${d} offset=${ offset }, multiplier=${multiplier}, limit=${limit}`); // eslint-disable-line if (d === null || typeof d === 'undefined') { return d; } if (d.value) { d = d.value; } if (!(d instanceof Date)) { d = new Date(d); } d = addOffset(d, offset, multiplier); if (limit.next) { const dNow = new Date(); d.setMilliseconds(0); dNow.setMilliseconds(600); // security const cmp = dNow.getTime(); while (d.getTime() <= cmp) { d.setDate(d.getDate() + 1); } } const r = limitDate(limit, d); if (r.hasChanged) { return r.date; } return d; } /** * calculates limitation of a date * @param {ILimitationsObj} limit - limitation object * @param {Date} d - Date to check * @returns {ILimitedDate} result limited Date Object. */ function limitDate(limit, d) { let hasChanged = false; let error = ''; if (typeof(limit.days) === 'string') { if ((limit.days === '*') || (limit.days === '')) { delete limit.months; } else { const tmp = limit.days.split(','); limit.days = []; tmp.forEach(element => { const el = parseInt(element.trim()); if (!isNaN(el)) { // @ts-ignore limit.days.push(el); } }); } } if (limit.days && Array.isArray(limit.days)) { const dayx = calcDayOffset(limit.days, d.getDay()); if (dayx > 0) { d.setDate(d.getDate() + dayx); hasChanged = true; } else if (dayx < 0) { error = 'No valid day of week found!'; } } if (typeof(limit.months) === 'string') { if ((limit.months === '*') || (limit.months === '')) { delete limit.months; } else { const tmp = limit.months.split(','); limit.months = []; tmp.forEach(element => { const el = parseInt(element.trim()); if (!isNaN(el)) { // @ts-ignore limit.months.push(el); } }); } } if (limit.months && Array.isArray(limit.months)) { const monthx = calcMonthOffset(limit.months, d.getMonth()); if (monthx > 0) { d.setMonth(d.getMonth() + monthx); hasChanged = true; } else if (monthx < 0) { error = 'No valid month found!'; } } if (isTrue(limit.onlyEvenDays) && !isTrue(limit.onlyOddDays)) { // eslint-disable-line eqeqeq let time = d.getDate(); while ((time % 2 !== 0)) { // odd d.setDate(d.getDate() + 1); time = d.getDate(); hasChanged = true; } } if (isTrue(limit.onlyOddDays) && !isTrue(limit.onlyEvenDays)) { // eslint-disable-line eqeqeq let time = d.getDate(); while((time % 2 === 0)) { // even d.setDate(d.getDate() + 1); time = d.getDate(); hasChanged = true; } } if (isTrue(limit.onlyEvenWeeks) && !isTrue(limit.onlyOddWeeks)) { // eslint-disable-line eqeqeq let week = getWeekOfYear(d); while (week[1] % 2 !== 0) { // odd d.setDate(d.getDate() + 1); week = getWeekOfYear(d); hasChanged = true; } } if (isTrue(limit.onlyOddWeeks) && !isTrue(limit.onlyEvenWeeks)) { // eslint-disable-line eqeqeq let week = getWeekOfYear(d); while(week[1] % 2 === 0) { // even d.setDate(d.getDate() + 1); week = getWeekOfYear(d); hasChanged = true; } } return {date:d, hasChanged, error}; } /*******************************************************************************************************/ /** * parses a string which contains only a time to a Date object of today * @param {string} t text representation of a time * @param {Date} [date] bade Date object for parsing the time, now will be used if not defined * @param {boolean} [utc] define if the time should be in utc * @param {number} [timeZoneOffset] define a time zone offset if required * @return {Date|null} the parsed date object or **null** if can not parsed */ function getTimeOfText(t, date, utc, timeZoneOffset) { // console.debug(`getTimeOfText t=${ t } date=${ date.toISOString() } utc=${ utc } timeZoneOffset=${ timeZoneOffset }`); // eslint-disable-line const d = date ? new Date(date) : new Date(); if (t && (!t.includes('.')) && (!t.includes('-'))) { t = t.toLocaleLowerCase(); // const matches = t.match(/(0\d|1\d|2[0-3]|\d)(?::([0-5]\d|\d))(?::([0-5]\d|\d))?\s*(p?)/); if (t.includes('utc')) { utc = true; t = t.replace('utc','').trim(); } if (t.includes('local')) { utc = false; t = t.replace('local','').trim(); } const matches = t.match(/(0\d|1\d|2[0-3]|\d)(?::([0-5]\d|\d))(?::([0-5]\d|\d))?\s*(p?)(?:a|am|m|\s)?\s*(?:(\+|-)(\d\d):([0-5]\d)|(\+|-)(\d\d\d\d?))?/); if (matches) { if (utc || matches[5] || matches[6] || matches[9] ) { // matches[5] - GMT, UTC or Z; matches[6] - +/-; matches[9] - +/- d.setUTCHours((parseInt(matches[1]) + (matches[4] ? 12 : 0)), (parseInt(matches[2]) || 0), (parseInt(matches[3]) || 0), 0); if (matches[6] && matches[7] && matches[8]) { // +00:00 - timezone offset const v = ((parseInt(matches[7]) * 60) + (matches[8] ? parseInt(matches[8]) : 0)) * ((matches[6] === '-') ? -1 : 1); const m = d.getMinutes() + v; d.setMinutes(m); } if (matches[9] && matches[10]) { // +0000 or +000 const v = (parseInt(matches[10])) * ((matches[9] === '-') ? -1 : 1); const m = d.getMinutes() + v; d.setMinutes(m); } return d; // timeZoneOffset will be ignored! } d.setHours((parseInt(matches[1]) + (matches[4] ? 12 : 0)), (parseInt(matches[2]) || 0), (parseInt(matches[3]) || 0), 0); if (timeZoneOffset) { return convertDateTimeZone(d, timeZoneOffset); } return d; } } return null; } /*******************************************************************************************************/ /** * parses a string which contains a date or only a time to a Date object * @param {any} dt number or text which contains a date or a time * @param {boolean} preferMonthFirst if true, Dates with moth first should be preferd, otherwise month last (european) * @param {boolean} [utc] indicates if the date should be in utc * @param {number} [timeZoneOffset] timezone offset in minutes of the input date * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @return {Date} the parsed date object, throws an error if can not parsed */ function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset, dNow) { // console.debug('getDateOfText dt=' + util.inspect(dt, { colors: true, compact: 10, breakLength: Infinity })); // eslint-disable-line if (dt === null || typeof dt === 'undefined') { throw new Error('Could not evaluate as a valid Date or time. Value is null or undefined!'); } else if (dt === '') { if (timeZoneOffset) { return convertDateTimeZone(new Date(), timeZoneOffset); } return new Date(); } if (typeof dt === 'object') { dt = String(dt); } if (!isNaN(dt)) { dt = Number(dt); let dto = new Date(dt); if (utc || timeZoneOffset === 0) { // @ts-ignore dto = Date.UTC(dt); } else if (timeZoneOffset) { dto = convertDateTimeZone(dto, timeZoneOffset); } if (isValidDate(dto)) { return dto; } } // @ts-ignore const result = getTimeOfText(String(dt), utc, timeZoneOffset); if (result !== null && typeof result !== 'undefined') { return result; } if (typeof dt === 'string') { let res = _parseDateTime(dt, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } res = _parseDate(dt, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } // @ts-ignore res = _parseArray(dt, _dateFormat.parseTimes, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } if (utc || timeZoneOffset === 0) { if (!dt.includes('UTC') && !dt.includes('utc') && !dt.includes('+') && !dt.includes('-')) { dt += ' UTC'; } } if (utc || timeZoneOffset === 0) { if (!dt.includes('+') && !dt.includes('-')) { if (timeZoneOffset < 0) { dt += '-'; } else { dt += '+'; } dt += pad2(Math.floor(Math.abs(timeZoneOffset) / 60)) + ':' + pad2(Math.abs(timeZoneOffset) % 60); } } // @ts-ignore res = Date.parse(dt); // @ts-ignore if (!isNaN(res)) { return res; } } throw new Error('could not evaluate ' + String(dt) + ' as a valid Date or time.'); } /*******************************************************************************************************/ /* * Date Format 1.2.3 * (c) 2007-2009 Steven Levithan <stevenlevithan.com> * MIT license * * Includes enhancements by Scott Trenda <scott.trenda.net> * and Kris Kowal <cixar.com/~kris.kowal/> * * Accepts a date, a mask, or a date and a mask. * Returns a formatted version of the given date. * The date defaults to the current date/time. * * http://blog.stevenlevithan.com/archives/date-time-format * http://stevenlevithan.com/assets/misc/date.format.js */ // Regexes and supporting functions are cached through closure /** * Formate a date to the given Format string * @param {Date} date - JavaScript Date to format * @param {string} mask - mask of the date * @param {boolean} [utc] - if true the time should be delivered as UTC * @param {number} [timeZoneOffset] - define a different timezoneOffset in Minutes for output * @return {string} date as depending on the given Format */ const _dateFormat = (function () { const token = /x{1,2}|d{1,4}|E{1,2}|M{1,4}|NNN|yy(?:yy)?|([HhKkmsTt])\1?|l{1,3}|[LSZ]|z{1,2}|o{1,4}|ww|w|dy|ddy|"[^"]*"|'[^']*'/g; // const timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g; // const timezoneClip = /[^-+\dA-Z]/g; // Regexes and supporting functions are cached through closure return function (date, mask, utc, timeZoneOffset) { const dF = _dateFormat; // You can't provide utc if you skip other Args. (use the "UTC:" mask prefix) if (arguments.length === 1 && Object.prototype.toString.call(date) === '[object String]' && !/\d/.test(date)) { mask = date; date = undefined; } // Passing date through Date applies Date.parse, if necessary const dNow = new Date(); date = date ? new Date(date) : dNow; if (!isValidDate(date)) { throw new SyntaxError('invalid date'); } const dayDiff = (date.getDate() - dNow.getDate()); if (timeZoneOffset === 0) { utc = true; } else if (timeZoneOffset) { date = convertDateTimeZone(date, timeZoneOffset); } // @ts-ignore mask = String(mask || _dateFormat.isoDateTime); // Allow setting the utc argument via the mask if (mask.slice(0, 4) === 'UTC:' || mask.slice(0, 4) === 'utc:') { mask = mask.slice(4); utc = true; } const _ = utc ? 'getUTC' : 'get'; const d = date[_ + 'Date'](); const D = date[_ + 'Day'](); const M = date[_ + 'Month'](); const y = date[_ + 'FullYear'](); const H = date[_ + 'Hours'](); // 0-23 const m = date[_ + 'Minutes'](); const s = date[_ + 'Seconds'](); const l = date[_ + 'Milliseconds'](); const o = utc ? 0 : ( (timeZoneOffset) ? timeZoneOffset : date.getTimezoneOffset()); const flags = { d, dd