505 wiersze
22 KiB
JavaScript

/**
* Copyright 2018 Bart Butenaers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
function CalculatorNode(config) {
RED.nodes.createNode(this, config);
this.inputMsgField = config.inputMsgField;
this.outputMsgField = config.outputMsgField;
this.operation = config.operation;
this.constant = config.constant;
this.round = config.round;
this.decimals = config.decimals;
var node = this;
// Test if an object contains the specified property (handles multiple levels like obj.a.b.c).
// (See https://www.customd.com/articles/37/checking-javascript-objects-for-existence-of-a-nested-element )
function objectHasProperty(obj, prop) {
var parts = prop.split('.');
for (var i = 0, l = parts.length; i < l; i++) {
var part = parts[i];
if ((obj !== null) && (typeof(obj) === 'object') && (part in obj)) {
obj = obj[part];
}
else {
return false;
}
}
return true;
}
// https://www.jacklmoore.com/notes/rounding-in-javascript/
function round(value, decimals) {
return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
}
// Check whether the input is correct
function checkInput(checkNumber, inputValue, minCount, maxCount) {
var values = [];
var numbers = [];
var isArray = Array.isArray(inputValue);
if (!isArray) {
if (minCount > 1) {
node.error("The msg." + this.inputMsgField + " should be an array");
return null;
}
// Seems we have enough with an array containing a single item
values.push(inputValue);
}
else {
// Let's check all the numbers in the array
values = inputValue;
}
if (maxCount && minCount === maxCount) {
if (values.length !== minCount) {
if (node.constant) {
node.error("The msg." + this.inputMsgField + " should be an array with " + (--minCount) + " numbers (because constant value specified)");
}
else {
node.error("The msg." + this.inputMsgField + " should be an array with " + minCount + " numbers");
}
return null;
}
}
else {
if (values.length < minCount) {
if (node.constant) {
node.error("The msg." + this.inputMsgField + " should be an array with minimum " + (--minCount) + " numbers (because constant value specified)");
}
else {
node.error("The msg." + this.inputMsgField + " should be an array with minimum " + minCount + " numbers");
}
return null;
}
if (maxCount && values.length > maxCount) {
if (node.constant) {
node.error("The msg." + this.inputMsgField + " should be an array with maximum " + (--maxCount) + " numbers (because constant value specified)");
}
else {
node.error("The msg." + this.inputMsgField + " should be an array with maximum " + maxCount + " numbers");
}
return null;
}
}
for (var i = 0; i < values.length; i++) {
var number = parseFloat(values[i]);
if (checkNumber && isNaN(number)){
node.error("The msg." + this.inputMsgField + " should only contain number(s)");
return null;
}
numbers.push(number);
}
return numbers;
}
node.on("input", function(msg) {
var operation = node.operation;
var numbers = [];
var msgKeyValue;
var count;
var result;
if (!objectHasProperty(msg, node.inputMsgField)) {
node.error("The input message doesn't have have a msg." + node.inputMsgField + " field")
return null;
}
try {
msgKeyValue = RED.util.getMessageProperty(msg, node.inputMsgField);
}
catch(err) {
node.error("The msg." + node.inputMsgField + " field can not be read");
return;
}
// Check whether the input data is an arry.
// Remark: we won't take into account the constant value (below)
var isArray = Array.isArray(msgKeyValue);
// When a constant value is specified, this will be appended to the end of the array
if (node.constant) {
if (!isArray) {
// To be able to append the constantValue (as second value), we need to convert the number to an array with one number
msgKeyValue = [ msgKeyValue ];
}
msgKeyValue.push(parseFloat(node.constant));
}
if (!operation || operation === "") {
operation = msg.operation;
if (!operation) {
node.error("An msg.operation should be supplied");
return null;
}
}
switch(operation) {
case "abs":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.abs(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "acos":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.acos(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "acosh":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.acosh(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "asin":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.asin(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "asinh":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.asinh(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "atan":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.atan(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "atanh":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.atanh(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "avg":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
result = numbers.reduce(function(a, b) { return a + b; });
result = result / numbers.length;
break;
case "cbrt":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.cbrt(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "ceil":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.ceil(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "cos":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.cos(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "cosh":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.cosh(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "dec":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = a - 1;
});
result = (isArray) ? numbers : numbers[0];
break;
case "div":
numbers = checkInput(true, msgKeyValue, 2);
if (!numbers) return;
if (node.constant === 0) {
node.error("The constant value not be 0 (as denominator)");
return null;
}
for (var i = 1; i < numbers.length; i++) {
if (numbers[i] === 0) {
node.error("The msg." + node.inputMsgField + " should only contain non-zero number(s) for the denominators");
return null;
}
}
result = numbers.reduce(function(a, b) { return a / b; });
break;
case "exp":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.exp(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "inc":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = a + 1;
});
result = (isArray) ? numbers : numbers[0];
break;
case "floor":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.floor(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "log":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.log(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "log10":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.log10(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "max":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
result = numbers.reduce(function(a, b) { return (a > b) ? a : b });
break;
case "min":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
result = numbers.reduce(function(a, b) { return (a > b) ? b : a });
break;
case "mult":
numbers = checkInput(true, msgKeyValue, 2);
if (!numbers) return;
result = numbers.reduce(function(a, b) { return a * b; });
break;
case "mod":
numbers = checkInput(true, msgKeyValue, 2, 2);
if (!numbers) return;
result = numbers[0] % numbers[1];
break;
case "pow":
numbers = checkInput(true, msgKeyValue, 2, 2);
if (!numbers) return;
result = Math.pow(numbers[0], numbers[1]);
break;
case "rand":
// When the payload contains an array, then we will generate an array (with same length) of random numbers.
// Regardless of the content of the content of the payload array, since we don't need it for our calculations ...
if (isArray) {
numbers = new Array(msgKeyValue.length);
}
else {
numbers = new Array(1);
}
// Remark: 'forEach' does not work on an un-initialized array
for (var j = 0; j < numbers.length; j++) {
numbers[j] = Math.random();
}
result = (isArray) ? numbers : numbers[0];
break;
case "randb":
numbers = checkInput(true, msgKeyValue, 2, 2);
if (!numbers) return;
result = Math.floor(Math.random() * (numbers[1] - numbers[0] + 1)) + numbers[0];
break;
case "randa":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
result = numbers[Math.floor(Math.random() * numbers.length)];
break;
case "len":
numbers = checkInput(false, msgKeyValue, 1);
if (!numbers) return;
result = numbers.length;
break;
// Implementation of sorting, since the sort node does not behave correctly at this moment.
// (see https://github.com/akashtalole/node-red-contrib-sort/issues/1)
case "sorta":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.sort(function(a, b) {
return a - b;
});
result = numbers;
break;
case "sortd":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.sort(function(a, b) {
return b - a;
});
result = numbers;
break;
case "range":
numbers = checkInput(true, msgKeyValue, 2, 2);
if (!numbers) return;
//numbers[0] = Math.trunc(numbers[0]);
//numbers[1] = Math.trunc(numbers[1]);
result = [];
for (var k = numbers[0]; k <= numbers[1]; k++) {
result.push(k);
}
break;
case "dist":
numbers = checkInput(true, msgKeyValue, 2);
if (!numbers) return;
numbers.sort();
result = numbers[numbers.length - 1] - numbers[0];
break;
case "rdec":
numbers = checkInput(true, msgKeyValue, 2, 2);
if (!numbers) return;
// See http://www.jacklmoore.com/notes/rounding-in-javascript/
result = Number(Math.round(numbers[0] + 'e' + numbers[1]) + 'e-' + numbers[1]);
break;
case "round":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.round(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "sin":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.sin(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "sinh":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.sinh(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "sqrt":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.sqrt(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "sum":
numbers = checkInput(true, msgKeyValue, 2);
if (!numbers) return;
result = numbers.reduce(function(a, b) { return a + b; });
break;
case "sub":
numbers = checkInput(true, msgKeyValue, 2);
if (!numbers) return;
result = numbers.reduce(function(a, b) { return a - b; });
break;
case "tan":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.tan(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "tanh":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.tanh(a);
});
result = (isArray) ? numbers : numbers[0];
break;
case "trunc":
numbers = checkInput(true, msgKeyValue, 1);
if (!numbers) return;
numbers.forEach(function(a, index) {
numbers[index] = Math.trunc(a);
});
result = (isArray) ? numbers : numbers[0];
break;
default:
node.error("The msg.operation contains an unsupported operation '" + operation + "'");
return null;
}
// If required, round the result to the specified number of decimals
if (node.round) {
if (Array.isArray(result)) {
for (var j = 0; j < result.length; j++) {
result[j] = round(result[j], node.decimals);
}
}
else {
result = round(result, node.decimals);
}
}
RED.util.setMessageProperty(msg, node.outputMsgField, result, true);
node.send(msg);
});
}
RED.nodes.registerType("calculator", CalculatorNode);
}