Node-Red configuration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

math.js 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. /**
  2. * Copyright 2018 Bart Butenaers
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. **/
  16. module.exports = function(RED) {
  17. function CalculatorNode(config) {
  18. RED.nodes.createNode(this, config);
  19. this.inputMsgField = config.inputMsgField;
  20. this.outputMsgField = config.outputMsgField;
  21. this.operation = config.operation;
  22. this.constant = config.constant;
  23. this.round = config.round;
  24. this.decimals = config.decimals;
  25. var node = this;
  26. // Test if an object contains the specified property (handles multiple levels like obj.a.b.c).
  27. // (See https://www.customd.com/articles/37/checking-javascript-objects-for-existence-of-a-nested-element )
  28. function objectHasProperty(obj, prop) {
  29. var parts = prop.split('.');
  30. for (var i = 0, l = parts.length; i < l; i++) {
  31. var part = parts[i];
  32. if ((obj !== null) && (typeof(obj) === 'object') && (part in obj)) {
  33. obj = obj[part];
  34. }
  35. else {
  36. return false;
  37. }
  38. }
  39. return true;
  40. }
  41. // https://www.jacklmoore.com/notes/rounding-in-javascript/
  42. function round(value, decimals) {
  43. return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
  44. }
  45. // Check whether the input is correct
  46. function checkInput(checkNumber, inputValue, minCount, maxCount) {
  47. var values = [];
  48. var numbers = [];
  49. var isArray = Array.isArray(inputValue);
  50. if (!isArray) {
  51. if (minCount > 1) {
  52. node.error("The msg." + this.inputMsgField + " should be an array");
  53. return null;
  54. }
  55. // Seems we have enough with an array containing a single item
  56. values.push(inputValue);
  57. }
  58. else {
  59. // Let's check all the numbers in the array
  60. values = inputValue;
  61. }
  62. if (maxCount && minCount === maxCount) {
  63. if (values.length !== minCount) {
  64. if (node.constant) {
  65. node.error("The msg." + this.inputMsgField + " should be an array with " + (--minCount) + " numbers (because constant value specified)");
  66. }
  67. else {
  68. node.error("The msg." + this.inputMsgField + " should be an array with " + minCount + " numbers");
  69. }
  70. return null;
  71. }
  72. }
  73. else {
  74. if (values.length < minCount) {
  75. if (node.constant) {
  76. node.error("The msg." + this.inputMsgField + " should be an array with minimum " + (--minCount) + " numbers (because constant value specified)");
  77. }
  78. else {
  79. node.error("The msg." + this.inputMsgField + " should be an array with minimum " + minCount + " numbers");
  80. }
  81. return null;
  82. }
  83. if (maxCount && values.length > maxCount) {
  84. if (node.constant) {
  85. node.error("The msg." + this.inputMsgField + " should be an array with maximum " + (--maxCount) + " numbers (because constant value specified)");
  86. }
  87. else {
  88. node.error("The msg." + this.inputMsgField + " should be an array with maximum " + maxCount + " numbers");
  89. }
  90. return null;
  91. }
  92. }
  93. for (var i = 0; i < values.length; i++) {
  94. var number = parseFloat(values[i]);
  95. if (checkNumber && isNaN(number)){
  96. node.error("The msg." + this.inputMsgField + " should only contain number(s)");
  97. return null;
  98. }
  99. numbers.push(number);
  100. }
  101. return numbers;
  102. }
  103. node.on("input", function(msg) {
  104. var operation = node.operation;
  105. var numbers = [];
  106. var msgKeyValue;
  107. var count;
  108. var result;
  109. if (!objectHasProperty(msg, node.inputMsgField)) {
  110. node.error("The input message doesn't have have a msg." + node.inputMsgField + " field")
  111. return null;
  112. }
  113. try {
  114. msgKeyValue = RED.util.getMessageProperty(msg, node.inputMsgField);
  115. }
  116. catch(err) {
  117. node.error("The msg." + node.inputMsgField + " field can not be read");
  118. return;
  119. }
  120. // Check whether the input data is an arry.
  121. // Remark: we won't take into account the constant value (below)
  122. var isArray = Array.isArray(msgKeyValue);
  123. // When a constant value is specified, this will be appended to the end of the array
  124. if (node.constant) {
  125. if (!isArray) {
  126. // To be able to append the constantValue (as second value), we need to convert the number to an array with one number
  127. msgKeyValue = [ msgKeyValue ];
  128. }
  129. msgKeyValue.push(parseFloat(node.constant));
  130. }
  131. if (!operation || operation === "") {
  132. operation = msg.operation;
  133. if (!operation) {
  134. node.error("An msg.operation should be supplied");
  135. return null;
  136. }
  137. }
  138. switch(operation) {
  139. case "abs":
  140. numbers = checkInput(true, msgKeyValue, 1);
  141. if (!numbers) return;
  142. numbers.forEach(function(a, index) {
  143. numbers[index] = Math.abs(a);
  144. });
  145. result = (isArray) ? numbers : numbers[0];
  146. break;
  147. case "acos":
  148. numbers = checkInput(true, msgKeyValue, 1);
  149. if (!numbers) return;
  150. numbers.forEach(function(a, index) {
  151. numbers[index] = Math.acos(a);
  152. });
  153. result = (isArray) ? numbers : numbers[0];
  154. break;
  155. case "acosh":
  156. numbers = checkInput(true, msgKeyValue, 1);
  157. if (!numbers) return;
  158. numbers.forEach(function(a, index) {
  159. numbers[index] = Math.acosh(a);
  160. });
  161. result = (isArray) ? numbers : numbers[0];
  162. break;
  163. case "asin":
  164. numbers = checkInput(true, msgKeyValue, 1);
  165. if (!numbers) return;
  166. numbers.forEach(function(a, index) {
  167. numbers[index] = Math.asin(a);
  168. });
  169. result = (isArray) ? numbers : numbers[0];
  170. break;
  171. case "asinh":
  172. numbers = checkInput(true, msgKeyValue, 1);
  173. if (!numbers) return;
  174. numbers.forEach(function(a, index) {
  175. numbers[index] = Math.asinh(a);
  176. });
  177. result = (isArray) ? numbers : numbers[0];
  178. break;
  179. case "atan":
  180. numbers = checkInput(true, msgKeyValue, 1);
  181. if (!numbers) return;
  182. numbers.forEach(function(a, index) {
  183. numbers[index] = Math.atan(a);
  184. });
  185. result = (isArray) ? numbers : numbers[0];
  186. break;
  187. case "atanh":
  188. numbers = checkInput(true, msgKeyValue, 1);
  189. if (!numbers) return;
  190. numbers.forEach(function(a, index) {
  191. numbers[index] = Math.atanh(a);
  192. });
  193. result = (isArray) ? numbers : numbers[0];
  194. break;
  195. case "avg":
  196. numbers = checkInput(true, msgKeyValue, 1);
  197. if (!numbers) return;
  198. result = numbers.reduce(function(a, b) { return a + b; });
  199. result = result / numbers.length;
  200. break;
  201. case "cbrt":
  202. numbers = checkInput(true, msgKeyValue, 1);
  203. if (!numbers) return;
  204. numbers.forEach(function(a, index) {
  205. numbers[index] = Math.cbrt(a);
  206. });
  207. result = (isArray) ? numbers : numbers[0];
  208. break;
  209. case "ceil":
  210. numbers = checkInput(true, msgKeyValue, 1);
  211. if (!numbers) return;
  212. numbers.forEach(function(a, index) {
  213. numbers[index] = Math.ceil(a);
  214. });
  215. result = (isArray) ? numbers : numbers[0];
  216. break;
  217. case "cos":
  218. numbers = checkInput(true, msgKeyValue, 1);
  219. if (!numbers) return;
  220. numbers.forEach(function(a, index) {
  221. numbers[index] = Math.cos(a);
  222. });
  223. result = (isArray) ? numbers : numbers[0];
  224. break;
  225. case "cosh":
  226. numbers = checkInput(true, msgKeyValue, 1);
  227. if (!numbers) return;
  228. numbers.forEach(function(a, index) {
  229. numbers[index] = Math.cosh(a);
  230. });
  231. result = (isArray) ? numbers : numbers[0];
  232. break;
  233. case "dec":
  234. numbers = checkInput(true, msgKeyValue, 1);
  235. if (!numbers) return;
  236. numbers.forEach(function(a, index) {
  237. numbers[index] = a - 1;
  238. });
  239. result = (isArray) ? numbers : numbers[0];
  240. break;
  241. case "div":
  242. numbers = checkInput(true, msgKeyValue, 2);
  243. if (!numbers) return;
  244. if (node.constant === 0) {
  245. node.error("The constant value not be 0 (as denominator)");
  246. return null;
  247. }
  248. for (var i = 1; i < numbers.length; i++) {
  249. if (numbers[i] === 0) {
  250. node.error("The msg." + node.inputMsgField + " should only contain non-zero number(s) for the denominators");
  251. return null;
  252. }
  253. }
  254. result = numbers.reduce(function(a, b) { return a / b; });
  255. break;
  256. case "exp":
  257. numbers = checkInput(true, msgKeyValue, 1);
  258. if (!numbers) return;
  259. numbers.forEach(function(a, index) {
  260. numbers[index] = Math.exp(a);
  261. });
  262. result = (isArray) ? numbers : numbers[0];
  263. break;
  264. case "inc":
  265. numbers = checkInput(true, msgKeyValue, 1);
  266. if (!numbers) return;
  267. numbers.forEach(function(a, index) {
  268. numbers[index] = a + 1;
  269. });
  270. result = (isArray) ? numbers : numbers[0];
  271. break;
  272. case "floor":
  273. numbers = checkInput(true, msgKeyValue, 1);
  274. if (!numbers) return;
  275. numbers.forEach(function(a, index) {
  276. numbers[index] = Math.floor(a);
  277. });
  278. result = (isArray) ? numbers : numbers[0];
  279. break;
  280. case "log":
  281. numbers = checkInput(true, msgKeyValue, 1);
  282. if (!numbers) return;
  283. numbers.forEach(function(a, index) {
  284. numbers[index] = Math.log(a);
  285. });
  286. result = (isArray) ? numbers : numbers[0];
  287. break;
  288. case "log10":
  289. numbers = checkInput(true, msgKeyValue, 1);
  290. if (!numbers) return;
  291. numbers.forEach(function(a, index) {
  292. numbers[index] = Math.log10(a);
  293. });
  294. result = (isArray) ? numbers : numbers[0];
  295. break;
  296. case "max":
  297. numbers = checkInput(true, msgKeyValue, 1);
  298. if (!numbers) return;
  299. result = numbers.reduce(function(a, b) { return (a > b) ? a : b });
  300. break;
  301. case "min":
  302. numbers = checkInput(true, msgKeyValue, 1);
  303. if (!numbers) return;
  304. result = numbers.reduce(function(a, b) { return (a > b) ? b : a });
  305. break;
  306. case "mult":
  307. numbers = checkInput(true, msgKeyValue, 2);
  308. if (!numbers) return;
  309. result = numbers.reduce(function(a, b) { return a * b; });
  310. break;
  311. case "mod":
  312. numbers = checkInput(true, msgKeyValue, 2, 2);
  313. if (!numbers) return;
  314. result = numbers[0] % numbers[1];
  315. break;
  316. case "pow":
  317. numbers = checkInput(true, msgKeyValue, 2, 2);
  318. if (!numbers) return;
  319. result = Math.pow(numbers[0], numbers[1]);
  320. break;
  321. case "rand":
  322. // When the payload contains an array, then we will generate an array (with same length) of random numbers.
  323. // Regardless of the content of the content of the payload array, since we don't need it for our calculations ...
  324. if (isArray) {
  325. numbers = new Array(msgKeyValue.length);
  326. }
  327. else {
  328. numbers = new Array(1);
  329. }
  330. // Remark: 'forEach' does not work on an un-initialized array
  331. for (var j = 0; j < numbers.length; j++) {
  332. numbers[j] = Math.random();
  333. }
  334. result = (isArray) ? numbers : numbers[0];
  335. break;
  336. case "randb":
  337. numbers = checkInput(true, msgKeyValue, 2, 2);
  338. if (!numbers) return;
  339. result = Math.floor(Math.random() * (numbers[1] - numbers[0] + 1)) + numbers[0];
  340. break;
  341. case "randa":
  342. numbers = checkInput(true, msgKeyValue, 1);
  343. if (!numbers) return;
  344. result = numbers[Math.floor(Math.random() * numbers.length)];
  345. break;
  346. case "len":
  347. numbers = checkInput(false, msgKeyValue, 1);
  348. if (!numbers) return;
  349. result = numbers.length;
  350. break;
  351. // Implementation of sorting, since the sort node does not behave correctly at this moment.
  352. // (see https://github.com/akashtalole/node-red-contrib-sort/issues/1)
  353. case "sorta":
  354. numbers = checkInput(true, msgKeyValue, 1);
  355. if (!numbers) return;
  356. numbers.sort(function(a, b) {
  357. return a - b;
  358. });
  359. result = numbers;
  360. break;
  361. case "sortd":
  362. numbers = checkInput(true, msgKeyValue, 1);
  363. if (!numbers) return;
  364. numbers.sort(function(a, b) {
  365. return b - a;
  366. });
  367. result = numbers;
  368. break;
  369. case "range":
  370. numbers = checkInput(true, msgKeyValue, 2, 2);
  371. if (!numbers) return;
  372. //numbers[0] = Math.trunc(numbers[0]);
  373. //numbers[1] = Math.trunc(numbers[1]);
  374. result = [];
  375. for (var k = numbers[0]; k <= numbers[1]; k++) {
  376. result.push(k);
  377. }
  378. break;
  379. case "dist":
  380. numbers = checkInput(true, msgKeyValue, 2);
  381. if (!numbers) return;
  382. numbers.sort();
  383. result = numbers[numbers.length - 1] - numbers[0];
  384. break;
  385. case "rdec":
  386. numbers = checkInput(true, msgKeyValue, 2, 2);
  387. if (!numbers) return;
  388. // See http://www.jacklmoore.com/notes/rounding-in-javascript/
  389. result = Number(Math.round(numbers[0] + 'e' + numbers[1]) + 'e-' + numbers[1]);
  390. break;
  391. case "round":
  392. numbers = checkInput(true, msgKeyValue, 1);
  393. if (!numbers) return;
  394. numbers.forEach(function(a, index) {
  395. numbers[index] = Math.round(a);
  396. });
  397. result = (isArray) ? numbers : numbers[0];
  398. break;
  399. case "sin":
  400. numbers = checkInput(true, msgKeyValue, 1);
  401. if (!numbers) return;
  402. numbers.forEach(function(a, index) {
  403. numbers[index] = Math.sin(a);
  404. });
  405. result = (isArray) ? numbers : numbers[0];
  406. break;
  407. case "sinh":
  408. numbers = checkInput(true, msgKeyValue, 1);
  409. if (!numbers) return;
  410. numbers.forEach(function(a, index) {
  411. numbers[index] = Math.sinh(a);
  412. });
  413. result = (isArray) ? numbers : numbers[0];
  414. break;
  415. case "sqrt":
  416. numbers = checkInput(true, msgKeyValue, 1);
  417. if (!numbers) return;
  418. numbers.forEach(function(a, index) {
  419. numbers[index] = Math.sqrt(a);
  420. });
  421. result = (isArray) ? numbers : numbers[0];
  422. break;
  423. case "sum":
  424. numbers = checkInput(true, msgKeyValue, 2);
  425. if (!numbers) return;
  426. result = numbers.reduce(function(a, b) { return a + b; });
  427. break;
  428. case "sub":
  429. numbers = checkInput(true, msgKeyValue, 2);
  430. if (!numbers) return;
  431. result = numbers.reduce(function(a, b) { return a - b; });
  432. break;
  433. case "tan":
  434. numbers = checkInput(true, msgKeyValue, 1);
  435. if (!numbers) return;
  436. numbers.forEach(function(a, index) {
  437. numbers[index] = Math.tan(a);
  438. });
  439. result = (isArray) ? numbers : numbers[0];
  440. break;
  441. case "tanh":
  442. numbers = checkInput(true, msgKeyValue, 1);
  443. if (!numbers) return;
  444. numbers.forEach(function(a, index) {
  445. numbers[index] = Math.tanh(a);
  446. });
  447. result = (isArray) ? numbers : numbers[0];
  448. break;
  449. case "trunc":
  450. numbers = checkInput(true, msgKeyValue, 1);
  451. if (!numbers) return;
  452. numbers.forEach(function(a, index) {
  453. numbers[index] = Math.trunc(a);
  454. });
  455. result = (isArray) ? numbers : numbers[0];
  456. break;
  457. default:
  458. node.error("The msg.operation contains an unsupported operation '" + operation + "'");
  459. return null;
  460. }
  461. // If required, round the result to the specified number of decimals
  462. if (node.round) {
  463. if (Array.isArray(result)) {
  464. for (var j = 0; j < result.length; j++) {
  465. result[j] = round(result[j], node.decimals);
  466. }
  467. }
  468. else {
  469. result = round(result, node.decimals);
  470. }
  471. }
  472. RED.util.setMessageProperty(msg, node.outputMsgField, result, true);
  473. node.send(msg);
  474. });
  475. }
  476. RED.nodes.registerType("calculator", CalculatorNode);
  477. }