node-red-contrib-sun-position
Version:
NodeRED nodes to get sun and moon position
1,375 lines (1,287 loc) • 94 kB
JavaScript
// @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