How to use forwardFlags method in unexpected

Best JavaScript code snippet using unexpected

timeTable.umd.js

Source:timeTable.umd.js Github

copy

Full Screen

1(function (global, factory) {2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :3 typeof define === 'function' && define.amd ? define(factory) :4 (global = global || self, global.TimeTable = factory());5}(this, (function () { 'use strict';6 /**7 * Copyright 2021 Christian Meinert8 *9 * Licensed under the Apache License, Version 2.0 (the "License");10 * you may not use this file except in compliance with the License.11 * You may obtain a copy of the License at12 *13 * http://www.apache.org/licenses/LICENSE-2.014 *15 * Unless required by applicable law or agreed to in writing, software16 * distributed under the License is distributed on an "AS IS" BASIS,17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.18 * See the License for the specific language governing permissions and19 * limitations under the License.20 **/21 /**22 * Notes:23 * 24 * all known issues or tasks are marked with "ToDo:"25 * 26 */27 class TimeTable {28 constructor(content = [[]], rowMap = {}) {29 this.node = console;30 this.setTable(content,rowMap);31 this.forwardFlags = [{forwarded:0, col:undefined}];32 this.schedules = [];33 this.rulesToApply = [];34 }35 /**36 * set a logger class. It must contain `log`, `warn` and `error` functions37 * @param {object} logger logger class (defaults to `console`)38 **/39 setLogger(logger) {40 if (logger === undefined) {41 this.node=undefined;42 } else {43 console.log(`setLogger ${(typeof logger.log === 'function') ? "ok" : "failed"}`);44 if (typeof logger.log === 'function') {45 this.node = logger;46 }47 }48 }49 /**50 * deep merge object / array51 * @param {object} target target object52 * @param {object} source source object53 **/54 mergeObject(target,source) {55 if (typeof source === 'object') {56 Object.keys(source).forEach(element => {57 if (typeof source[element] !== "object" || source[element]===null) {58 target[element] = source[element];59 } else {60 if (!target.hasOwnProperty(element)) {61 target[element] = (Array.isArray(source[element])) ? [] : {};62 }63 this.mergeObject(target[element],source[element]);64 }65 });66 } else {67 target=source;68 }69 }70 /**71 * get reference to table data72 * @return {array} [rows][columns]73 **/74 getTable() { return this.content };75 /**76 * get last timestamp of the table77 * @param {number} [offset=0] number of rows from end (default = 0)78 * @return {number} timestamp79 **/80 getLastTimestamp(offset = 0) { return this.content[0][this.content[0].length-1-offset] };81 /**82 * get timestamp of the table83 * @param {number} [offset=0] number of rows from beginning (default = 0)84 * @return {number} timestamp85 **/86 getTimestamp(offset = 0) { return this.content[0][offset] };87 /**88 * set table data (reference)89 * row map will be deleted if not provided90 * @param {array} data [rows][columns] (reference)91 * @param {object} rowMap (reference) map to all row topic:indices92 **/93 setTable(data, rowMap) { 94 if (Array.isArray(data)) {95 this.content = data;96 if (rowMap===undefined) ; else {97 this.rowMap = rowMap;98 }99 Object.keys(this.rowMap).forEach(topic => {100 let i = this.rowMap[topic];101 if (!Array.isArray(this.content[i])) this.content[i] = [];102 });103 this.columns = this.content[0].length;104 this.rows = this.content.length;105 } else {this.node?.warn('timeTable.setTable() data must be an array!', data);}106 };107 /**108 * get row map109 * @return {object} rowMap {id:row}110 **/111 getRowMap() { return this.rowMap };112 /**113 * set row map114 * @param {object} rowMap {id:row}115 **/116 setRowMap(rowMap) {117 if (typeof rowMap === 'object') {118 // better keep the reference119 Object.keys(this.rowMap).forEach(key => delete this.rowMap[key]);120 this.mergeObject(this.rowMap,rowMap);121 } else {this.node?.warn('timeTable.rowMap() rowMap must be an object!', rowMap);}122 };123 /**124 * set limit rules to apply to incoming data. Previous limits will be deleted125 * @param {array} rules set of rules [{plugin:"limitColumns/limitTime",columns:{number},last:{number},period:{number},enabled:{boolean}}}]126 **/127 setLimits(rules) {128 this.rulesToApply = [];129 rules.forEach(rule => {130 if (rule.enabled && rule.limitColumnsPeriods===0) {131 if (rule.plugin===`limitColumns` || rule.plugin==='limitTime') {132 this.rulesToApply.push(rule);133 }134 }135 });136 }137 /**138 * remap rows: sort and filter existing rows according to an array of ids139 * @param {array} ids new array of keys to remap existing data140 * @return {object} updates rowMap {id:row}141 **/142 remapRows(ids) {143 if (Array.isArray(ids) && ids.length>0) {144 let mappingFlag = false;145 var tempTable = [];146 tempTable[0]=this.content[0] || [];147 ids.forEach((id,index) => {148 if (!mappingFlag && index+1!==this.rowMap[id]) { mappingFlag = true;} if (this.hasRow(id)) { // use existing row149 tempTable[index+1] = this.content[this.rowMap[id]] || [];150 } else { // create new row151 tempTable[index+1] = Array(this.content[0].length);152 this.rowMap[id] = index+1;153 }154 });155 ids.forEach((id,index) => { 156 this.rowMap[id]=index+1; // update the map157 });158 this.content=tempTable;159 if (mappingFlag) {this.node?.log(`remappedRows to [${Object.keys(this.rowMap).toString()}]`);}160 }161 return this.content;162 }163 /**164 * get width (columns)165 * @return {number} amount of columns166 **/167 getWidth() { return this.content[0].length };168 /**169 * get height (rows) without time row170 * @return {number} amount of rows171 **/172 getHeight() { return this.content.length-1 };173 /**174 * set height (rows) and adding not populated rows175 * @param {number} rows of rows without time row176 **/177 setHeight(rows) {178 var table = this;179 // this.node?.log('setHeight',table.content);180 let row = table.content.length;181 while (row <= rows) {182 table.content[row]=Array(table.content[0].length);183 row++;184 } 185 table.rows = table.content.length-1;186 };187 /**188 * checks if a row id exists189 * @param {string} id190 * @return {number} amount of rows191 **/192 hasRow(id) { return this.rowMap.hasOwnProperty(id); };193 /**194 * get row id by index, returns undefined if index don't match195 * @param {number} index196 * @return {string} row id197 **/198 getRowId(index) { 199 return Object.keys(this.rowMap).find(key => this.rowMap[key] === index);200 }201 /**202 * get row index by id, returns -1 if row does mot exist exists203 * @param {string} id204 * @return {number} row index or `undefined` if row dows not exists205 **/206 getRowIndex(id) { 207 return this.rowMap[id];208 /*209 if (this.rowMap.hasOwnProperty(id)) {210 return this.rowMap[id];211 }212 return undefined;213 */214 }215 /**216 * get table data as a CSV string217 * JSON is not able of sparse arrays so a own 218 * stringify method is necessary219 * @return {string} stringified array220 **/221 getCSV() {222 var table = this;223 var result = '';224 table.content.forEach(row => {225 for (let i = 0; i < row.length; i++) { 226 if (row[i]!==undefined) {227 result += (row[i]!==null) ? row[i] : 'null';228 }229 result += ',';230 }231 result = result.slice(0,-1); // remove the last comma232 result += "\n";233 });234 result = result.slice(0,-2); // remove the newLine235 return result;236 };237 /**238 * set table data from a string239 * JSON is not able of sparse arrays so a own parse method is necessary240 * cells are parsed as float, 'null' strings results in a null value241 * @param {string} dataString CSV String ("\n" as row and "," as column separator) i.e. as produced by table.getCSV();242 * @return {array} [rows][columns]243 **/244 parseCSV(dataString) {245 this.node?.log(`parseCSV ${dataString.length} bytes`);246 this.content.length = 0;247 this.content[0] = [];248 let rowStrings = dataString.split('\n');249 if (rowStrings.length>1) {250 rowStrings.forEach((rowString,row) => {251 this.content[row] = [];252 let columnStrings = rowString.split(',');253 columnStrings.forEach((cell,column) => {254 if (cell!=='') {255 if (cell!=='null') {256 this.content[row][column] = parseFloat(cell);257 } else {258 this.content[row][column] = null;259 }260 }261 });262 });263 }264 this.columns=this.content[0].length;265 this.rows=this.content.length;266 }267 /**268 * add a row 269 * @param {string|number} id of that row or index number270 * @param {array} [defaultRow=[]] (optional) default content271 * @return {number} index of existing or created row (-1 if error)272 **/273 addRow(id, defaultRow = []) {274 var table = this;275 switch (typeof id) {276 case 'string':277 if (!table.rowMap.hasOwnProperty(id)) { // add a new row278 table.content[table.content.length] = defaultRow;279 table.content[table.content.length-1].length = table.content[0].length; // fill sparse array280 table.rowMap[id] = table.rows = table.content.length-1;281 }282 return table.rowMap[id];283 case 'number':284 if (id < table.content.length) {285 return id;286 } else {287 this.node?.log(`timeTable.addRow(id) id index exceeds number of initialized rows is: ${id} max ${table.content.length}`);288 return -1;289 }290 default :291 this.node?.log(`timeTable.addRow(id) id has to be of type number or string! is: "${typeof id}"`);292 return -1;293 }294 };295 296 /**297 * forwardValue(row) will forward the last known value to the latest column to enable constant states like set values in the chart298 * The data points between the last "real" value and the forwarded value will be deleted to eliminate repeating values299 * @param {number|string} row row index or topic300 **/301 forwardValue(row) {302 var table = this;303 if (typeof row === 'string') {304 row = table.rowMap[row];305 }306 if (row > 0 && row < table.content.length) {307 let col = table.content[0].length-1;308 let endCol = col;309 if (table.content[row][col]!==undefined) {310 table.forwardFlags[row]===undefined;311 this.node?.log('value present no need to forward values');312 return;313 }314 while (col>0 && table.content[row][col]===undefined) {315 col--;316 }317 if (col>0) {318 table.content[row][endCol] = table.content[row][col];319 this.node?.log(`forwarded from:${col} to:${endCol}`);320 if (table.forwardFlags[row]!== undefined) {321 this.node?.log(`deleted ${table.forwardFlags[row]}`);322 delete table.content[row][table.forwardFlags[row]];323 }324 table.forwardFlags[row]=endCol;325 }326 this.node?.log(`forwardValue(${row})`); //, table.content[row]);327 } else {328 this.node?.log(`timeTable.forwardValue(row) row=${row} is unknown`);329 }330 }331 /**332 * add a column 333 * @param {number} time timestamp to be added334 * @return {number} column index335 **/336 addColumnTimestamp(time) {337 var table = this;338 // ToDo: It is more likely that the timestamp is at the end of the array:339 // let col = table.content[0].length-1;340 // while (col>0 && time<table.content[0][col]) {col--}; 341 let col = table.content[0].findIndex(element => element >= time);342 table.rows = table.content.length-1;343 table.columns = table.content[0].length;344 if (col < 0 || col === undefined) { // add new column @ the end345 table.columns++;346 table.content.forEach(row => {347 if (row===undefined) row=[];348 row.length = table.columns;349 });350 col=table.columns;351 } else if (time !== table.content[0][col]) { // insert a column352 table.content.forEach(row => {353 table.columns++;354 row.splice(col, 0, undefined);355 delete row[col];356 }); 357 } else { // existing column358 col++;359 }360 table.content[0][col-1] = time;361 table.rulesToApply.forEach(rule => {362 table[rule.plugin](rule);363 });364 return col-1;365 };366 /** 367 * add one or many values to one or many time columns368 * @param {number} time timestamp to be added369 * @param {array} values [{id,index,timestamp,value}] where index is used if present, timestamp is optional370 * @return {number} number of successfully added values371 **/372 addValues(time,values) {373 var table = this;374 var addedValues = 0;375 values.forEach(value => {376 let column = table.addColumnTimestamp((value.timestamp===undefined) ? time : value.timestamp);377 let row = table.addRow((value.index===undefined) ? value.id : value.index); // get or add row378 if (row>0) {379 table.content[row][column]=value.value;380 addedValues++;381 }382 });383 return addedValues;384 }385 /** 386 * clean the table array: 387 * 388 * - columns with no data in all rows will be removed from array389 * @param {object} [rule={}] rule config390 * @param {number} [start=0] start column index391 * @param {number} [end=length] end column index392 * @returns {array} reference to clean table393 **/394 cleanTable(rule = {},start = 0,end = 0){395 var table = this;396 var empty = true;397 var usedColumns = [];398 if (end === 0) end = table.content[0].length;399 for (let col=start; col<end; col++) { // do not use reduce as it works over the complete array400 empty = true;401 for (let row=1; row<table.content.length; row ++) {402 if (table.content[row][col]!==undefined) {empty = false; break;}403 }404 if (!empty) {405 usedColumns.push(col);406 }407 }408 this.node?.log(`cleanTable ${usedColumns.length}/${table.content[0].length} delete ${table.content[0].length-usedColumns.length} columns`);409 var newTable=[];410 for (let row=0; row<table.content.length; row ++) {411 newTable[row]=[];412 }413 usedColumns.forEach((index,col) => {414 for (let row=0; row<table.content.length; row ++) {415 if (table.content[row][index]!==undefined) {newTable[row][col]=table.content[row][index];} }416 });417 // this.node?.log(`done: ${newTable[0].length} columns left`);418 table.content=newTable;419 return newTable;420 }421 /** 422 * calculate the **minimum** value of a specific period423 * 424 * **For efficiency delete cells "on the go" where data is reduced by result of this function**425 * @param {object} rule rule config426 * @param {array} rowContent complete (sparse) row content427 * @param {number} start start column index428 * @param {number} end end column index429 **/430 minCalc(rule,rowContent,start,end) {431 var result = 0;432 var columnsToDelete = [];433 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array434 if (rowContent[i]) {435 if (rowContent[i]>result) result = rowContent[i];436 columnsToDelete.push(i); // record column indices to delete later437 }438 }439 if (columnsToDelete.length>1) {440 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected441 return result;442 } else {443 return undefined;444 }445 }446 /** 447 * calculate the **maximum** value of a specific period448 * 449 * **For efficiency delete cells "on the go" where data is reduced by result of this function**450 * @param {object} rule rule config451 * @param {array} rowContent complete (sparse) row content452 * @param {number} start start column index453 * @param {number} end end column index454 **/455 maxCalc(rule,rowContent,start,end) {456 var result = MAX_VALUE;457 var columnsToDelete = [];458 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array459 if (rowContent[i]) {460 if (rowContent[i]<result) result = rowContent[i];461 columnsToDelete.push(i); // record column indices to delete later462 }463 }464 if (columnsToDelete.length>1) {465 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected466 return result;467 } else {468 return undefined;469 }470 }471 /** 472 * calculate the **average** value of a specific period473 * 474 * **For efficiency delete cells "on the go" where data is reduced by result of this function**475 * @param {object} rule rule config476 * @param {array} rowContent complete (sparse) row content477 * @param {number} start start column index478 * @param {number} end end column index479 **/480 averageCalc(rule,rowContent,start,end) {481 var amount = 0;482 var total = 0;483 var columnsToDelete = [];484 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array485 if (rowContent[i]) {486 total += rowContent[i];487 amount++;488 columnsToDelete.push(i); // record column indices to delete later489 }490 }491 if (columnsToDelete.length>1) {492 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected493 this.node?.log(`averageCalc [${start}:${end}] = ${total/amount} columns deleted:${columnsToDelete.length}`);494 return total/amount;495 } else {496 return undefined;497 }498 }499 /** 500 * calculate the **sum** value of a specific period501 * 502 * **For efficiency delete cells "on the go" where data is reduced by result of this function**503 * @param {object} rule rule config504 * @param {array} rowContent complete (sparse) row content505 * @param {number} start start column index506 * @param {number} end end column index507 **/508 sumCalc(rule,rowContent,start,end) {509 var sum = 0;510 var columnsToDelete = [];511 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array512 if (rowContent[i]) {513 sum += rowContent[i];514 columnsToDelete.push(i); // record column indices to delete later515 }516 }517 if (columnsToDelete.length>1) {518 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected519 return sum;520 } else {521 return undefined;522 }523 }524 /** 525 * reduce data by rule526 * @param {object} rule rule object containing **method,older,threshold,span,periods** property527 * @param {number} startThreshold (optional = 0) period start timestamp (in seconds) 528 * @return {number} next valid threshold529 **/530 reduceData(rule,startThreshold = 0) { 531 var table = this;532 if (startThreshold <= 0 ) startThreshold = table.content[0][0];533 const endThreshold = Date.now()/1000 - (rule.older * rule.threshold); 534 if (table.content[0][0]===undefined || table.content[0][0]>endThreshold) {535 if (table.content[0][0]!==undefined) {536 this.node?.log(`reduceData: No data before ${new Date(endThreshold*1000).toLocaleDateString()}`);537 }538 return -1; // no data available older than time Limit;539 }540 541 const periodSeconds = rule.span * rule.periods;542 const startColumn = table.content[0].findIndex(timestamp => timestamp >= startThreshold);543 const endColumn = table.content[0].findIndex(timestamp => timestamp > endThreshold);544 var cursor = {545 nextThreshold:startThreshold+periodSeconds,546 start:{timestamp:startThreshold,column:startColumn},547 end:{timestamp:startThreshold,column:startColumn},548 ruleEnd:{timestamp:endThreshold,column:endColumn-1},549 midPoint:0550 };551 var result = 0;552 var resultAt = 0;553 // this.node?.log(`reduceData between ${startColumn}/${new Date(startThreshold*1000).toLocaleDateString()} and ${endColumn}/${new Date(endThreshold*1000).toLocaleDateString()} rule="${rule.method}"`); // ,cursor,rule554 for (let row = 1; row < table.content.length; row ++) {555 if (rule.applyTo==='' || (Array.isArray(rule.applyTo) && rule.applyTo.includes(table.getRowId(row)))) {556 cursor.start.column=startColumn;557 cursor.start.timestamp=startThreshold;558 cursor.end.column=startColumn;559 cursor.end.timestamp=startThreshold;560 cursor.nextThreshold=startThreshold+periodSeconds;561 while (cursor.end.column < endColumn) {562 while(cursor.end.timestamp < cursor.nextThreshold) {563 cursor.end.column++;564 cursor.end.timestamp = table.content[0][cursor.end.column];565 }566 if (cursor.end.column-cursor.start.column > 1) {567 result = table[rule.method+'Calc'](rule,table.content[row],cursor.start.column,cursor.end.column);568 if (result!==undefined) {569 switch (rule.resultAt) {570 case 0: resultAt = cursor.start.column; break;571 case 1: resultAt = cursor.end.column; break;572 case 2: resultAt = cursor.start.column + Math.floor((cursor.end.column-cursor.start.column)/2); break;573 case 3: resultAt = -1; break;574 }575 // this.node?.log(`calc range:${cursor.start.column}-${cursor.end.column} @${resultAt}=${result}`);576 if (resultAt>=0) {577 table.content[row][resultAt] = result;578 } else {579 this.addValues(cursor.start.timestamp+((cursor.end.timestamp-cursor.start.timestamp)/2),{index:row,value:result});580 }581 }582 }583 cursor.end.column++;584 cursor.start.column = cursor.end.column;585 cursor.start.timestamp = table.content[0][cursor.start.column];586 cursor.end.timestamp = cursor.start.timestamp;587 cursor.nextThreshold += periodSeconds;588 }589 }590 }591 return endThreshold;592 }593 /** 594 * limit the amount of columns595 * @param {array} rules array of rule objects596 * @return {number} number of removed columns597 **/598 applyReductionRules(rules){599 var table = this;600 table.removedData = 0;601 let startTime = 0;602 let reductionRules = rules.filter(rule => rule.threshold > 0 && rule.older > 0 && rule.enabled);603 reductionRules.sort((a,b)=>{return (b.older*b.threshold) - (a.older*a.threshold);});604 //this.node?.log(`applyScheduledRules sorted`,reductionRules);605 reductionRules.forEach((rule, index) => {606 //this.node?.log(`applied #${index}:"${rule.plugin} start ${startTime}`);607 startTime = table.reduceData(rule,startTime);608 //this.node?.log(`applied #${index}:"${rule.plugin}" next rule start ${startTime}s`)609 });610 return table.removedData;611 }612 /** 613 * limit the amount of columns614 * @param {object} rule rule object containing **columns** property615 * @return {number} number of removed columns616 **/617 limitColumns(rule) { 618 var table = this;619 let toRemove = 0;620 if (table.content[0].length > rule.columns) {621 toRemove = table.content[0].length - rule.columns;622 table.content.forEach(row => row.splice(0,toRemove));623 }624 return toRemove;625 }626 /** 627 * limit the time span628 * @param {object} rule rule object containing **last and period** property629 * @return {number} number of removed columns630 **/631 limitTime(rule) { 632 var table = this;633 if (table.content[0].length < 1) return 0;634 let limit = Date.now()/1000 - (rule.last * rule.period);635 let toRemove = 0;636 while (toRemove < table.content[0].length && table.content[0][toRemove] < limit) { toRemove ++;} if (toRemove > 0) {637 table.content.forEach(row => row.splice(0,toRemove));638 this.node?.log(`limitTime columns deleted:${toRemove} left: ${table.getWidth()}`);639 }640 return toRemove;641 }642 /**643 * apply a set one rule to the data644 * @param {object} rule object defining the rule645 * @return {number} number of affected columns. -1 if an error accrued646 */647 applyRule(rule) {648 var table = this;649 var result = 0;650 if (typeof table[rule.plugin] === 'function') {651 result += table[rule.plugin](rule);652 } else {653 this.node?.log(`rule plugin "${rule.plugin}" unknown!`);654 }655 return result;656 }657 /**658 * set timers for scheduled rules659 * all existing timers will be canceled660 * @param {array} rules array of objects defining the rules661 * @param {boolean} [client = false] flag if client values should be used662 * @return {number} amount of timers set663 */664 setTimers(rules, client = false) {665 var table = this;666 // clear running timers667 table.schedules.forEach((schedule,index) => {668 clearInterval(table.schedules[index]);669 });670 table.schedules = [];671 // set reduction rules timers672 let reductionRules = rules.filter(rule => rule.threshold > 0 && rule.older > 0 && rule.enabled);673 reductionRules.sort((a,b)=>{return (b.older*b.threshold) - (a.older*a.threshold);});674 var timeBase = Date.now()/1000;675 var startTime = 0;676 reductionRules.forEach(rule => {677 if (!rule.enabled) return;678 rule.startTime = startTime;679 rule.endTime = timeBase - (rule.older * rule.threshold); // advance start time to end of period ("older than")680 681 table.schedules.push(setInterval(() => {682 var scheduledRule = rule;683 table.reduceData(scheduledRule, scheduledRule.startTime);684 },rule.span * rule.periods * 1000));685 startTime = rule.endTime;686 this.node?.log(`scheduler "${rule.plugin}" from ${new Date(rule.startTime*1000).toLocaleDateString()} ${new Date(rule.startTime*1000).toLocaleTimeString()} to ${new Date(rule.endTime*1000).toLocaleDateString()} ${new Date(rule.endTime*1000).toLocaleTimeString()} set for every ${rule.span * rule.periods}s`);687 });688 // set other rules689 rules.forEach((rule,index) => {690 let intervalMs = 0;691 switch (rule.plugin) {692 case 'reduceData': // already handled above693 return;694 case 'limitColumns':695 if (!client && rule.limitColumnsPeriods>0) { // only if by time696 intervalMs = rule.limitColumnsEvery * rule.limitColumnsPeriods * 1000;697 } if (client && rule.checkEvery>0) {698 intervalMs = rule.checkEvery * 1000;699 }700 break;701 case 'limitTime':702 if (!client && rule.limitColumnsPeriods>0) { // only if by time 703 intervalMs = rule.limitTimeEvery * rule.limitTimePeriods * 1000;704 } if (client && rule.checkEvery>0) {705 intervalMs = rule.checkEvery * 1000;706 } 707 break;708 case 'cleanTable':709 intervalMs = rule.cleanEvery * rule.cleanPeriod * 1000;710 break;711 }712 if (intervalMs>0 && rule.enabled) {713 table.schedules.push(setInterval(() => {714 var scheduledRule = rule;715 table.applyRule(scheduledRule);716 },intervalMs));717 this.node?.log(`scheduler "${rule.plugin}" every ${intervalMs/1000}s`);718 }719 });720 return table.schedules.length;721 }722 /**723 * apply a set of rules to the data724 * only enabled rules are applied725 * the array will be sorted: 1st limit amount, 2nd limit total time and then scheduled actions726 * @param {array} rules array of objects defining the rules727 * @return {number} number of affected columns. -1 if an error accrued728 */729 applyRules(rules) {730 var table = this;731 var result = 0;732 if (rules === undefined || !Array.isArray(rules)) {733 this.node?.warn(`applyRules: rules are undefined! exiting.`);734 return -1;735 }736 table.singleRules = rules.filter(rule => rule.plugin === 'limitColumns' && rule.enabled);737 table.singleRules.concat(rules.filter(rule => rule.plugin === 'limitTime' && rule.enabled));738 table.singleRules.forEach((rule,index) => {739 result += table.applyRule(rule);740 });741 result+= table.applyReductionRules(rules);742 return result;743 }744 getSizes() {745 var table = this;746 var result = {747 memoryBytes : 0,748 memoryString : '',749 columns : table.content[0].length,750 rows : table.content.length-1, 751 cellsUsed : 0,752 cellsTotal : table.content[0].length * (table.content.length-1),753 cellsUnused : 0,754 ratio: 1,755 ratioPercent: "100%",756 timeTotal : 0,757 };758 table.content.forEach((row,rowIndex) =>{759 if (rowIndex>0) {760 row.forEach(() => result.cellsUsed++);761 }762 });763 result.memoryBytes = (result.cellsUsed + result.columns) * 8;764 result.memoryString = (result.memoryBytes>1024) ? (result.memoryBytes / 1024).toFixed(2) + 'kb' :765 (result.memoryBytes> 1024*1024) ? (result.memoryBytes / 1024 / 1024).toFixed(2) + 'mb' :766 result.memoryBytes + 'bytes';767 result.cellsUnused = result.cellsTotal - result.cellsUsed;768 if (result.cellsTotal>0) {769 result.ratio = result.cellsUsed / result.cellsTotal;770 result.ratioPercent = (result.ratio*100).toFixed(2)+"%";771 }772 result.timeTotal = table.content[0][table.content[0].length-1] - table.content[0][0];773 return result;774 }775 }776 // module.exports = TimeTable;777 return TimeTable;...

Full Screen

Full Screen

timeTable.cjs.js

Source:timeTable.cjs.js Github

copy

Full Screen

1'use strict';2/**3 * Copyright 2021 Christian Meinert4 *5 * Licensed under the Apache License, Version 2.0 (the "License");6 * you may not use this file except in compliance with the License.7 * You may obtain a copy of the License at8 *9 * http://www.apache.org/licenses/LICENSE-2.010 *11 * Unless required by applicable law or agreed to in writing, software12 * distributed under the License is distributed on an "AS IS" BASIS,13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14 * See the License for the specific language governing permissions and15 * limitations under the License.16 **/17/**18 * Notes:19 * 20 * all known issues or tasks are marked with "ToDo:"21 * 22 */23class TimeTable {24 constructor(content = [[]], rowMap = {}) {25 this.node = console;26 this.setTable(content,rowMap);27 this.forwardFlags = [{forwarded:0, col:undefined}];28 this.schedules = [];29 this.rulesToApply = [];30 }31 /**32 * set a logger class. It must contain `log`, `warn` and `error` functions33 * @param {object} logger logger class (defaults to `console`)34 **/35 setLogger(logger) {36 if (logger === undefined) {37 this.node=undefined;38 } else {39 console.log(`setLogger ${(typeof logger.log === 'function') ? "ok" : "failed"}`);40 if (typeof logger.log === 'function') {41 this.node = logger;42 }43 }44 }45 /**46 * deep merge object / array47 * @param {object} target target object48 * @param {object} source source object49 **/50 mergeObject(target,source) {51 if (typeof source === 'object') {52 Object.keys(source).forEach(element => {53 if (typeof source[element] !== "object" || source[element]===null) {54 target[element] = source[element];55 } else {56 if (!target.hasOwnProperty(element)) {57 target[element] = (Array.isArray(source[element])) ? [] : {};58 }59 this.mergeObject(target[element],source[element]);60 }61 });62 } else {63 target=source;64 }65 }66 /**67 * get reference to table data68 * @return {array} [rows][columns]69 **/70 getTable() { return this.content };71 /**72 * get last timestamp of the table73 * @param {number} [offset=0] number of rows from end (default = 0)74 * @return {number} timestamp75 **/76 getLastTimestamp(offset = 0) { return this.content[0][this.content[0].length-1-offset] };77 /**78 * get timestamp of the table79 * @param {number} [offset=0] number of rows from beginning (default = 0)80 * @return {number} timestamp81 **/82 getTimestamp(offset = 0) { return this.content[0][offset] };83 /**84 * set table data (reference)85 * row map will be deleted if not provided86 * @param {array} data [rows][columns] (reference)87 * @param {object} rowMap (reference) map to all row topic:indices88 **/89 setTable(data, rowMap) { 90 if (Array.isArray(data)) {91 this.content = data;92 if (rowMap===undefined) ; else {93 this.rowMap = rowMap;94 }95 Object.keys(this.rowMap).forEach(topic => {96 let i = this.rowMap[topic];97 if (!Array.isArray(this.content[i])) this.content[i] = [];98 });99 this.columns = this.content[0].length;100 this.rows = this.content.length;101 } else {this.node?.warn('timeTable.setTable() data must be an array!', data);}102 };103 /**104 * get row map105 * @return {object} rowMap {id:row}106 **/107 getRowMap() { return this.rowMap };108 /**109 * set row map110 * @param {object} rowMap {id:row}111 **/112 setRowMap(rowMap) {113 if (typeof rowMap === 'object') {114 // better keep the reference115 Object.keys(this.rowMap).forEach(key => delete this.rowMap[key]);116 this.mergeObject(this.rowMap,rowMap);117 } else {this.node?.warn('timeTable.rowMap() rowMap must be an object!', rowMap);}118 };119 /**120 * set limit rules to apply to incoming data. Previous limits will be deleted121 * @param {array} rules set of rules [{plugin:"limitColumns/limitTime",columns:{number},last:{number},period:{number},enabled:{boolean}}}]122 **/123 setLimits(rules) {124 this.rulesToApply = [];125 rules.forEach(rule => {126 if (rule.enabled && rule.limitColumnsPeriods===0) {127 if (rule.plugin===`limitColumns` || rule.plugin==='limitTime') {128 this.rulesToApply.push(rule);129 }130 }131 });132 }133 /**134 * remap rows: sort and filter existing rows according to an array of ids135 * @param {array} ids new array of keys to remap existing data136 * @return {object} updates rowMap {id:row}137 **/138 remapRows(ids) {139 if (Array.isArray(ids) && ids.length>0) {140 let mappingFlag = false;141 var tempTable = [];142 tempTable[0]=this.content[0] || [];143 ids.forEach((id,index) => {144 if (!mappingFlag && index+1!==this.rowMap[id]) { mappingFlag = true;} if (this.hasRow(id)) { // use existing row145 tempTable[index+1] = this.content[this.rowMap[id]] || [];146 } else { // create new row147 tempTable[index+1] = Array(this.content[0].length);148 this.rowMap[id] = index+1;149 }150 });151 ids.forEach((id,index) => { 152 this.rowMap[id]=index+1; // update the map153 });154 this.content=tempTable;155 if (mappingFlag) {this.node?.log(`remappedRows to [${Object.keys(this.rowMap).toString()}]`);}156 }157 return this.content;158 }159 /**160 * get width (columns)161 * @return {number} amount of columns162 **/163 getWidth() { return this.content[0].length };164 /**165 * get height (rows) without time row166 * @return {number} amount of rows167 **/168 getHeight() { return this.content.length-1 };169 /**170 * set height (rows) and adding not populated rows171 * @param {number} rows of rows without time row172 **/173 setHeight(rows) {174 var table = this;175 // this.node?.log('setHeight',table.content);176 let row = table.content.length;177 while (row <= rows) {178 table.content[row]=Array(table.content[0].length);179 row++;180 } 181 table.rows = table.content.length-1;182 };183 /**184 * checks if a row id exists185 * @param {string} id186 * @return {number} amount of rows187 **/188 hasRow(id) { return this.rowMap.hasOwnProperty(id); };189 /**190 * get row id by index, returns undefined if index don't match191 * @param {number} index192 * @return {string} row id193 **/194 getRowId(index) { 195 return Object.keys(this.rowMap).find(key => this.rowMap[key] === index);196 }197 /**198 * get row index by id, returns -1 if row does mot exist exists199 * @param {string} id200 * @return {number} row index or `undefined` if row dows not exists201 **/202 getRowIndex(id) { 203 return this.rowMap[id];204 /*205 if (this.rowMap.hasOwnProperty(id)) {206 return this.rowMap[id];207 }208 return undefined;209 */210 }211 /**212 * get table data as a CSV string213 * JSON is not able of sparse arrays so a own 214 * stringify method is necessary215 * @return {string} stringified array216 **/217 getCSV() {218 var table = this;219 var result = '';220 table.content.forEach(row => {221 for (let i = 0; i < row.length; i++) { 222 if (row[i]!==undefined) {223 result += (row[i]!==null) ? row[i] : 'null';224 }225 result += ',';226 }227 result = result.slice(0,-1); // remove the last comma228 result += "\n";229 });230 result = result.slice(0,-2); // remove the newLine231 return result;232 };233 /**234 * set table data from a string235 * JSON is not able of sparse arrays so a own parse method is necessary236 * cells are parsed as float, 'null' strings results in a null value237 * @param {string} dataString CSV String ("\n" as row and "," as column separator) i.e. as produced by table.getCSV();238 * @return {array} [rows][columns]239 **/240 parseCSV(dataString) {241 this.node?.log(`parseCSV ${dataString.length} bytes`);242 this.content.length = 0;243 this.content[0] = [];244 let rowStrings = dataString.split('\n');245 if (rowStrings.length>1) {246 rowStrings.forEach((rowString,row) => {247 this.content[row] = [];248 let columnStrings = rowString.split(',');249 columnStrings.forEach((cell,column) => {250 if (cell!=='') {251 if (cell!=='null') {252 this.content[row][column] = parseFloat(cell);253 } else {254 this.content[row][column] = null;255 }256 }257 });258 });259 }260 this.columns=this.content[0].length;261 this.rows=this.content.length;262 }263 /**264 * add a row 265 * @param {string|number} id of that row or index number266 * @param {array} [defaultRow=[]] (optional) default content267 * @return {number} index of existing or created row (-1 if error)268 **/269 addRow(id, defaultRow = []) {270 var table = this;271 switch (typeof id) {272 case 'string':273 if (!table.rowMap.hasOwnProperty(id)) { // add a new row274 table.content[table.content.length] = defaultRow;275 table.content[table.content.length-1].length = table.content[0].length; // fill sparse array276 table.rowMap[id] = table.rows = table.content.length-1;277 }278 return table.rowMap[id];279 case 'number':280 if (id < table.content.length) {281 return id;282 } else {283 this.node?.log(`timeTable.addRow(id) id index exceeds number of initialized rows is: ${id} max ${table.content.length}`);284 return -1;285 }286 default :287 this.node?.log(`timeTable.addRow(id) id has to be of type number or string! is: "${typeof id}"`);288 return -1;289 }290 };291 292 /**293 * forwardValue(row) will forward the last known value to the latest column to enable constant states like set values in the chart294 * The data points between the last "real" value and the forwarded value will be deleted to eliminate repeating values295 * @param {number|string} row row index or topic296 **/297 forwardValue(row) {298 var table = this;299 if (typeof row === 'string') {300 row = table.rowMap[row];301 }302 if (row > 0 && row < table.content.length) {303 let col = table.content[0].length-1;304 let endCol = col;305 if (table.content[row][col]!==undefined) {306 table.forwardFlags[row]===undefined;307 this.node?.log('value present no need to forward values');308 return;309 }310 while (col>0 && table.content[row][col]===undefined) {311 col--;312 }313 if (col>0) {314 table.content[row][endCol] = table.content[row][col];315 this.node?.log(`forwarded from:${col} to:${endCol}`);316 if (table.forwardFlags[row]!== undefined) {317 this.node?.log(`deleted ${table.forwardFlags[row]}`);318 delete table.content[row][table.forwardFlags[row]];319 }320 table.forwardFlags[row]=endCol;321 }322 this.node?.log(`forwardValue(${row})`); //, table.content[row]);323 } else {324 this.node?.log(`timeTable.forwardValue(row) row=${row} is unknown`);325 }326 }327 /**328 * add a column 329 * @param {number} time timestamp to be added330 * @return {number} column index331 **/332 addColumnTimestamp(time) {333 var table = this;334 // ToDo: It is more likely that the timestamp is at the end of the array:335 // let col = table.content[0].length-1;336 // while (col>0 && time<table.content[0][col]) {col--}; 337 let col = table.content[0].findIndex(element => element >= time);338 table.rows = table.content.length-1;339 table.columns = table.content[0].length;340 if (col < 0 || col === undefined) { // add new column @ the end341 table.columns++;342 table.content.forEach(row => {343 if (row===undefined) row=[];344 row.length = table.columns;345 });346 col=table.columns;347 } else if (time !== table.content[0][col]) { // insert a column348 table.content.forEach(row => {349 table.columns++;350 row.splice(col, 0, undefined);351 delete row[col];352 }); 353 } else { // existing column354 col++;355 }356 table.content[0][col-1] = time;357 table.rulesToApply.forEach(rule => {358 table[rule.plugin](rule);359 });360 return col-1;361 };362 /** 363 * add one or many values to one or many time columns364 * @param {number} time timestamp to be added365 * @param {array} values [{id,index,timestamp,value}] where index is used if present, timestamp is optional366 * @return {number} number of successfully added values367 **/368 addValues(time,values) {369 var table = this;370 var addedValues = 0;371 values.forEach(value => {372 let column = table.addColumnTimestamp((value.timestamp===undefined) ? time : value.timestamp);373 let row = table.addRow((value.index===undefined) ? value.id : value.index); // get or add row374 if (row>0) {375 table.content[row][column]=value.value;376 addedValues++;377 }378 });379 return addedValues;380 }381 /** 382 * clean the table array: 383 * 384 * - columns with no data in all rows will be removed from array385 * @param {object} [rule={}] rule config386 * @param {number} [start=0] start column index387 * @param {number} [end=length] end column index388 * @returns {array} reference to clean table389 **/390 cleanTable(rule = {},start = 0,end = 0){391 var table = this;392 var empty = true;393 var usedColumns = [];394 if (end === 0) end = table.content[0].length;395 for (let col=start; col<end; col++) { // do not use reduce as it works over the complete array396 empty = true;397 for (let row=1; row<table.content.length; row ++) {398 if (table.content[row][col]!==undefined) {empty = false; break;}399 }400 if (!empty) {401 usedColumns.push(col);402 }403 }404 this.node?.log(`cleanTable ${usedColumns.length}/${table.content[0].length} delete ${table.content[0].length-usedColumns.length} columns`);405 var newTable=[];406 for (let row=0; row<table.content.length; row ++) {407 newTable[row]=[];408 }409 usedColumns.forEach((index,col) => {410 for (let row=0; row<table.content.length; row ++) {411 if (table.content[row][index]!==undefined) {newTable[row][col]=table.content[row][index];} }412 });413 // this.node?.log(`done: ${newTable[0].length} columns left`);414 table.content=newTable;415 return newTable;416 }417 /** 418 * calculate the **minimum** value of a specific period419 * 420 * **For efficiency delete cells "on the go" where data is reduced by result of this function**421 * @param {object} rule rule config422 * @param {array} rowContent complete (sparse) row content423 * @param {number} start start column index424 * @param {number} end end column index425 **/426 minCalc(rule,rowContent,start,end) {427 var result = 0;428 var columnsToDelete = [];429 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array430 if (rowContent[i]) {431 if (rowContent[i]>result) result = rowContent[i];432 columnsToDelete.push(i); // record column indices to delete later433 }434 }435 if (columnsToDelete.length>1) {436 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected437 return result;438 } else {439 return undefined;440 }441 }442 /** 443 * calculate the **maximum** value of a specific period444 * 445 * **For efficiency delete cells "on the go" where data is reduced by result of this function**446 * @param {object} rule rule config447 * @param {array} rowContent complete (sparse) row content448 * @param {number} start start column index449 * @param {number} end end column index450 **/451 maxCalc(rule,rowContent,start,end) {452 var result = MAX_VALUE;453 var columnsToDelete = [];454 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array455 if (rowContent[i]) {456 if (rowContent[i]<result) result = rowContent[i];457 columnsToDelete.push(i); // record column indices to delete later458 }459 }460 if (columnsToDelete.length>1) {461 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected462 return result;463 } else {464 return undefined;465 }466 }467 /** 468 * calculate the **average** value of a specific period469 * 470 * **For efficiency delete cells "on the go" where data is reduced by result of this function**471 * @param {object} rule rule config472 * @param {array} rowContent complete (sparse) row content473 * @param {number} start start column index474 * @param {number} end end column index475 **/476 averageCalc(rule,rowContent,start,end) {477 var amount = 0;478 var total = 0;479 var columnsToDelete = [];480 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array481 if (rowContent[i]) {482 total += rowContent[i];483 amount++;484 columnsToDelete.push(i); // record column indices to delete later485 }486 }487 if (columnsToDelete.length>1) {488 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected489 this.node?.log(`averageCalc [${start}:${end}] = ${total/amount} columns deleted:${columnsToDelete.length}`);490 return total/amount;491 } else {492 return undefined;493 }494 }495 /** 496 * calculate the **sum** value of a specific period497 * 498 * **For efficiency delete cells "on the go" where data is reduced by result of this function**499 * @param {object} rule rule config500 * @param {array} rowContent complete (sparse) row content501 * @param {number} start start column index502 * @param {number} end end column index503 **/504 sumCalc(rule,rowContent,start,end) {505 var sum = 0;506 var columnsToDelete = [];507 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array508 if (rowContent[i]) {509 sum += rowContent[i];510 columnsToDelete.push(i); // record column indices to delete later511 }512 }513 if (columnsToDelete.length>1) {514 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected515 return sum;516 } else {517 return undefined;518 }519 }520 /** 521 * reduce data by rule522 * @param {object} rule rule object containing **method,older,threshold,span,periods** property523 * @param {number} startThreshold (optional = 0) period start timestamp (in seconds) 524 * @return {number} next valid threshold525 **/526 reduceData(rule,startThreshold = 0) { 527 var table = this;528 if (startThreshold <= 0 ) startThreshold = table.content[0][0];529 const endThreshold = Date.now()/1000 - (rule.older * rule.threshold); 530 if (table.content[0][0]===undefined || table.content[0][0]>endThreshold) {531 if (table.content[0][0]!==undefined) {532 this.node?.log(`reduceData: No data before ${new Date(endThreshold*1000).toLocaleDateString()}`);533 }534 return -1; // no data available older than time Limit;535 }536 537 const periodSeconds = rule.span * rule.periods;538 const startColumn = table.content[0].findIndex(timestamp => timestamp >= startThreshold);539 const endColumn = table.content[0].findIndex(timestamp => timestamp > endThreshold);540 var cursor = {541 nextThreshold:startThreshold+periodSeconds,542 start:{timestamp:startThreshold,column:startColumn},543 end:{timestamp:startThreshold,column:startColumn},544 ruleEnd:{timestamp:endThreshold,column:endColumn-1},545 midPoint:0546 };547 var result = 0;548 var resultAt = 0;549 // this.node?.log(`reduceData between ${startColumn}/${new Date(startThreshold*1000).toLocaleDateString()} and ${endColumn}/${new Date(endThreshold*1000).toLocaleDateString()} rule="${rule.method}"`); // ,cursor,rule550 for (let row = 1; row < table.content.length; row ++) {551 if (rule.applyTo==='' || (Array.isArray(rule.applyTo) && rule.applyTo.includes(table.getRowId(row)))) {552 cursor.start.column=startColumn;553 cursor.start.timestamp=startThreshold;554 cursor.end.column=startColumn;555 cursor.end.timestamp=startThreshold;556 cursor.nextThreshold=startThreshold+periodSeconds;557 while (cursor.end.column < endColumn) {558 while(cursor.end.timestamp < cursor.nextThreshold) {559 cursor.end.column++;560 cursor.end.timestamp = table.content[0][cursor.end.column];561 }562 if (cursor.end.column-cursor.start.column > 1) {563 result = table[rule.method+'Calc'](rule,table.content[row],cursor.start.column,cursor.end.column);564 if (result!==undefined) {565 switch (rule.resultAt) {566 case 0: resultAt = cursor.start.column; break;567 case 1: resultAt = cursor.end.column; break;568 case 2: resultAt = cursor.start.column + Math.floor((cursor.end.column-cursor.start.column)/2); break;569 case 3: resultAt = -1; break;570 }571 // this.node?.log(`calc range:${cursor.start.column}-${cursor.end.column} @${resultAt}=${result}`);572 if (resultAt>=0) {573 table.content[row][resultAt] = result;574 } else {575 this.addValues(cursor.start.timestamp+((cursor.end.timestamp-cursor.start.timestamp)/2),{index:row,value:result});576 }577 }578 }579 cursor.end.column++;580 cursor.start.column = cursor.end.column;581 cursor.start.timestamp = table.content[0][cursor.start.column];582 cursor.end.timestamp = cursor.start.timestamp;583 cursor.nextThreshold += periodSeconds;584 }585 }586 }587 return endThreshold;588 }589 /** 590 * limit the amount of columns591 * @param {array} rules array of rule objects592 * @return {number} number of removed columns593 **/594 applyReductionRules(rules){595 var table = this;596 table.removedData = 0;597 let startTime = 0;598 let reductionRules = rules.filter(rule => rule.threshold > 0 && rule.older > 0 && rule.enabled);599 reductionRules.sort((a,b)=>{return (b.older*b.threshold) - (a.older*a.threshold);});600 //this.node?.log(`applyScheduledRules sorted`,reductionRules);601 reductionRules.forEach((rule, index) => {602 //this.node?.log(`applied #${index}:"${rule.plugin} start ${startTime}`);603 startTime = table.reduceData(rule,startTime);604 //this.node?.log(`applied #${index}:"${rule.plugin}" next rule start ${startTime}s`)605 });606 return table.removedData;607 }608 /** 609 * limit the amount of columns610 * @param {object} rule rule object containing **columns** property611 * @return {number} number of removed columns612 **/613 limitColumns(rule) { 614 var table = this;615 let toRemove = 0;616 if (table.content[0].length > rule.columns) {617 toRemove = table.content[0].length - rule.columns;618 table.content.forEach(row => row.splice(0,toRemove));619 }620 return toRemove;621 }622 /** 623 * limit the time span624 * @param {object} rule rule object containing **last and period** property625 * @return {number} number of removed columns626 **/627 limitTime(rule) { 628 var table = this;629 if (table.content[0].length < 1) return 0;630 let limit = Date.now()/1000 - (rule.last * rule.period);631 let toRemove = 0;632 while (toRemove < table.content[0].length && table.content[0][toRemove] < limit) { toRemove ++;} if (toRemove > 0) {633 table.content.forEach(row => row.splice(0,toRemove));634 this.node?.log(`limitTime columns deleted:${toRemove} left: ${table.getWidth()}`);635 }636 return toRemove;637 }638 /**639 * apply a set one rule to the data640 * @param {object} rule object defining the rule641 * @return {number} number of affected columns. -1 if an error accrued642 */643 applyRule(rule) {644 var table = this;645 var result = 0;646 if (typeof table[rule.plugin] === 'function') {647 result += table[rule.plugin](rule);648 } else {649 this.node?.log(`rule plugin "${rule.plugin}" unknown!`);650 }651 return result;652 }653 /**654 * set timers for scheduled rules655 * all existing timers will be canceled656 * @param {array} rules array of objects defining the rules657 * @param {boolean} [client = false] flag if client values should be used658 * @return {number} amount of timers set659 */660 setTimers(rules, client = false) {661 var table = this;662 // clear running timers663 table.schedules.forEach((schedule,index) => {664 clearInterval(table.schedules[index]);665 });666 table.schedules = [];667 // set reduction rules timers668 let reductionRules = rules.filter(rule => rule.threshold > 0 && rule.older > 0 && rule.enabled);669 reductionRules.sort((a,b)=>{return (b.older*b.threshold) - (a.older*a.threshold);});670 var timeBase = Date.now()/1000;671 var startTime = 0;672 reductionRules.forEach(rule => {673 if (!rule.enabled) return;674 rule.startTime = startTime;675 rule.endTime = timeBase - (rule.older * rule.threshold); // advance start time to end of period ("older than")676 677 table.schedules.push(setInterval(() => {678 var scheduledRule = rule;679 table.reduceData(scheduledRule, scheduledRule.startTime);680 },rule.span * rule.periods * 1000));681 startTime = rule.endTime;682 this.node?.log(`scheduler "${rule.plugin}" from ${new Date(rule.startTime*1000).toLocaleDateString()} ${new Date(rule.startTime*1000).toLocaleTimeString()} to ${new Date(rule.endTime*1000).toLocaleDateString()} ${new Date(rule.endTime*1000).toLocaleTimeString()} set for every ${rule.span * rule.periods}s`);683 });684 // set other rules685 rules.forEach((rule,index) => {686 let intervalMs = 0;687 switch (rule.plugin) {688 case 'reduceData': // already handled above689 return;690 case 'limitColumns':691 if (!client && rule.limitColumnsPeriods>0) { // only if by time692 intervalMs = rule.limitColumnsEvery * rule.limitColumnsPeriods * 1000;693 } if (client && rule.checkEvery>0) {694 intervalMs = rule.checkEvery * 1000;695 }696 break;697 case 'limitTime':698 if (!client && rule.limitColumnsPeriods>0) { // only if by time 699 intervalMs = rule.limitTimeEvery * rule.limitTimePeriods * 1000;700 } if (client && rule.checkEvery>0) {701 intervalMs = rule.checkEvery * 1000;702 } 703 break;704 case 'cleanTable':705 intervalMs = rule.cleanEvery * rule.cleanPeriod * 1000;706 break;707 }708 if (intervalMs>0 && rule.enabled) {709 table.schedules.push(setInterval(() => {710 var scheduledRule = rule;711 table.applyRule(scheduledRule);712 },intervalMs));713 this.node?.log(`scheduler "${rule.plugin}" every ${intervalMs/1000}s`);714 }715 });716 return table.schedules.length;717 }718 /**719 * apply a set of rules to the data720 * only enabled rules are applied721 * the array will be sorted: 1st limit amount, 2nd limit total time and then scheduled actions722 * @param {array} rules array of objects defining the rules723 * @return {number} number of affected columns. -1 if an error accrued724 */725 applyRules(rules) {726 var table = this;727 var result = 0;728 if (rules === undefined || !Array.isArray(rules)) {729 this.node?.warn(`applyRules: rules are undefined! exiting.`);730 return -1;731 }732 table.singleRules = rules.filter(rule => rule.plugin === 'limitColumns' && rule.enabled);733 table.singleRules.concat(rules.filter(rule => rule.plugin === 'limitTime' && rule.enabled));734 table.singleRules.forEach((rule,index) => {735 result += table.applyRule(rule);736 });737 result+= table.applyReductionRules(rules);738 return result;739 }740 getSizes() {741 var table = this;742 var result = {743 memoryBytes : 0,744 memoryString : '',745 columns : table.content[0].length,746 rows : table.content.length-1, 747 cellsUsed : 0,748 cellsTotal : table.content[0].length * (table.content.length-1),749 cellsUnused : 0,750 ratio: 1,751 ratioPercent: "100%",752 timeTotal : 0,753 };754 table.content.forEach((row,rowIndex) =>{755 if (rowIndex>0) {756 row.forEach(() => result.cellsUsed++);757 }758 });759 result.memoryBytes = (result.cellsUsed + result.columns) * 8;760 result.memoryString = (result.memoryBytes>1024) ? (result.memoryBytes / 1024).toFixed(2) + 'kb' :761 (result.memoryBytes> 1024*1024) ? (result.memoryBytes / 1024 / 1024).toFixed(2) + 'mb' :762 result.memoryBytes + 'bytes';763 result.cellsUnused = result.cellsTotal - result.cellsUsed;764 if (result.cellsTotal>0) {765 result.ratio = result.cellsUsed / result.cellsTotal;766 result.ratioPercent = (result.ratio*100).toFixed(2)+"%";767 }768 result.timeTotal = table.content[0][table.content[0].length-1] - table.content[0][0];769 return result;770 }771}772// module.exports = TimeTable;...

Full Screen

Full Screen

timeTable.esm.js

Source:timeTable.esm.js Github

copy

Full Screen

1/**2 * Copyright 2021 Christian Meinert3 *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 at7 *8 * http://www.apache.org/licenses/LICENSE-2.09 *10 * Unless required by applicable law or agreed to in writing, software11 * 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 and14 * limitations under the License.15 **/16/**17 * Notes:18 * 19 * all known issues or tasks are marked with "ToDo:"20 * 21 */22class TimeTable {23 constructor(content = [[]], rowMap = {}) {24 this.node = console;25 this.setTable(content,rowMap);26 this.forwardFlags = [{forwarded:0, col:undefined}];27 this.schedules = [];28 this.rulesToApply = [];29 }30 /**31 * set a logger class. It must contain `log`, `warn` and `error` functions32 * @param {object} logger logger class (defaults to `console`)33 **/34 setLogger(logger) {35 if (logger === undefined) {36 this.node=undefined;37 } else {38 console.log(`setLogger ${(typeof logger.log === 'function') ? "ok" : "failed"}`);39 if (typeof logger.log === 'function') {40 this.node = logger;41 }42 }43 }44 /**45 * deep merge object / array46 * @param {object} target target object47 * @param {object} source source object48 **/49 mergeObject(target,source) {50 if (typeof source === 'object') {51 Object.keys(source).forEach(element => {52 if (typeof source[element] !== "object" || source[element]===null) {53 target[element] = source[element];54 } else {55 if (!target.hasOwnProperty(element)) {56 target[element] = (Array.isArray(source[element])) ? [] : {};57 }58 this.mergeObject(target[element],source[element]);59 }60 });61 } else {62 target=source;63 }64 }65 /**66 * get reference to table data67 * @return {array} [rows][columns]68 **/69 getTable() { return this.content };70 /**71 * get last timestamp of the table72 * @param {number} [offset=0] number of rows from end (default = 0)73 * @return {number} timestamp74 **/75 getLastTimestamp(offset = 0) { return this.content[0][this.content[0].length-1-offset] };76 /**77 * get timestamp of the table78 * @param {number} [offset=0] number of rows from beginning (default = 0)79 * @return {number} timestamp80 **/81 getTimestamp(offset = 0) { return this.content[0][offset] };82 /**83 * set table data (reference)84 * row map will be deleted if not provided85 * @param {array} data [rows][columns] (reference)86 * @param {object} rowMap (reference) map to all row topic:indices87 **/88 setTable(data, rowMap) { 89 if (Array.isArray(data)) {90 this.content = data;91 if (rowMap===undefined) ; else {92 this.rowMap = rowMap;93 }94 Object.keys(this.rowMap).forEach(topic => {95 let i = this.rowMap[topic];96 if (!Array.isArray(this.content[i])) this.content[i] = [];97 });98 this.columns = this.content[0].length;99 this.rows = this.content.length;100 } else {this.node?.warn('timeTable.setTable() data must be an array!', data);}101 };102 /**103 * get row map104 * @return {object} rowMap {id:row}105 **/106 getRowMap() { return this.rowMap };107 /**108 * set row map109 * @param {object} rowMap {id:row}110 **/111 setRowMap(rowMap) {112 if (typeof rowMap === 'object') {113 // better keep the reference114 Object.keys(this.rowMap).forEach(key => delete this.rowMap[key]);115 this.mergeObject(this.rowMap,rowMap);116 } else {this.node?.warn('timeTable.rowMap() rowMap must be an object!', rowMap);}117 };118 /**119 * set limit rules to apply to incoming data. Previous limits will be deleted120 * @param {array} rules set of rules [{plugin:"limitColumns/limitTime",columns:{number},last:{number},period:{number},enabled:{boolean}}}]121 **/122 setLimits(rules) {123 this.rulesToApply = [];124 rules.forEach(rule => {125 if (rule.enabled && rule.limitColumnsPeriods===0) {126 if (rule.plugin===`limitColumns` || rule.plugin==='limitTime') {127 this.rulesToApply.push(rule);128 }129 }130 });131 }132 /**133 * remap rows: sort and filter existing rows according to an array of ids134 * @param {array} ids new array of keys to remap existing data135 * @return {object} updates rowMap {id:row}136 **/137 remapRows(ids) {138 if (Array.isArray(ids) && ids.length>0) {139 let mappingFlag = false;140 var tempTable = [];141 tempTable[0]=this.content[0] || [];142 ids.forEach((id,index) => {143 if (!mappingFlag && index+1!==this.rowMap[id]) { mappingFlag = true;} if (this.hasRow(id)) { // use existing row144 tempTable[index+1] = this.content[this.rowMap[id]] || [];145 } else { // create new row146 tempTable[index+1] = Array(this.content[0].length);147 this.rowMap[id] = index+1;148 }149 });150 ids.forEach((id,index) => { 151 this.rowMap[id]=index+1; // update the map152 });153 this.content=tempTable;154 if (mappingFlag) {this.node?.log(`remappedRows to [${Object.keys(this.rowMap).toString()}]`);}155 }156 return this.content;157 }158 /**159 * get width (columns)160 * @return {number} amount of columns161 **/162 getWidth() { return this.content[0].length };163 /**164 * get height (rows) without time row165 * @return {number} amount of rows166 **/167 getHeight() { return this.content.length-1 };168 /**169 * set height (rows) and adding not populated rows170 * @param {number} rows of rows without time row171 **/172 setHeight(rows) {173 var table = this;174 // this.node?.log('setHeight',table.content);175 let row = table.content.length;176 while (row <= rows) {177 table.content[row]=Array(table.content[0].length);178 row++;179 } 180 table.rows = table.content.length-1;181 };182 /**183 * checks if a row id exists184 * @param {string} id185 * @return {number} amount of rows186 **/187 hasRow(id) { return this.rowMap.hasOwnProperty(id); };188 /**189 * get row id by index, returns undefined if index don't match190 * @param {number} index191 * @return {string} row id192 **/193 getRowId(index) { 194 return Object.keys(this.rowMap).find(key => this.rowMap[key] === index);195 }196 /**197 * get row index by id, returns -1 if row does mot exist exists198 * @param {string} id199 * @return {number} row index or `undefined` if row dows not exists200 **/201 getRowIndex(id) { 202 return this.rowMap[id];203 /*204 if (this.rowMap.hasOwnProperty(id)) {205 return this.rowMap[id];206 }207 return undefined;208 */209 }210 /**211 * get table data as a CSV string212 * JSON is not able of sparse arrays so a own 213 * stringify method is necessary214 * @return {string} stringified array215 **/216 getCSV() {217 var table = this;218 var result = '';219 table.content.forEach(row => {220 for (let i = 0; i < row.length; i++) { 221 if (row[i]!==undefined) {222 result += (row[i]!==null) ? row[i] : 'null';223 }224 result += ',';225 }226 result = result.slice(0,-1); // remove the last comma227 result += "\n";228 });229 result = result.slice(0,-2); // remove the newLine230 return result;231 };232 /**233 * set table data from a string234 * JSON is not able of sparse arrays so a own parse method is necessary235 * cells are parsed as float, 'null' strings results in a null value236 * @param {string} dataString CSV String ("\n" as row and "," as column separator) i.e. as produced by table.getCSV();237 * @return {array} [rows][columns]238 **/239 parseCSV(dataString) {240 this.node?.log(`parseCSV ${dataString.length} bytes`);241 this.content.length = 0;242 this.content[0] = [];243 let rowStrings = dataString.split('\n');244 if (rowStrings.length>1) {245 rowStrings.forEach((rowString,row) => {246 this.content[row] = [];247 let columnStrings = rowString.split(',');248 columnStrings.forEach((cell,column) => {249 if (cell!=='') {250 if (cell!=='null') {251 this.content[row][column] = parseFloat(cell);252 } else {253 this.content[row][column] = null;254 }255 }256 });257 });258 }259 this.columns=this.content[0].length;260 this.rows=this.content.length;261 }262 /**263 * add a row 264 * @param {string|number} id of that row or index number265 * @param {array} [defaultRow=[]] (optional) default content266 * @return {number} index of existing or created row (-1 if error)267 **/268 addRow(id, defaultRow = []) {269 var table = this;270 switch (typeof id) {271 case 'string':272 if (!table.rowMap.hasOwnProperty(id)) { // add a new row273 table.content[table.content.length] = defaultRow;274 table.content[table.content.length-1].length = table.content[0].length; // fill sparse array275 table.rowMap[id] = table.rows = table.content.length-1;276 }277 return table.rowMap[id];278 case 'number':279 if (id < table.content.length) {280 return id;281 } else {282 this.node?.log(`timeTable.addRow(id) id index exceeds number of initialized rows is: ${id} max ${table.content.length}`);283 return -1;284 }285 default :286 this.node?.log(`timeTable.addRow(id) id has to be of type number or string! is: "${typeof id}"`);287 return -1;288 }289 };290 291 /**292 * forwardValue(row) will forward the last known value to the latest column to enable constant states like set values in the chart293 * The data points between the last "real" value and the forwarded value will be deleted to eliminate repeating values294 * @param {number|string} row row index or topic295 **/296 forwardValue(row) {297 var table = this;298 if (typeof row === 'string') {299 row = table.rowMap[row];300 }301 if (row > 0 && row < table.content.length) {302 let col = table.content[0].length-1;303 let endCol = col;304 if (table.content[row][col]!==undefined) {305 table.forwardFlags[row]===undefined;306 this.node?.log('value present no need to forward values');307 return;308 }309 while (col>0 && table.content[row][col]===undefined) {310 col--;311 }312 if (col>0) {313 table.content[row][endCol] = table.content[row][col];314 this.node?.log(`forwarded from:${col} to:${endCol}`);315 if (table.forwardFlags[row]!== undefined) {316 this.node?.log(`deleted ${table.forwardFlags[row]}`);317 delete table.content[row][table.forwardFlags[row]];318 }319 table.forwardFlags[row]=endCol;320 }321 this.node?.log(`forwardValue(${row})`); //, table.content[row]);322 } else {323 this.node?.log(`timeTable.forwardValue(row) row=${row} is unknown`);324 }325 }326 /**327 * add a column 328 * @param {number} time timestamp to be added329 * @return {number} column index330 **/331 addColumnTimestamp(time) {332 var table = this;333 // ToDo: It is more likely that the timestamp is at the end of the array:334 // let col = table.content[0].length-1;335 // while (col>0 && time<table.content[0][col]) {col--}; 336 let col = table.content[0].findIndex(element => element >= time);337 table.rows = table.content.length-1;338 table.columns = table.content[0].length;339 if (col < 0 || col === undefined) { // add new column @ the end340 table.columns++;341 table.content.forEach(row => {342 if (row===undefined) row=[];343 row.length = table.columns;344 });345 col=table.columns;346 } else if (time !== table.content[0][col]) { // insert a column347 table.content.forEach(row => {348 table.columns++;349 row.splice(col, 0, undefined);350 delete row[col];351 }); 352 } else { // existing column353 col++;354 }355 table.content[0][col-1] = time;356 table.rulesToApply.forEach(rule => {357 table[rule.plugin](rule);358 });359 return col-1;360 };361 /** 362 * add one or many values to one or many time columns363 * @param {number} time timestamp to be added364 * @param {array} values [{id,index,timestamp,value}] where index is used if present, timestamp is optional365 * @return {number} number of successfully added values366 **/367 addValues(time,values) {368 var table = this;369 var addedValues = 0;370 values.forEach(value => {371 let column = table.addColumnTimestamp((value.timestamp===undefined) ? time : value.timestamp);372 let row = table.addRow((value.index===undefined) ? value.id : value.index); // get or add row373 if (row>0) {374 table.content[row][column]=value.value;375 addedValues++;376 }377 });378 return addedValues;379 }380 /** 381 * clean the table array: 382 * 383 * - columns with no data in all rows will be removed from array384 * @param {object} [rule={}] rule config385 * @param {number} [start=0] start column index386 * @param {number} [end=length] end column index387 * @returns {array} reference to clean table388 **/389 cleanTable(rule = {},start = 0,end = 0){390 var table = this;391 var empty = true;392 var usedColumns = [];393 if (end === 0) end = table.content[0].length;394 for (let col=start; col<end; col++) { // do not use reduce as it works over the complete array395 empty = true;396 for (let row=1; row<table.content.length; row ++) {397 if (table.content[row][col]!==undefined) {empty = false; break;}398 }399 if (!empty) {400 usedColumns.push(col);401 }402 }403 this.node?.log(`cleanTable ${usedColumns.length}/${table.content[0].length} delete ${table.content[0].length-usedColumns.length} columns`);404 var newTable=[];405 for (let row=0; row<table.content.length; row ++) {406 newTable[row]=[];407 }408 usedColumns.forEach((index,col) => {409 for (let row=0; row<table.content.length; row ++) {410 if (table.content[row][index]!==undefined) {newTable[row][col]=table.content[row][index];} }411 });412 // this.node?.log(`done: ${newTable[0].length} columns left`);413 table.content=newTable;414 return newTable;415 }416 /** 417 * calculate the **minimum** value of a specific period418 * 419 * **For efficiency delete cells "on the go" where data is reduced by result of this function**420 * @param {object} rule rule config421 * @param {array} rowContent complete (sparse) row content422 * @param {number} start start column index423 * @param {number} end end column index424 **/425 minCalc(rule,rowContent,start,end) {426 var result = 0;427 var columnsToDelete = [];428 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array429 if (rowContent[i]) {430 if (rowContent[i]>result) result = rowContent[i];431 columnsToDelete.push(i); // record column indices to delete later432 }433 }434 if (columnsToDelete.length>1) {435 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected436 return result;437 } else {438 return undefined;439 }440 }441 /** 442 * calculate the **maximum** value of a specific period443 * 444 * **For efficiency delete cells "on the go" where data is reduced by result of this function**445 * @param {object} rule rule config446 * @param {array} rowContent complete (sparse) row content447 * @param {number} start start column index448 * @param {number} end end column index449 **/450 maxCalc(rule,rowContent,start,end) {451 var result = MAX_VALUE;452 var columnsToDelete = [];453 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array454 if (rowContent[i]) {455 if (rowContent[i]<result) result = rowContent[i];456 columnsToDelete.push(i); // record column indices to delete later457 }458 }459 if (columnsToDelete.length>1) {460 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected461 return result;462 } else {463 return undefined;464 }465 }466 /** 467 * calculate the **average** value of a specific period468 * 469 * **For efficiency delete cells "on the go" where data is reduced by result of this function**470 * @param {object} rule rule config471 * @param {array} rowContent complete (sparse) row content472 * @param {number} start start column index473 * @param {number} end end column index474 **/475 averageCalc(rule,rowContent,start,end) {476 var amount = 0;477 var total = 0;478 var columnsToDelete = [];479 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array480 if (rowContent[i]) {481 total += rowContent[i];482 amount++;483 columnsToDelete.push(i); // record column indices to delete later484 }485 }486 if (columnsToDelete.length>1) {487 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected488 this.node?.log(`averageCalc [${start}:${end}] = ${total/amount} columns deleted:${columnsToDelete.length}`);489 return total/amount;490 } else {491 return undefined;492 }493 }494 /** 495 * calculate the **sum** value of a specific period496 * 497 * **For efficiency delete cells "on the go" where data is reduced by result of this function**498 * @param {object} rule rule config499 * @param {array} rowContent complete (sparse) row content500 * @param {number} start start column index501 * @param {number} end end column index502 **/503 sumCalc(rule,rowContent,start,end) {504 var sum = 0;505 var columnsToDelete = [];506 for (let i=start; i<end; i++) { // do not use reduce as it works over the complete array507 if (rowContent[i]) {508 sum += rowContent[i];509 columnsToDelete.push(i); // record column indices to delete later510 }511 }512 if (columnsToDelete.length>1) {513 columnsToDelete.forEach(column => delete rowContent[column]); // delete cells if more than one cell detected514 return sum;515 } else {516 return undefined;517 }518 }519 /** 520 * reduce data by rule521 * @param {object} rule rule object containing **method,older,threshold,span,periods** property522 * @param {number} startThreshold (optional = 0) period start timestamp (in seconds) 523 * @return {number} next valid threshold524 **/525 reduceData(rule,startThreshold = 0) { 526 var table = this;527 if (startThreshold <= 0 ) startThreshold = table.content[0][0];528 const endThreshold = Date.now()/1000 - (rule.older * rule.threshold); 529 if (table.content[0][0]===undefined || table.content[0][0]>endThreshold) {530 if (table.content[0][0]!==undefined) {531 this.node?.log(`reduceData: No data before ${new Date(endThreshold*1000).toLocaleDateString()}`);532 }533 return -1; // no data available older than time Limit;534 }535 536 const periodSeconds = rule.span * rule.periods;537 const startColumn = table.content[0].findIndex(timestamp => timestamp >= startThreshold);538 const endColumn = table.content[0].findIndex(timestamp => timestamp > endThreshold);539 var cursor = {540 nextThreshold:startThreshold+periodSeconds,541 start:{timestamp:startThreshold,column:startColumn},542 end:{timestamp:startThreshold,column:startColumn},543 ruleEnd:{timestamp:endThreshold,column:endColumn-1},544 midPoint:0545 };546 var result = 0;547 var resultAt = 0;548 // this.node?.log(`reduceData between ${startColumn}/${new Date(startThreshold*1000).toLocaleDateString()} and ${endColumn}/${new Date(endThreshold*1000).toLocaleDateString()} rule="${rule.method}"`); // ,cursor,rule549 for (let row = 1; row < table.content.length; row ++) {550 if (rule.applyTo==='' || (Array.isArray(rule.applyTo) && rule.applyTo.includes(table.getRowId(row)))) {551 cursor.start.column=startColumn;552 cursor.start.timestamp=startThreshold;553 cursor.end.column=startColumn;554 cursor.end.timestamp=startThreshold;555 cursor.nextThreshold=startThreshold+periodSeconds;556 while (cursor.end.column < endColumn) {557 while(cursor.end.timestamp < cursor.nextThreshold) {558 cursor.end.column++;559 cursor.end.timestamp = table.content[0][cursor.end.column];560 }561 if (cursor.end.column-cursor.start.column > 1) {562 result = table[rule.method+'Calc'](rule,table.content[row],cursor.start.column,cursor.end.column);563 if (result!==undefined) {564 switch (rule.resultAt) {565 case 0: resultAt = cursor.start.column; break;566 case 1: resultAt = cursor.end.column; break;567 case 2: resultAt = cursor.start.column + Math.floor((cursor.end.column-cursor.start.column)/2); break;568 case 3: resultAt = -1; break;569 }570 // this.node?.log(`calc range:${cursor.start.column}-${cursor.end.column} @${resultAt}=${result}`);571 if (resultAt>=0) {572 table.content[row][resultAt] = result;573 } else {574 this.addValues(cursor.start.timestamp+((cursor.end.timestamp-cursor.start.timestamp)/2),{index:row,value:result});575 }576 }577 }578 cursor.end.column++;579 cursor.start.column = cursor.end.column;580 cursor.start.timestamp = table.content[0][cursor.start.column];581 cursor.end.timestamp = cursor.start.timestamp;582 cursor.nextThreshold += periodSeconds;583 }584 }585 }586 return endThreshold;587 }588 /** 589 * limit the amount of columns590 * @param {array} rules array of rule objects591 * @return {number} number of removed columns592 **/593 applyReductionRules(rules){594 var table = this;595 table.removedData = 0;596 let startTime = 0;597 let reductionRules = rules.filter(rule => rule.threshold > 0 && rule.older > 0 && rule.enabled);598 reductionRules.sort((a,b)=>{return (b.older*b.threshold) - (a.older*a.threshold);});599 //this.node?.log(`applyScheduledRules sorted`,reductionRules);600 reductionRules.forEach((rule, index) => {601 //this.node?.log(`applied #${index}:"${rule.plugin} start ${startTime}`);602 startTime = table.reduceData(rule,startTime);603 //this.node?.log(`applied #${index}:"${rule.plugin}" next rule start ${startTime}s`)604 });605 return table.removedData;606 }607 /** 608 * limit the amount of columns609 * @param {object} rule rule object containing **columns** property610 * @return {number} number of removed columns611 **/612 limitColumns(rule) { 613 var table = this;614 let toRemove = 0;615 if (table.content[0].length > rule.columns) {616 toRemove = table.content[0].length - rule.columns;617 table.content.forEach(row => row.splice(0,toRemove));618 }619 return toRemove;620 }621 /** 622 * limit the time span623 * @param {object} rule rule object containing **last and period** property624 * @return {number} number of removed columns625 **/626 limitTime(rule) { 627 var table = this;628 if (table.content[0].length < 1) return 0;629 let limit = Date.now()/1000 - (rule.last * rule.period);630 let toRemove = 0;631 while (toRemove < table.content[0].length && table.content[0][toRemove] < limit) { toRemove ++;} if (toRemove > 0) {632 table.content.forEach(row => row.splice(0,toRemove));633 this.node?.log(`limitTime columns deleted:${toRemove} left: ${table.getWidth()}`);634 }635 return toRemove;636 }637 /**638 * apply a set one rule to the data639 * @param {object} rule object defining the rule640 * @return {number} number of affected columns. -1 if an error accrued641 */642 applyRule(rule) {643 var table = this;644 var result = 0;645 if (typeof table[rule.plugin] === 'function') {646 result += table[rule.plugin](rule);647 } else {648 this.node?.log(`rule plugin "${rule.plugin}" unknown!`);649 }650 return result;651 }652 /**653 * set timers for scheduled rules654 * all existing timers will be canceled655 * @param {array} rules array of objects defining the rules656 * @param {boolean} [client = false] flag if client values should be used657 * @return {number} amount of timers set658 */659 setTimers(rules, client = false) {660 var table = this;661 // clear running timers662 table.schedules.forEach((schedule,index) => {663 clearInterval(table.schedules[index]);664 });665 table.schedules = [];666 // set reduction rules timers667 let reductionRules = rules.filter(rule => rule.threshold > 0 && rule.older > 0 && rule.enabled);668 reductionRules.sort((a,b)=>{return (b.older*b.threshold) - (a.older*a.threshold);});669 var timeBase = Date.now()/1000;670 var startTime = 0;671 reductionRules.forEach(rule => {672 if (!rule.enabled) return;673 rule.startTime = startTime;674 rule.endTime = timeBase - (rule.older * rule.threshold); // advance start time to end of period ("older than")675 676 table.schedules.push(setInterval(() => {677 var scheduledRule = rule;678 table.reduceData(scheduledRule, scheduledRule.startTime);679 },rule.span * rule.periods * 1000));680 startTime = rule.endTime;681 this.node?.log(`scheduler "${rule.plugin}" from ${new Date(rule.startTime*1000).toLocaleDateString()} ${new Date(rule.startTime*1000).toLocaleTimeString()} to ${new Date(rule.endTime*1000).toLocaleDateString()} ${new Date(rule.endTime*1000).toLocaleTimeString()} set for every ${rule.span * rule.periods}s`);682 });683 // set other rules684 rules.forEach((rule,index) => {685 let intervalMs = 0;686 switch (rule.plugin) {687 case 'reduceData': // already handled above688 return;689 case 'limitColumns':690 if (!client && rule.limitColumnsPeriods>0) { // only if by time691 intervalMs = rule.limitColumnsEvery * rule.limitColumnsPeriods * 1000;692 } if (client && rule.checkEvery>0) {693 intervalMs = rule.checkEvery * 1000;694 }695 break;696 case 'limitTime':697 if (!client && rule.limitColumnsPeriods>0) { // only if by time 698 intervalMs = rule.limitTimeEvery * rule.limitTimePeriods * 1000;699 } if (client && rule.checkEvery>0) {700 intervalMs = rule.checkEvery * 1000;701 } 702 break;703 case 'cleanTable':704 intervalMs = rule.cleanEvery * rule.cleanPeriod * 1000;705 break;706 }707 if (intervalMs>0 && rule.enabled) {708 table.schedules.push(setInterval(() => {709 var scheduledRule = rule;710 table.applyRule(scheduledRule);711 },intervalMs));712 this.node?.log(`scheduler "${rule.plugin}" every ${intervalMs/1000}s`);713 }714 });715 return table.schedules.length;716 }717 /**718 * apply a set of rules to the data719 * only enabled rules are applied720 * the array will be sorted: 1st limit amount, 2nd limit total time and then scheduled actions721 * @param {array} rules array of objects defining the rules722 * @return {number} number of affected columns. -1 if an error accrued723 */724 applyRules(rules) {725 var table = this;726 var result = 0;727 if (rules === undefined || !Array.isArray(rules)) {728 this.node?.warn(`applyRules: rules are undefined! exiting.`);729 return -1;730 }731 table.singleRules = rules.filter(rule => rule.plugin === 'limitColumns' && rule.enabled);732 table.singleRules.concat(rules.filter(rule => rule.plugin === 'limitTime' && rule.enabled));733 table.singleRules.forEach((rule,index) => {734 result += table.applyRule(rule);735 });736 result+= table.applyReductionRules(rules);737 return result;738 }739 getSizes() {740 var table = this;741 var result = {742 memoryBytes : 0,743 memoryString : '',744 columns : table.content[0].length,745 rows : table.content.length-1, 746 cellsUsed : 0,747 cellsTotal : table.content[0].length * (table.content.length-1),748 cellsUnused : 0,749 ratio: 1,750 ratioPercent: "100%",751 timeTotal : 0,752 };753 table.content.forEach((row,rowIndex) =>{754 if (rowIndex>0) {755 row.forEach(() => result.cellsUsed++);756 }757 });758 result.memoryBytes = (result.cellsUsed + result.columns) * 8;759 result.memoryString = (result.memoryBytes>1024) ? (result.memoryBytes / 1024).toFixed(2) + 'kb' :760 (result.memoryBytes> 1024*1024) ? (result.memoryBytes / 1024 / 1024).toFixed(2) + 'mb' :761 result.memoryBytes + 'bytes';762 result.cellsUnused = result.cellsTotal - result.cellsUsed;763 if (result.cellsTotal>0) {764 result.ratio = result.cellsUsed / result.cellsTotal;765 result.ratioPercent = (result.ratio*100).toFixed(2)+"%";766 }767 result.timeTotal = table.content[0][table.content[0].length-1] - table.content[0][0];768 return result;769 }770}771// module.exports = TimeTable;...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1const test = require('unexpected')2 .clone()3 .use(require('unexpected-sinon'));4const sinon = require('sinon');5test('test', () => {6 const spy = sinon.spy();7 spy();8 test.expect(spy, 'was called once');9});10test.run();11const test = require('unexpected')12 .clone()13 .use(require('unexpected-sinon'))14 .forwardFlags({ test: ['timeout'] });15test('test', () => {16 const spy = sinon.spy();17 spy();18 test.expect(spy, 'was called once');19});20test.run();21const test = require('unexpected')22 .clone()23 .use(require('unexpected-sinon'))24 .forwardFlags({ test: ['timeout'] });25test('test', () => {26 const spy = sinon.spy();27 spy();28 test.expect(spy, 'was called once');29});30test.run();

Full Screen

Using AI Code Generation

copy

Full Screen

1var expect = require('unexpected');2var unexpected = require('unexpected');3var unexpected = require('unexpected');4var unexpected = require('unexpected');5var unexpected = require('unexpected');6var expect = require('unexpected');7var unexpected = require('unexpected');8var unexpected = require('unexpected');9var unexpected = require('unexpected');10var unexpected = require('unexpected');11var expect = require('unexpected');12var unexpected = require('unexpected');13var unexpected = require('unexpected');14var unexpected = require('unexpected');15var unexpected = require('unexpected');16var expect = require('unexpected');17var unexpected = require('unexpected');18var unexpected = require('unexpected');19var unexpected = require('unexpected');20var unexpected = require('unexpected');21var expect = require('unexpected');22var unexpected = require('unexpected');23var unexpected = require('unexpected');24var unexpected = require('unexpected');25var unexpected = require('unexpected');26var expect = require('unexpected');27var unexpected = require('unexpected');28var unexpected = require('unexpected');29var unexpected = require('unexpected');30var unexpected = require('unexpected');31var expect = require('unexpected');32var unexpected = require('unexpected');33var unexpected = require('unexpected');34var unexpected = require('unexpected');35var unexpected = require('unexpected');36var expect = require('unexpected');37var unexpected = require('unexpected');38var unexpected = require('unexpected');39var unexpected = require('unexpected');40var unexpected = require('unexpected');41var expect = require('unexpected');42var unexpected = require('unexpected');43var unexpected = require('unexpected');44var unexpected = require('unexpected');45var unexpected = require('unexpected');46var expect = require('unexpected');47var unexpected = require('unexpected');48var unexpected = require('unexpected');49var unexpected = require('unexpected');50var unexpected = require('unexpected');51var expect = require('unexpected');52var unexpected = require('unexpected');53var unexpected = require('unexpected');54var unexpected = require('unexpected');55var unexpected = require('unexpected');

Full Screen

Using AI Code Generation

copy

Full Screen

1const unexpected = require('unexpected');2const unexpectedReact = require('unexpected-react');3const expect = unexpected.clone().use(unexpectedReact);4const { forwardFlags } = expect;5const React = require('react');6const { render } = require('react-dom');7const { renderToStaticMarkup } = require('react-dom/server');8const MyComponent = ({ children }) => <div>{children}</div>;9const MyComponentWithProps = ({ children, ...props }) => (10 <div {...props}>{children}</div>11);12const MyComponentWithPropsAndChildren = ({ children, ...props }) => (13 <div {...props}>14 {children}15);16const MyComponentWithPropsAndChildrenAndClassName = ({17}) => (18 <div {...props}>19 {children}20);21const MyComponentWithPropsAndChildrenAndClassNameAndId = ({22}) => (23 <div {...props}>24 {children}25);26const MyComponentWithPropsAndChildrenAndClassNameAndIdAndStyle = ({27}) => (28 <div {...props}>29 {children}30);31const MyComponentWithPropsAndChildrenAndClassNameAndIdAndStyleAndOnClick = ({32}) => (33 <div {...props}>34 {children}35);36const MyComponentWithPropsAndChildrenAndClassNameAndIdAndStyleAndOnClickAndOnMouseOver = ({37}) => (38 <div {...props}>39 {children}40);41const MyComponentWithPropsAndChildrenAndClassNameAndIdAndStyleAndOnClickAndOnMouseOverAndKey = ({42}) => (43 <div {...props}>44 {children}45);46const MyComponentWithPropsAndChildrenAndClassNameAndIdAndStyleAndOnClickAndOnMouseOverAndKeyAndRef = ({47}) => (48 <div {...props}>

Full Screen

Using AI Code Generation

copy

Full Screen

1var htmllike = require('unexpected-htmllike');2var expect = require('unexpected').clone();3expect.use(htmllike);4expect(5 {6 }7);8expect(9 {10 },11 {12 }13);14expect(15 {16 },17 {18 },19 {20 }21);22module.exports = function (expect) {23 expect.addAssertion('<string> to have attributes <object> [with flags] <object> [with flags] <object>', function (expect, subject, value, flags1, flags2) {24 });25};26module.exports = function (expect) {27 expect.addAssertion('<string> to have attributes <object>', function (expect, subject, value) {28 });29};30module.exports = function (expect) {31 expect.addAssertion('<string> to have attributes <object> [with flags] <object>', function (expect, subject, value, flags) {32 });33};34module.exports = function (expect) {35 expect.addAssertion('<string> to have attributes <object> [with flags] <object> [with flags] <object>', function (expect, subject, value, flags1, flags2) {36 });37};38module.exports = function (expect) {39 expect.addAssertion('<string> to have attributes <object> [with flags] <object> [with flags] <object>', function (expect, subject, value, flags1, flags2) {40 });41};42module.exports = function (expect

Full Screen

Using AI Code Generation

copy

Full Screen

1const unexpected = require('unexpected');2const unexpectedCli = require('unexpected-cli');3const unexpectedReact = require('unexpected-react');4const expect = unexpected.clone()5 .use(unexpectedCli())6 .use(unexpectedReact);7module.exports = expect;8const expect = require('./test');9describe('test', function () {10 it('should pass', function () {11 expect(12 );13 });14});15{16 "scripts": {17 }18}19 1 passing (9ms)20 0 passing (9ms)21 at Context.it (test.js:18:7)

Full Screen

Using AI Code Generation

copy

Full Screen

1var unexpected = require('unexpected');2var expect = unexpected.clone();3expect.addAssertion('<string> to be a <string>', function (expect, subject, value) {4 console.log('Inside assertion');5 console.log('Subject: ' + subject);6 console.log('Value: ' + value);7 expect(subject, 'to equal', value);8});9expect('test', 'to be a', 'test');

Full Screen

Using AI Code Generation

copy

Full Screen

1const { forwardFlags } = require('unexpected');2forwardFlags();3expect = require('unexpected');4expect('hello', 'to equal', 'world');5const { forwardFlags } = require('unexpected');6forwardFlags();7expect = require('unexpected');8expect('hello', 'to equal', 'world');9const { forwardFlags } = require('unexpected');10forwardFlags();11expect = require('unexpected');12expect('hello', 'to equal', 'world');13const { forwardFlags } = require('unexpected');14forwardFlags();15expect = require('unexpected');16expect('hello', 'to equal', 'world');17const { forwardFlags } = require('unexpected');18forwardFlags();19expect = require('unexpected');20expect('hello', 'to equal', 'world');21const { forwardFlags } = require('unexpected');22forwardFlags();23expect = require('unexpected');24expect('hello', 'to equal', 'world');25const { forwardFlags } = require('unexpected');26forwardFlags();27expect = require('unexpected');28expect('hello', 'to equal', 'world');29const { forwardFlags } = require('unexpected');30forwardFlags();31expect = require('unexpected');32expect('hello', 'to equal', 'world');33const { forwardFlags } = require('unexpected');34forwardFlags();35expect = require('unexpected

Full Screen

Using AI Code Generation

copy

Full Screen

1const expect = require('unexpected');2const regex = /xyz/gi;3expect(regex, 'to have flags', 'gi');4const expect = require('unexpected');5const regex = /xyz/gi;6expect(regex, 'to have flags', ['g', 'i']);7const expect = require('unexpected');8const regex = /xyz/gi;9expect(regex, 'to have flags', 'g', 'i');10const expect = require('unexpected');11const regex = /xyz/gi;12expect(regex, 'to have flags', ['g', 'i', 'm']);13const expect = require('unexpected');14const regex = /xyz/gi;15expect(regex, 'to have flags', 'g', 'i', 'm');16const expect = require('unexpected');17const regex = /xyz/gi;18expect(regex, 'to have flags', 'g', 'i', 'm', 'u', 'y');19const expect = require('unexpected');20const regex = /xyz/gi;21expect(regex, 'to have flags', 'g', 'i', 'm', 'u', 'y', 's');22const expect = require('unexpected');23const regex = /xyz/gi;24expect(regex, 'to have flags', 'g', 'i', 'm', 'u', 'y', 's', 'z');25const expect = require('unexpected');

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run unexpected automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful