@testcomplete/excelhandler
Version:
Read & Parse the provided Excel to offer method to handle Column & data as table
932 lines (809 loc) • 34.5 kB
JavaScript
// Temp for dev (compatibility with system which has no console)
try {
clog = console.log;
} catch($err) {
clog = function(){};
}
// @TODO : Mettre à jour les @return (JSDOC)
/**
* Instantiates an enhanced Array, which works as Array with extra features
* to manipulates rows/cells.
*
* Callback System (Inspired from SAP Enhancement Concept) :
* - Core.add() :
* - pre, receive {Arguments} func params, must return {Arguments}.
* - push, receive {String|Array} one argument value, must return {String|Array}.
* - post, receive {Array} on game data, must return {Array}.
*
*
* @param {Array} $fields Field list of the new table.
* @param {Array} $keys Field from field list which will compose the unique key.
* @param {Array} $array 2D Table data.
*
* @return {Array}
*
* @constructor
*/
function TableJs($fields, $keys, $array) {
// -----------------------------------------------------
// Master Data
// -----------------------------------------------------
/**
* @type {TableJs} Current TableJS Instance.
*/
let self = this;
/**
* @type {Array} Array which contains data. Master Array
* @private
*/
self._data = [];
// -----------------------------------------------------
// Internal Data
// -----------------------------------------------------
/**
* @type {null|TableJs} Next to a selection, a new TableJs is created and
* parent TableJs is link in this property.
* @private
*/
self._parent = null;
/**
* @type {Array} List of defined fields.
* @private
*/
self._fields = [];
/**
* @type {Array} List of defined fields which compose the key.
* @private
*/
self._keys = [];
/**
* Indexes for performances
*
* @type {{
* nextId: number, Each row must have a unique ID
* deprecated: boolean, Flag indicating indexes are deprecated
* byId: {Object}, Return row index using row ID
* byKeys: {Object}, Return row index using row key (composition of fields)
* byFields: {Object} Return rows indexes for a value for a field
* }}
* @private
*/
self._indexes = {
deprecated: true,
nextId: 0,
byId: {},
byKeys: {},
byFields: {}
};
/**
* Enhance Master Array for chaining features while keeping manipulating Array
*/
Object.defineProperties(self._data, {
/**
* Keeping TableJs reference in Array
*/
instance: {
enumerable: false,
writable: false,
value: self
},
/**
* Rewrite native Push method to add table row instead of unique value.
*
* @return {Number} return the next index number (as original one).
*/
push: {
enumerable: false,
writable: false,
value: function () {
let nextIndex = this.length;
for (let a in arguments) {
if(!arguments.hasOwnProperty(a)) continue;
let argv = arguments[a];
if (!(Object.prototype.toString.call(argv) === "[object Array]")) {
argv = [argv];
}
let row = this.data().consolidate(argv);
// Re-indexing next to push
this.core().indexing(row, nextIndex);
this[nextIndex] = row;
nextIndex++;
}
return nextIndex;
}
},
/**
* Make a full new copy of the table (no link between rows)
*
* @return {TableJs}
*/
copy: {
enumerable: false,
writable: false,
value: function () {
let copy = [];
for (let i in this) {
let row = this[i];
let newRow = [];
for (let f in row) {
newRow.push(row[f]);
}
copy.push(newRow)
}
// Note : do not attach parent (core().setParent()).
// The copy must fully detached.
return new TableJs(
self._fields,
self._keys,
copy
);
}
},
/**
* Update field(s) of selection with provided values
*
* @param {Object} $newFieldValues Each properties must be the field with value
*
* @return {Array}
*/
update: {
enumerable: false,
writable: false,
value: function ($newFieldValues) {
for (let field in $newFieldValues) {
if(!$newFieldValues.hasOwnProperty(field)) continue;
// Check if function exist (to prevent issues for wrong property in update)
if (!this.hasOwnProperty(field)) continue;
// Make this update
this.forEach(function ($row) {
$row[field]($newFieldValues[field]);
});
}
return self._data;
}
},
/**
* Delete rows of the table. Purposed for sub TableJs.
*
* @return {Array}
*/
delete: {
enumerable: false,
writable: false,
value: function () {
// The order is very important,
// Because on splice, indexes are updated.
// We have to process in reversed order.
// We have to delete row at same time
// Get RowId. Method deleteRows is in charge to delete them in
// the appropriate order
let rowsId = [];
this.forEach(function ($row) {
rowsId.push($row.id);
});
this.core().deleteRows(rowsId);
// Next to the deletion,
// We have to update indexes
self._data.core().indexing();
// Parent are deprecated
self._data.core().setDeprecated(true);
return self._data;
}
},
/**
* WIP ?
*/
insert: {
enumerable: false,
writable: false,
value: function () {
return self._data;
}
},
/**
* Create a new table row with appropriates fields and methods
*
* @param [Array|String] [Optional] Data for the new row.
*
* @return [Array] The TableJs Row with all generated methods.
*/
new: {
enumerable: false,
writable: false,
value: function ($aRowData = []) {
// Create a new empty row, then push it in the table.
// Return the created row
return this[this.push($aRowData) - 1];
}
},
/**
* Return indexes table. Not purpose for handling but to take acknowledge
* about data.
*
* @return {object}
*/
indexes: {
enumerable: false,
writable: false,
value: function () {
return self._indexes;
}
},
/**
* Fields, Keys & Data have the same working process.
* - Pooling by using Core
* - Specialisation using callbacks
*
* @return ...
*/
core: {
enumerable: false,
writable: false,
value: function () {
// For better usage, each variant can be called directly
// Call directly, return instance.
// Call with empty parameter will return sub-methods
if (arguments.length > 0) {
return self._data.core.apply(this).set.apply(this, arguments);
}
// Return basic method + those for call (fields, keys or data)
let returning = Object.assign({
/**
* Flush & Set provided arguments values in bound variable name (this.data).
*
* @return {TableJs}
*/
set: function () {
let data = self[`_${this.data}`];
// Refresh data
data = [];
// Add Data
return self._data.core.apply(this).add.apply(this, arguments);
},
/**
* Add to the end arguments values in bound variable name (this.data).
*
* @return {TableJs}
*/
add: function () {
let data = self[`_${this.data}`];
// Pre processor for arguments
if (this.callbacks && this.callbacks.add && this.callbacks.add.pre) {
arguments = this.callbacks.add.pre.apply(this, arguments);
}
// Process arguments
for (let a = 0; a < arguments.length; a++) {
let argv = arguments[a];
// Argument value is an Array
if (Object.prototype.toString.call(argv) === "[object Array]") {
argv.forEach(function ($value) {
if (data.lastIndexOf($value) < 0) {
if (this.callbacks && this.callbacks.add && this.callbacks.add.push) {
$value = this.callbacks.add.push.call(this, $value);
}
data.push($value);
}
}.bind(this));
}
// Argument value is a String
if (typeof argv === 'string') {
if (data.lastIndexOf(argv) < 0) {
if (this.callbacks && this.callbacks.add && this.callbacks.add.push) {
argv = this.callbacks.add.push.call(this, argv);
}
data.push(argv);
}
} else {
// What can we do for other type ?
//
// Note : TestComplete not able to see arguments
// passed directly with bracket as an instance of Array
// For unknown, try as object which is an table
try {
argv.forEach(function ($value) {
if (data.lastIndexOf($value) < 0) {
if (this.callbacks && this.callbacks.add && this.callbacks.add.push) {
$value = this.callbacks.add.push.call(this, $value);
}
data.push($value);
}
}.bind(this));
} catch ($err) {
}
}
}
// Post Processing
if (this.callbacks && this.callbacks.add && this.callbacks.add.post) {
data = this.callbacks.add.post.call(this, data);
}
// Indexing Data ---> Made by Push method
// self._data.core.apply(this).indexing();
return self._data;
},
/**
* Return values stored in bounded variable name (this.data).
*
* @return {Array}
*/
get: function () {
return self[`_${this.data}`];
},
/**
* Read data an creates index to easily return values.
*/
indexing: function ($row, $index) {
let sourceData = null;
// When row is provided, only index the provided row
if ($row !== undefined) {
sourceData = [$row];
}
// Indexing game data
else {
// Flush Indexes
self._indexes.byId = {};
self._indexes.byKeys = {};
self._indexes.byFields = {};
sourceData = self._data;
// Index in now refreshed
self._indexes.deprecated = false;
}
// Reading Data
for (let r = 0; r < sourceData.length; r++) {
let row = sourceData[r];
let rowId = row.id;
// Read Row Id and store its index
self._indexes.byId[rowId] = ($index === undefined) ? r : $index;
// Reading for fields
for (let f = 0; f < self._fields.length; f++) {
let field = self._fields[f];
let value = row[f];
if (!self._indexes.byFields[field]) self._indexes.byFields[field] = {};
if (!self._indexes.byFields[field][value]) self._indexes.byFields[field][value] = [];
self._indexes.byFields[field][value].push(($index === undefined) ? r : $index);
}
// Reading Key
if (self._keys.length > 0) {
let rowKey = "";
let keyFields = [];
// Get fields with their index to have
// a unique key (according to fields definition)
// regardless the order of the key definition.
for (let k = 0; k < self._keys.length; k++) {
let key = self._keys[k];
let keyFieldIndex = self._fields.lastIndexOf(key);
keyFields[keyFieldIndex] = key;
}
// Making the unique key
for (let i in keyFields) {
if(!keyFields.hasOwnProperty(i)) continue;
rowKey += String(row[i]);
}
// Check if the key is not already defined
if (self._indexes.byKeys.hasOwnProperty(rowKey)) {
let message = `Error : The generated key '${rowKey}' already exist for the row index ${self._indexes.byKeys[rowKey]}\n`;
message += `You should add some fields as key component or check your data.\n`;
message += `Your selected fields for the unique keys are : \n`;
self._keys.map(function($field){
message += ` • ${$field}\n`;
});
throw message ;
} else {
self._indexes.byKeys[rowKey] = ($index === undefined) ? r : $index;
}
}
}
},
/**
* Indicates the index table is deprecated.
* Reflection for all parents TableJs.
*
* @return {boolean}
*/
setDeprecated: function ($parentOnly = false) {
// In some case, we only want to deprecated
// index of parent(s)
if (!$parentOnly) {
self._indexes.deprecated = true;
}
if (self._parent) {
self._parent._data.core().setDeprecated();
}
return true;
},
/**
* Set in current table the parent table for communication.
*
* @param {TableJs} $parent
*
* @return {[]}
*/
setParent: function ($parent) {
self._parent = $parent;
self._indexes.nextId = $parent._indexes.nextId;
return self._data;
},
/**
* Increment for all TableJs the self._indexes.nextId
* when a new row is added.
*/
incrementNextId: function () {
self._indexes.nextId++;
if (self._parent) {
self._parent._data.core().incrementNextId();
}
},
/**
* Return rows where the cells respond to the requested values.
*
* @param {String} arguments[0] Fields to control.
* @param {Arguments} arguments[1] List of value to retrieve.
*
* @return {Array} Table with corresponding rows.
*/
values: function () {
let field = arguments[0];
let requestedValues = arguments[1];
let values = [];
let data = [];
// Process All argument (except first which is field)
for (let a = 0; a < requestedValues.length; a++) {
let forValue = requestedValues[a];
// For common processing, transform string to array
if (!(Object.prototype.toString.call(forValue) === "[object Array]")) {
forValue = [forValue];
}
// Manage duplicated values
forValue.forEach(function ($value) {
if(values.lastIndexOf($value) < 0) values.push($value);
});
}
// Retrieve rows for provided values
if (values.length > 0) {
// If index is flagged as deprecated,
// perform a full reindex to get right values
if (self._indexes.deprecated) {
this.core().indexing();
}
values.forEach(function ($value) {
if (self._indexes.byFields[field][$value]) {
self._indexes.byFields[field][$value].forEach(function ($index) {
if (self._data[$index]) data.push(self._data[$index]);
});
}
});
} else {
let distinct = [];
for(let value in self._indexes.byFields[field]) {
distinct.push(value);
}
return distinct;
}
// Result is a part of table, so create a new TableJs
// to get all features
return new TableJs(
self._fields,
self._keys,
data
).core().setParent(self);
},
/**
* Set and/or Return the value of the corresponding field.
*
* @return {*} Value of the field
*/
value: function ($field, $row, $value) {
let fieldIndex = self._fields.lastIndexOf($field);
// If a value is set, that implies we want to set
// a new value
if ($value !== undefined) {
// Update Locally
$row[fieldIndex] = $value;
// Set Index deprecated
self._data.core().setDeprecated();
}
// In any case, return the current value.
return $row[fieldIndex];
},
/**
* Delete definitely the provided row (reflected in all parents).
*
* @param {Array} $rowsId List of Row Id to delete.
*/
deleteRows: function ($rowsId) {
// Retrieve indexes for rows ID
let indexes = {};
$rowsId.forEach(function ($id) {
indexes[self._indexes.byId[$id]] = $id;
});
let reversed = [];
for (let idx in indexes) {
reversed.push(parseInt(idx));
}
reversed.reverse().forEach(function ($index) {
self._data.splice($index, 1);
});
// Delete in all parents
if (self._parent) {
self._parent._data.core().deleteRows($rowsId);
}
}
}, this.returns);
// Create methods to get rows from fields
let coreContext = this;
self._fields.forEach(function ($field) {
returning[$field] = function () {
return self._data.core.apply(this).values.call(this, $field, arguments);
}.bind(coreContext);
});
// To standardize & for extended functions,
// Make function wrapper to bind "this".
for (let fName in returning) {
if (!returning.hasOwnProperty(fName)) continue;
let fn = returning[fName];
returning[fName] = function () {
return fn.apply(this, arguments);
}.bind(this);
}
return returning;
}
},
/**
* Field Manager.
*
* @return ...
*/
fields: {
enumerable: false,
writable: false,
value: function () {
let functions = {
/**
* Define new property which is a function where the name
* is the provided field name.
*
* Purpose : Core().add()/push callback point.
*
* @param {String} $field
*
* @return {*}
*/
createMethod: function ($field) {
Object.defineProperty(self._data, $field, {
enumerable: false,
writable: false,
value: function () {
return self._data.core.apply(this).values.call(this, $field, arguments);
}
});
return $field;
},
/**
* Process all existing entries to consolidate rows according
* to the current field list.
*
* Purpose : Core().add()/post callback point.
*
* @param {Array} $fields In its context, receive field list
*
*/
consolidateAll: function ($fields) {
if (self._data.length > 0) {
self._data = self._data.map(function ($row) {
return self._data.data().consolidate($row);
});
}
return $fields;
}
};
let extended = {
data: 'fields',
callbacks: {
add: {
push: functions.createMethod,
post: functions.consolidateAll
}
},
returns: functions
};
return self._data.core.apply(extended, arguments);
}
},
/**
* Keys Manager.
*
* @return ...
*/
keys: {
enumerable: false,
writable: false,
value: function () {
let functions = {
/**
* Before pushing the requested field as key, check if the field
* is defined.
*
* Purpose : Core().add()/push callback point.
*
* @param {String} $field
*
* @return {String} $field
*/
isFieldExist: function ($field) {
if (self._fields.lastIndexOf($field) < 0) {
let field = $field;
if(field === '') field = '<empty>';
if(field === null) field = '<null>';
if(field === 'undefined') field = '<undefined>';
let message = `The requested field '${field}' as key component does not exist`;
throw message;
}
return $field;
},
/**
* Once fields are added as key component, trigger a new
* indexing process to update byKey index.
*
* Purpose : Core().add()/post callback point.
*
* @param {Array} $keys
*
* @return {Array} $keys
*/
triggerIndexing: function ($keys) {
self._data.core().indexing();
return $keys;
}
};
let extended = {
data: 'keys',
callbacks: {
add: {
push: functions.isFieldExist,
post: functions.triggerIndexing
}
},
returns: functions
};
return self._data.core.apply(extended, arguments);
}
},
/**
* Data Manager.
*
* @return ...
*/
data: {
enumerable: false,
writable: false,
value: function () {
let functions = {
/**
* Add to the end values in the table data.
* - String value stands for one line
* - Array of string stands for one line
* - Array of Array stands for data matrix
* All here before type can be mixted at once
*
* @return ...
*/
append: function () {
return self._data.core.apply(extended).add.apply(extended, arguments);
},
/**
* Wraps array of string into an array to become data matrix.
*
* Purpose : Core().add()/pre callback point.
*
* @return {IArguments}
*/
preprocArgs: function () {
for (let i in arguments) {
if(!arguments.hasOwnProperty(i)) continue;
let argv = arguments[i];
if (Object.prototype.toString.call(argv) === "[object Array]") {
if (argv.length > 0) {
if (typeof argv[0] === 'string') {
arguments[i] = [argv];
}
}
}
}
return arguments;
},
/**
* Complete row according to number of set fields.
*
* @param {Array} $row Data matrix row to process
*
* @return {Array} Consolidated row
*/
consolidate: function ($row) {
if (typeof $row === 'string') {
$row = [$row];
}
let fieldsNumber = self._fields.length;
let rowNumber = $row.length;
let missingCell = fieldsNumber - rowNumber;
if (missingCell > 0) {
for (let i = 0; i < missingCell; i++) {
$row.push('');
}
}
// Set a unique Row Id for internal use
let id = $row.id;
if (id === undefined) {
id = self._indexes.nextId;
self._data.core().incrementNextId();
}
// Set a unique Id for the row to identify them
// easily internally.
Object.defineProperty($row, 'id', {
enumerable: false,
writable: false,
configurable: true,
value: id
});
// Create 'field method' to return/set field value
// for the current row.
self._fields.forEach(function ($field) {
Object.defineProperty($row, $field, {
enumerable: false,
writable: false,
configurable: true,
value: function ($value) {
return self._data.core.apply(this).value.call(this, $field, $row, $value);
}
});
});
return $row;
},
/**
* Return a table row using it index.
*
* @param {Number} $index Row index starting from 0.
*
* @return {Array} Table row.
*/
getRow: function ($index = 0) {
if (typeof $index !== 'number') $index = 0;
return self._data[$index];
}
};
let extended = {
data: 'data',
callbacks: {
add: {
pre: functions.preprocArgs,
// push: functions.consolidate //---> Best integrated in Array push rewrited method
}
},
returns: functions
};
return self._data.core.apply(extended, arguments);
}
}
});
/** ------------------------------------------------------------------
* Internal Processing
* -------------------------------------------------------------------
*/
// Setting Up Fields
if ($fields !== undefined) {
self._data.fields($fields);
}
// Setting Up Keys
if ($keys !== undefined) {
self._data.keys($keys);
}
// Setting Up Data
if ($array !== undefined) {
self._data.data($array);
}
// Return enhanced Array
return self._data;
}
try {
module.exports = TableJs;
} catch ($err) {
// Not in NodeJs or require.js not available
}