recordarray
Version:
Array Class Extension for Records
575 lines (498 loc) • 18 kB
JavaScript
const defaultOptions = {};
Object.freeze(defaultOptions);
const defaultRecord = {};
Object.freeze(defaultRecord);
/** RecordArray
*
* @description: An extension of Array that provides record processing related methods
* @author Francis Carelse
* @version 0.0.11
*/
class RecordArray extends Array{
/**
* @constructor
* @param {Array} array (optional)
* @param {Object} options (optional)
*/
constructor(array = [], options = defaultOptions) {
super();
if(!(options instanceof Object)) options = defaultOptions;
if(options.data instanceof Array) array = options.data;
if(options.key) array.map(field=>({[options.key]:field}));
if(!(array instanceof Array)) array = [];
array.forEach(record => this.push(Object.assign({}, record)));
}
static defaultOptions = defaultOptions;
static defaultRecord = defaultRecord;
}
// If you change this then remember to freeze as it should not change across your application.
RecordArray.recordClass = Object;
RecordArray.new = function(array){
return new RecordArray(array);
};
RecordArray.prototype.asyncEach = async function(cb){
for(let i=0;i<this.length;i++){
await cb(this[i], i);
}
};
try{ // Export for browser environment
if(window instanceof Object)
window.RecordArray = RecordArray;
} catch(e){} // Not in browser environment
try{ // Export for commonJS environment
if(module instanceof Object && module.exports instanceof Object)
module.exports = RecordArray;
} catch(e){} // Not in commonJS environment
// // Extend the Array class via old method.
// RecordArray.prototype = Object.create(Array.prototype);
// RecordArray.prototype.constructor = RecordArray;
/**
* Find all by specified field equal to value
* @param field string: Key to use for searching records
* @param value any: Value to search for
* @param options object (optional): Additional parameters for the find operation
* options parameter can be boolean and will be used for the strict option
*/
RecordArray.prototype.findBy = function(field, value, options) {
// Create a RecordArray to be returned
let arr = new RecordArray();
options = options instanceof Object?
options:
RecordArray.defaultOptions;
// If no parameters then return empty RecordArray.
if(value === undefined){
if(options.returnIndex)
return -1;
else if(options.returnFirst){
return this.findByID(0,options) ||
this.findByTag('',options) ||
(
options.def !== undefined?
options.def:
RecordArray.defaultRecord
);
} else
return arr;
}
// Test field is not string primitive or string object then return.
if(typeof field != 'string' && !(field instanceof String)){
if(options.returnIndex)
return -1;
else if(options.returnFirst){
return this.findByID(0,options) ||
this.findByTag('',options) ||
(
options.def !== undefined?
options.def:
RecordArray.defaultRecord
);
} else
return arr;
}
// Ensure there is an options object
if(!(options instanceof Object) || options == null){
// Check if boolean to become the strict option
if(options instanceof Boolean || typeof options == 'boolean')
// Convert options to object with boolean value as strict option.
options = {strict: options};
else
// Set options to new basic parameters object
options = {};
}
// Force strict option to boolean
options.strict = !!options.strict;
// Force strict option to boolean
options.nth = options.nth || 1;
// If value not defined then just return the empty array
if (value === undefined) return arr;
// If null or undefined value to search for then enforce strict equality
if (value === null) options.strict = true;
// Go through all records
for(let i=0;i<this.length;i++){
const record = this[i];
// Find a matching field
field = Object.keys(record).filter(key=>
// Check the trim option
options.trim?
// Compare field with trimmed key
key.trim()==field:
// Otherwise compare field with key
key==field
)[0];
const compared = options.trim?
record[field].toString().trim():
record[field];
if(
// field should not be undefined
field !== undefined &&
// stored value is not undefined
compared !== undefined &&
// and apply strictness in comparison as per option between stored value and matching value
((!options.strict && compared == value) || Object.is(compared, value))
// Then append record to return RecordArray
){
if(!--options.nth){
if(options.returnIndex)
return i;
else if(options.returnFirst)
return record;
}
arr.push(record);
}
}
// Return resultant RecordArray or unfound return value.
if(options.returnIndex)
return -1;
else if(options.returnFirst)
return options.def !== undefined?
options.def:
{};
else
return arr;
}
RecordArray.prototype.findByID = function(value, options) {
return this.findBy("id", value, options);
}
RecordArray.prototype.findByTag = function(value, options) {
return this.findBy("tag", value, options);
}
RecordArray.prototype.findOne = function(key, value, options) {
return this.findBy(key, value, {...options, returnFirst: true});
}
RecordArray.prototype.findOneByID = function(value, options) {
return this.findBy('id', value, {...options, returnFirst: true});
}
RecordArray.prototype.findOneByTag = function(value, options) {
return this.findBy('tag', value, {...options, returnFirst: true});
}
RecordArray.prototype.indexBy = function(field, value, options) {
return this.findBy(field, value, {...options, returnIndex: true});
}
RecordArray.prototype.indexByID = function(value, options) {
return this.indexBy("id", value, options);
}
RecordArray.prototype.indexByTag = function(value, options) {
return this.indexBy("tag", value, options);
}
RecordArray.prototype.matchBy = function(key, values){
var arr = new RecordArray();
// Undefined values means no matches.
if(values === undefined) return arr;
// ensure values is an array. Insert into new array and assign if need be.
if(!(values instanceof Array)) values = [values];
// flatten values array;
values = [].concat(values);
for(var i = 0; i < values.length; i++)
arr[i] = this.findOne(key, values[i]);
return arr;
}
RecordArray.prototype.sortBy = function(field, order) {
// Assert field parameter is a string.
if (typeof field !== "string")
throw new TypeError("String expected for first parameter.");
// Assert order parameter is ASC or DESC
if (
typeof order !== "string" ||
!(order.toLowerCase() === "asc" || order.toLowerCase() === "desc")
)
throw new TypeError(
"'ASC' or 'DESC' String expected for second parameter."
);
// Return sorted using appropriate function
return this.sort(order.toLowerCase() == "asc" ? sortFnASC : sortFnDESC);
// Sorting Ascending Strategy
function sortFnASC(a, b) {
var c =
// Evaluate to 0 f equal
a[field] == b[field]?
0:
// 1 indicates wrong order. -1 indicates correct order
a[field] > b[field]?
1:
-1;
// This does not work if you do not assign to a variable before returning.
return c;
}
// Sorting Descending Strategy (just reverse the testing parameters)
function sortFnDESC(a, b) {
return sortFnASC(b, a);
}
}
/**
* Sort this RecordArray by a set of fields in ascending order
* Takes an array of strings or a space separated string of fieldnames
* @param {Array<String> | String} fields
*/
RecordArray.prototype.sortASC = function(fields) {
// If fields parameter is not already an Array
if (!(fields instanceof Array))
// Ensure is string and split space separated fieldnames
fields = fields.toString().split(" ");
// Throw out any non string fields
fields = fields.filter(f => typeof f === "string");
// If no fields left then abort
if (!fields.length)
throw new TypeError(
'Parameter "fields" needs to be an array of strings or space separated list of field names'
);
// Return sort using item pair evaluation strategy
return this.sort(function(a, b) {
// Iterate over fields list
for (var i = 0; i < fields.length; i++)
// Sequentially check for the first instance of inequality
if (a[fields[i]] != b[fields[i]])
// If wrong order then pass back 1 otherwise -1
return a[fields[i]] > b[fields[i]] ? 1 : -1;
// All fields are equal so return 0 for matching
return 0;
});
}
/**
* Sort this RecordArray by a set of fields in descending order
* Takes an array of strings or a space separated string of fieldnames
* @param {Array<String> | String} fields
*/
RecordArray.prototype.sortDESC = function(fields) {
// If fields parameter is not already an Array
if (!(fields instanceof Array))
// Ensure is string and split space separated fieldnames
fields = fields.toString().split(" ");
// Throw out any non string fields
fields = fields.filter(f => typeof f === "string");
// If no fields left then abort
if (!fields.length)
throw new TypeError(
'Parameter "fields" needs to be an array of strings or space separated list of field names'
);
// Return sort using item pair evaluation strategy
return this.sort(function(a, b) {
// Iterate over fields list
for (var i = 0; i < fields.length; i++)
// Sequentially check for the first instance of inequality
if (a[fields[i]] != b[fields[i]])
// If wrong order then pass back 1 otherwise -1
return a[fields[i]] < b[fields[i]] ? 1 : -1;
// All fields are equal so return 0 for matching;
return 0;
});
}
/**
* Clone this RecordArray or supplied Array of reords to a new RecordArray
* @param {Array} arr
*/
RecordArray.prototype.clone = function(arr) {
// If no source array supplied then use this one
arr = arr || this;
// Create new RecordArray
var clone = new RecordArray();
for (var i = 0; i < arr.length; i++) clone.push(Object.assign({}, arr[i]));
return clone;
}
/**
* @returns 'Array of cloned records'
*/
RecordArray.prototype.toArray = function() {
// Clone to an Array
return this.map(record => Object.assign({}, record));
}
RecordArray.prototype.getName = function(id) {
var records = this.findBy("id", id);
if (records.length === 0) return false;
else if (records.length > 0) return records[0].name;
};
RecordArray.prototype.getNameByTag = function(tag) {
var records = this.findBy("tag", tag);
if (records.length === 0) return false;
else if (records.length > 0) return records[0].name;
};
/**
* List all values of a specified field
* @param field string: Key to use for searching records
* @param options object (optional): Additional parameters for the list operation
* options parameter can be boolean and will be used for the trim option
*/
RecordArray.prototype.listValues = function(field, options) {
// Test field is string primitive or string object.
if(typeof field != 'string' && !(field instanceof String))
throw new TypeError('Field Name parameter required');
// Create a RecordArray to be returned
var arr = [];
// Ensure there is an options object
if(!(options instanceof Object)){
// Check if boolean to become the strict option
if(options instanceof Boolean || typeof options == 'boolean')
// Convert options to object with boolean value as strict option.
options = {trim: options};
else
// Set options to new basic parameters object
options = {};
}
// Force strict option to boolean
options.trim = !!options.trim;
// Use index 'i' for all index values
for (var i = 0; i < this.length; i++){
// Evaluate the field dependant on the trim option
let field = options.trim?
// Trim and evaluate the field in the record indexed by 'i'
this[i][key].toString().trim():
// Simply evaluate the field in the record indexed by 'i'
this[i][key];
// stored value is not undefined
if (field !== undefined)
// Then append value to returned array
arr.push(this[i][field]);
}
// Return resultant RecordArray
return arr;
}
// TBD
RecordArray.prototype.create = function(options, filters) {
throw Error("Function yet to be developed");
}
RecordArray.prototype.read = function(options, filters) {
throw Error("Function yet to be developed");
}
RecordArray.prototype.update = function(options, filters) {
throw Error("Function yet to be developed");
}
RecordArray.prototype.delete = function(options, filters) {
throw Error("Function yet to be developed");
}
RecordArray.prototype.list = function(options, filters) {
throw Error("Function yet to be developed");
}
// faulty. Comparing objects at the moment not keys or value.
RecordArray.compareRecords = (record1, record2, strict)=>{
// Default "strict" to true
if(strict !== false) strict = true;
// Compare Keys
let keys1 = Object.keys(record1).sort();
let keys2 = Object.keys(record2).sort();
if(strict && keys1.length !== keys2.length) return false;
// Compare keys
if(!keys1.every((value, index) => value === keys2[index])) return false;
// Compare values
if(!keys1.every((key, index) => record1[key] === record2[key])) return false;
return true;
}
/*
//// IndexBy function now handled by findBy function with option flag returnIndex
// Find the index of a record using it's "id"
RecordArray.prototype.indexBy = function(field, value, strict) {
// Assert field not null
if (field == null) return;
// Assert field is a string
if (typeof field !== "string") throw new TypeError("Field must be a string");
// Assert field exists in the first record
if (this[field] === undefined)
throw new TypeError("First record does not have field");
// Iterate index "i" for this recordarray
for (let i = 0; i < this.length; i++)
// If parameter strict is truthy then enforce type comparison
if (this[i][field] == value && (!strict || this[i][field] === value))
// exit function returning id as soon as found
return i;
};
// Find the index of a record using it's "id"
RecordArray.prototype.indexByID = function(id, strict) {
if (id == null) throw new TypeError("ID cannot be null or undefined");
return RecordArray.prototype.call(this, "id", id, strict);
};
// Find the index of a record using it's "tag"
RecordArray.prototype.indexByTag = function(tag, strict) {
if (tag == null) throw new TypeError("Tag cannot be null or undefined");
return RecordArray.prototype.call(this, "tag", tag, strict);
};
//// IndexBy function now handled by findBy function with option flag returnIndex
*/
RecordArray.prototype.unique = function(field, strict) {
// Default field to 'id'
if (field == null) field = "id";
// Compare current index with index of first occurence of record with field with that value)
return this.filter((e, i) => this.indexBy(field, e[field], strict) == i);
};
RecordArray.prototype.uniqueBy = function(field, strict) {
// Assert field is a string
if (field == null || typeof field !== "string")
throw new TypeError("Field required");
// Compare current index with index of first occurence of record with field with that value)
return this.filter((e, i) => this.indexFrom(field, e[field], strict) == i);
};
RecordArray.prototype.uniqueIDs = function(strict){
return this.unique().listValues('id');
};
RecordArray.prototype.hasRecord = function(record){
if(!!record.id)
return !!this.findOneByID(record.id);
else if(!!record.tag)
return !!this.findOneByTag(record.tag);
else
return false;
}
/**
* Extend the RecordArray array by updating or creating based on matching ID
*/
RecordArray.prototype.extend = function(arr) {
arr.forEach(record=>{
if(this.hasRecord(record)) this.update(record);
else this.push(record);
});
return this.sortASC('id');
};
RecordArray.prototype.topID = function(){
// Iterate over this recordArray and reduce all IDs to the largest.
return this.reduce((record, topID)=>
topID>record.id?
topID:
record.id
, 0);
}
RecordArray.prototype.merge = function(arr) {
arr.forEach(r=>this.push(r));
return this;
};
/*
* @description: Comparing 2 RecordArrays
* @author: Francis Carelse
* @param RA1: RecordArray
* @param RA2: RecordArray
* @param strict: Boolean will enforce second RecordArray only has the same records
* @param identical: Boolean will enforce each record by index is compared
* @returns: Boolean true if equal
* @note:
*/
RecordArray.compare = (RA1, RA2, options) => {
// Assert RA1 is an Array
if (!(RA1 instanceof Array))
throw new TypeError("Parameter 1 must be Array or RecordArray");
// Assert RA2 is an Array
if (!(RA2 instanceof Array))
throw new TypeError("Parameter 2 must be Array or RecordArray");
// Ensure there is an options object
if(!(options instanceof Object)){
// Check if boolean to become the strict option
if(options instanceof Boolean || typeof options == 'boolean')
// Convert options to object with boolean value as strict option.
options = {strict: options};
else
// Set options to new basic parameters object
options = {};
}
// Force strict option to boolean
options.strict = !!options.strict;
// Force identical option to boolean
options.identical = !!options.identical;
// Compare Lengths of unique IDs.
if (options.strict && RA1.uniqueIDs().length !== RA2.uniqueIDs().length) return false;
// Compare records
if (options.identical) {
if ( !RA1.every( ( record, index) =>
RecordArray.compareRecords(record, RA2[index], options.strict)
)) return false;
} else {
if ( !RA1.every(record =>
RecordArray.compareRecords(record, RA2.findOne("id", record.id), options.strict)
)) return false;
}
return true;
};