node-red-contrib-chronos
Version:
Time-based Node-RED scheduling, repeating, queueing, routing, filtering and manipulating nodes
542 lines (461 loc) • 22.1 kB
JavaScript
/*
* Copyright (c) 2020 - 2025 Jens-Uwe Rossbach
*
* This code is licensed under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const sinon = require("sinon");
const chronos = require("../../nodes/common/chronos.js");
const moment = require("moment");
const sunCalc = require("suncalc");
const jsonata = require("jsonata");
require("should-sinon");
describe("chronos", function()
{
afterEach(function()
{
sinon.restore();
});
context("initCustomTimes", function()
{
it("should call sunCalc.addTime", function()
{
const sunPositions =
[
{
angle: 5,
riseName: "testRise1",
setName: "testSet1"
},
{
angle: -3,
riseName: "testRise2",
setName: "testSet2"
}
];
sinon.spy(sunCalc, "addTime");
chronos.initCustomTimes(sunPositions);
sunCalc.addTime.should.be.calledWith(5, "__cust_testRise1", "__cust_testSet1");
sunCalc.addTime.should.be.calledWith(-3, "__cust_testRise2", "__cust_testSet2");
});
});
context("getCurrentTime", function()
{
it("should return current time", function()
{
// fake current time to be deterministic
sinon.stub(Date, "now").returns(new Date("2000-01-01T00:00:00.000Z"));
const node = {locale: "en-US", config: {timezone: ""}};
const res = chronos.getCurrentTime(node);
res.valueOf().should.equal(946684800000);
});
});
context("getTimeFrom", function()
{
it("should return time from string", function()
{
const node = {locale: "en-US", config: {timezone: ""}};
const res = chronos.getTimeFrom(node, "2000-01-01T00:00:00.000Z");
res.valueOf().should.equal(946684800000);
});
it("should return current time from string with custom time zone", function()
{
const node = {locale: "de-DE", config: {timezone: "Europe/Berlin"}};
const res = chronos.getTimeFrom(node, "2000-01-01T01:00:00.000");
res.valueOf().should.equal(946684800000);
});
it("should return time from Unix epoch", function()
{
const node = {locale: "en-US", config: {timezone: ""}};
const res = chronos.getTimeFrom(node, 946684800000);
res.valueOf().should.equal(946684800000);
});
});
context("getUserDate", function()
{
it("should return date from string", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {timezone: ""}};
const res = chronos.getUserDate(RED, node, "2000-01-01");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
});
it("should fail due to invalid date string", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US"};
(() => chronos.getUserDate(RED, node, "2000/01-01")).should.throw(chronos.TimeError);
});
});
context("isValidUserTime", function()
{
it("should return true for valid time", function()
{
chronos.isValidUserTime("8:10").should.be.true();
chronos.isValidUserTime("08:10").should.be.true();
chronos.isValidUserTime("5:37:12").should.be.true();
chronos.isValidUserTime("16:23:12").should.be.true();
chronos.isValidUserTime("8:10 AM").should.be.true();
chronos.isValidUserTime("9:12 PM").should.be.true();
chronos.isValidUserTime("7:23:12 AM").should.be.true();
chronos.isValidUserTime("7:23:12 am").should.be.true();
chronos.isValidUserTime("7:23:12 PM").should.be.true();
chronos.isValidUserTime("7:23:12 pm").should.be.true();
chronos.isValidUserTime(0).should.be.true();
chronos.isValidUserTime(100000).should.be.true();
chronos.isValidUserTime((24*60*60*1000)-1).should.be.true();
});
it("should return false for invalid time", function()
{
chronos.isValidUserTime("invalid").should.be.false();
chronos.isValidUserTime("8:87").should.be.false();
chronos.isValidUserTime("25:17:12").should.be.false();
chronos.isValidUserTime("16.23:12").should.be.false();
chronos.isValidUserTime("7:65 am").should.be.false();
chronos.isValidUserTime("56:56:12 am").should.be.false();
chronos.isValidUserTime("7:23:12 xm").should.be.false();
chronos.isValidUserTime(-1).should.be.false();
chronos.isValidUserTime(24*60*60*1000).should.be.false();
chronos.isValidUserTime(true).should.be.false();
chronos.isValidUserTime({a: "b"}).should.be.false();
chronos.isValidUserTime([1,2,3]).should.be.false();
});
});
context("isValidUserDate", function()
{
it("should return true for valid date", function()
{
chronos.isValidUserDate("2021-03-07").should.be.true();
chronos.isValidUserDate("2000-4-3").should.be.true();
});
it("should return false for invalid date", function()
{
chronos.isValidUserDate("2020/4/3").should.be.false();
chronos.isValidUserDate("27.10.2018").should.be.false();
chronos.isValidUserDate("100-03-03").should.be.false();
chronos.isValidUserDate("2016-25-14").should.be.false();
chronos.isValidUserDate("2010-06-45").should.be.false();
});
});
context("getTime", function()
{
it("should return user time", function()
{
// fake current time to be deterministic
sinon.stub(Date, "now").returns(new Date("2000-01-01T11:22:33.444Z"));
const RED = {"_": () => ""};
const node = {};
let res = chronos.getTime(RED, node, moment(), "time", "16:20");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(16);
res.minute().should.equal(20);
res.second().should.equal(0);
res.millisecond().should.equal(0);
res = chronos.getTime(RED, node, moment(), "time", "8:15:30");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(8);
res.minute().should.equal(15);
res.second().should.equal(30);
res.millisecond().should.equal(0);
res = chronos.getTime(RED, node, moment(), "time", "8:20 AM");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(8);
res.minute().should.equal(20);
res.second().should.equal(0);
res.millisecond().should.equal(0);
res = chronos.getTime(RED, node, moment(), "time", "14:20 AM");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(2);
res.minute().should.equal(20);
res.second().should.equal(0);
res.millisecond().should.equal(0);
res = chronos.getTime(RED, node, moment(), "time", "9:30 PM");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(21);
res.minute().should.equal(30);
res.second().should.equal(0);
res.millisecond().should.equal(0);
res = chronos.getTime(RED, node, moment(), "time", "15:30 PM");
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(15);
res.minute().should.equal(30);
res.second().should.equal(0);
res.millisecond().should.equal(0);
res = chronos.getTime(RED, node, moment(), "time", 59100000);
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(16);
res.minute().should.equal(25);
res.second().should.equal(0);
res.millisecond().should.equal(0);
});
it("should fail due to invalid user time", function()
{
// fake current time to be deterministic
sinon.stub(Date, "now").returns(new Date("2000-01-01T00:00:00.000Z"));
const RED = {"_": () => ""};
const node = {};
(() => chronos.getTime(RED, node, moment(), "time", "25:20")).should.throw(chronos.TimeError);
(() => chronos.getTime(RED, node, moment(), "time", "12.20:10")).should.throw(chronos.TimeError);
(() => chronos.getTime(RED, node, moment(), "time", true)).should.throw(chronos.TimeError);
});
it("should return sun time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getTimes").returns({"sunrise": new Date("2000-01-01T08:00:00.000Z")});
let res = chronos.getTime(RED, node, moment(), "sun", "sunrise");
res.utc();
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(8);
res.minute().should.equal(0);
res.second().should.equal(0);
});
it("should return sun time with custom time zone", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0, timezone: "Asia/Dubai"}};
sinon.stub(sunCalc, "getTimes").returns({"sunrise": new Date("2000-01-01T08:00:00.000Z")});
let res = chronos.getTime(RED, node, moment(), "sun", "sunrise");
res.utcOffset().should.equal(240); // +4h
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(12);
res.minute().should.equal(0);
res.second().should.equal(0);
});
it("should return custom sun time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getTimes").returns({"__cust_test": new Date("2000-01-01T08:00:00.000Z")});
let res = chronos.getTime(RED, node, moment(), "custom", "test");
res.utc();
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(8);
res.minute().should.equal(0);
res.second().should.equal(0);
});
it("should fail due to invalid sun time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getTimes")
.onFirstCall()
.returns({"sunrise": new Date("2000-01-01T08:00:00.000Z")})
.onSecondCall()
.returns({"sunrise": [2010, 12]});
(() => chronos.getTime(RED, node, moment(), "sun", "invalid")).should.throw(chronos.TimeError);
(() => chronos.getTime(RED, node, moment(), "sun", "sunrise")).should.throw(chronos.TimeError);
});
it("should fail due to unavailable sun time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getTimes").returns({"sunrise": null});
(() => chronos.getTime(RED, node, moment(), "sun", "sunrise")).should.throw(chronos.TimeError);
});
it("should return moon time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getMoonTimes").returns({"rise": new Date("2000-01-01T22:00:00.000Z")});
let res = chronos.getTime(RED, node, moment(), "moon", "rise");
res.utc();
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(22);
res.minute().should.equal(0);
res.second().should.equal(0);
});
it("should return moon time with custom time zone", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0, timezone: "Asia/Dubai"}};
sinon.stub(sunCalc, "getMoonTimes").returns({"rise": new Date("2000-01-01T18:00:00.000Z")});
let res = chronos.getTime(RED, node, moment(), "moon", "rise");
res.utcOffset().should.equal(240); // +4h
res.year().should.equal(2000);
res.month().should.equal(0);
res.date().should.equal(1);
res.hour().should.equal(22);
res.minute().should.equal(0);
res.second().should.equal(0);
});
it("should fail due to invalid moon time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getMoonTimes")
.onFirstCall()
.returns({"rise": new Date("2000-01-01T22:00:00.000Z")})
.onSecondCall()
.returns({"rise": [2010, 12]});
(() => chronos.getTime(RED, node, moment(), "moon", "invalid")).should.throw(chronos.TimeError);
(() => chronos.getTime(RED, node, moment(), "moon", "rise")).should.throw(chronos.TimeError);
});
it("should fail due to unavailable moon time", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
sinon.stub(sunCalc, "getMoonTimes").returns({"rise": null, alwaysUp: false, alwaysDown: true});
(() => chronos.getTime(RED, node, moment(), "moon", "rise")).should.throw(chronos.TimeError);
});
it("should do nothing due to invalid type", function()
{
const RED = {"_": () => ""};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
chronos.getTime(RED, node, moment(), "invalid", "abc");
// TODO: how to validate best?
});
});
context("getJSONataExpression", function()
{
it("should return expression", function()
{
const jsnExpr = {testToken: "chronos", registerFunction: sinon.stub()};
const RED = {util: {prepareJSONataExpression: sinon.stub().returns(jsnExpr)}};
const node = "fake";
let expr = chronos.getJSONataExpression(RED, node, "my expression");
expr.should.have.property("testToken", "chronos");
RED.util.prepareJSONataExpression.should.be.calledWith("my expression", node);
jsnExpr.registerFunction.should.have.callCount(14);
});
it("should register functions", function()
{
const RED = {"_": () => "", util: {prepareJSONataExpression: function(expr, node)
{
return jsonata(expr);
}}};
const node = {locale: "en-US", config: {latitude: 0, longitude: 0}};
const ts = chronos.getTimeFrom(node, "2021-12-02T12:34:56.789").valueOf();
let expr = chronos.getJSONataExpression(RED, node, "$millisecond($ts)");
expr.assign("ts", ts);
let result = expr.evaluate({});
result.should.equal(789);
expr = chronos.getJSONataExpression(RED, node, "$second($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(56);
expr = chronos.getJSONataExpression(RED, node, "$minute($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(34);
expr = chronos.getJSONataExpression(RED, node, "$hour($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(12);
expr = chronos.getJSONataExpression(RED, node, "$day($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(2);
expr = chronos.getJSONataExpression(RED, node, "$dayOfWeek($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(5);
expr = chronos.getJSONataExpression(RED, node, "$dayOfYear($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(336);
expr = chronos.getJSONataExpression(RED, node, "$week($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(49);
expr = chronos.getJSONataExpression(RED, node, "$month($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(12);
expr = chronos.getJSONataExpression(RED, node, "$quarter($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(4);
expr = chronos.getJSONataExpression(RED, node, "$year($ts)");
expr.assign("ts", ts);
result = expr.evaluate({});
result.should.equal(2021);
expr = chronos.getJSONataExpression(RED, node, "$time($ts, '11:22')");
expr.assign("ts", ts);
result = moment(expr.evaluate({}));
result.add(result.utcOffset(), "minutes");
result.valueOf().should.equal(1638444120000);
expr = chronos.getJSONataExpression(RED, node, "$time($ts, '11:22', 0, false)");
expr.assign("ts", ts);
result = moment(expr.evaluate({}));
result.add(result.utcOffset(), "minutes");
result.valueOf().should.equal(1638444120000);
expr = chronos.getJSONataExpression(RED, node, "$time($ts, '11:22', 10, false)");
expr.assign("ts", ts);
result = moment(expr.evaluate({}));
result.add(result.utcOffset(), "minutes");
result.valueOf().should.equal(1638444720000);
sinon.stub(Math, "random").returns(0.5);
expr = chronos.getJSONataExpression(RED, node, "$time($ts, '11:22', 20, true)");
expr.assign("ts", ts);
result = moment(expr.evaluate({}));
result.add(result.utcOffset(), "minutes");
result.valueOf().should.equal(1638444720000);
sinon.stub(sunCalc, "getTimes").returns({"sunset": new Date("2000-01-01T11:22:33.444Z")});
expr = chronos.getJSONataExpression(RED, node, "$sunTime($ts, 'sunset', 0, false)");
expr.assign("ts", ts);
result = expr.evaluate({});
sunCalc.getTimes.should.be.calledOnce();
result.should.equal(946725753444);
sunCalc.getTimes.resetHistory();
expr = chronos.getJSONataExpression(RED, node, "$sunTime($ts, 'sunset')");
expr.assign("ts", ts);
result = expr.evaluate({});
sunCalc.getTimes.should.be.calledOnce();
result.should.equal(946725753444);
sinon.stub(sunCalc, "getMoonTimes").returns({"rise": new Date("2000-01-01T11:22:33.444Z")});
expr = chronos.getJSONataExpression(RED, node, "$moonTime($ts, 'rise', 0, false)");
expr.assign("ts", ts);
result = expr.evaluate({});
sunCalc.getTimes.should.be.calledOnce();
result.should.equal(946725753444);
sunCalc.getMoonTimes.resetHistory();
expr = chronos.getJSONataExpression(RED, node, "$moonTime($ts, 'rise')");
expr.assign("ts", ts);
result = expr.evaluate({});
sunCalc.getTimes.should.be.calledOnce();
result.should.equal(946725753444);
});
});
});