123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- /**
- * 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);
- }
|