devoir
Version:
Lightweight Javascript library adding functionality used in everyday tasks
560 lines (477 loc) • 14.2 kB
JavaScript
// (c) 2016 Wyatt Greenway
// This code is licensed under MIT license (see LICENSE.txt for details)
// Helper Functions and utilities
(function(factory) {
module.exports = function(_root, _base) {
var root = _root || {},
base = _base;
if (!base)
base = require('./base');
return factory(root, base);
};
})(function(root, D) {
'use strict';
/**
* @namespace {devoir}
* @namespace {data} Devoir Data Functions
*/
function sortBySubKey(array, negate) {
var args = arguments;
return array.sort(function(a, b) {
var x, y;
for (var i = 2; i < args.length; i++) {
var key = args[i];
var isPath = (key.indexOf('.') > -1);
if (typeof a === 'object')
x = (isPath) ? D.get(a, key) : a[key];
else
x = a;
if (typeof b === 'object')
y = (isPath) ? D.get(b, key) : b[key];
else
y = b;
if (y !== undefined && x !== undefined)
break;
}
if (y === undefined || y === null || !('' + y).match(/\S/))
y = -1;
if (x === undefined || x === null || !('' + x).match(/\S/))
x = 1;
var result = ((x < y) ? -1 : ((x > y) ? 1 : 0));
return (negate) ? (result * -1) : result;
});
}
root.sortBySubKey = sortBySubKey;
/**
* @function {extend} Extend (copy) objects into base object. This should ALWAYS be used instead of jQuery.extend
because it is faster, and more importantly because it WILL NOT mangle instantiated
sub-objects.
* @param {[flags]}
* @type {Object} Will be considered the first of <b>args</b>
* @type {Boolean} If true this is a deep copy
* @type {Number} This is a bitwise combination of flags. Flags include: devoir.data.extend.DEEP, devoir.data.extend.NO_OVERWRITE, and devoir.data.extend.FILTER. If the 'FILTER' flag is specified the 2nd argument is expected to be a function to assist in which keys to copy
* @end
* @param {[args...]}
* @type {Object} Objects to copy into base object. First object in the argument list is the base object.
* @type {Function} If the second argument is a function and the bitwise flag "FILTER" is set then this function will be the callback used to filter the keys of objects during copy
@param {String} {key} Key of object(s) being copied
@param {*} {value} Value being copied
@param {Object} {src} Parent Object/Array where value is being copied from
@param {*} {dstValue} Value of same key at destination, if any
@param {Object} {dst} Parent Object/Array where value is being copied to
@end
* @end
* @return {Object} Base object with all other objects merged into it
*/
function extend() {
if (arguments.length === 0)
return;
if (arguments.length === 1)
return arguments[0];
var isDeep = false;
var allowOverwrite = true;
var onlyMatching = false;
var filterFunc;
var startIndex = 0;
var dst = arguments[0];
if (typeof dst === 'boolean') {
isDeep = dst;
startIndex++;
} else if (typeof dst === 'number') {
isDeep = (dst & extend.DEEP);
allowOverwrite = !(dst & extend.NO_OVERWRITE);
startIndex++;
filterFunc = (dst & extend.FILTER) ? arguments[startIndex++] : undefined;
}
//Destination object
dst = arguments[startIndex++];
if (!dst)
dst = {};
var val;
if (isDeep) {
for (var i = startIndex, len = arguments.length; i < len; i++) {
var thisArg = arguments[i];
if (!(thisArg instanceof Object))
continue;
var keys = Object.keys(thisArg);
for (var j = 0, jLen = keys.length; j < jLen; j++) {
var key = keys[j];
if (allowOverwrite !== true && dst.hasOwnProperty(key))
continue;
val = thisArg[key];
var dstVal = dst[key];
if (filterFunc && filterFunc(key, val, thisArg, dstVal, dst) === false)
continue;
if (val && typeof val === 'object' && !(val instanceof String) && !(val instanceof Number) &&
(val.constructor === Object.prototype.constructor || val.constructor === Array.prototype.constructor)) {
var isArray = (val instanceof Array);
if (!dstVal)
dstVal = (isArray) ? [] : {};
val = extend(true, (isArray) ? [] : {}, dstVal, val);
}
dst[key] = val;
}
}
} else {
for (var i = startIndex, len = arguments.length; i < len; i++) {
var thisArg = arguments[i];
if (!(thisArg instanceof Object))
continue;
var keys = Object.keys(thisArg);
for (var j = 0, jLen = keys.length; j < jLen; j++) {
var key = keys[j];
if (allowOverwrite !== true && dst.hasOwnProperty(key))
continue;
val = thisArg[key];
if (filterFunc) {
var dstVal = dst[key];
if (filterFunc(key, val, thisArg, dstVal, dst) === false)
continue;
}
dst[key] = val;
}
}
}
if (dst._audit) {
var b = dst._audit.base;
b.modified = D.now();
b.updateCount++;
}
return dst;
}
root.extend = extend;
(function extend_const(base) {
base.DEEP = 0x01;
base.NO_OVERWRITE = 0x02;
base.FILTER = 0x04;
})(extend);
function matching(deep) {
function buildPathMap(pathMap, obj, path) {
for (var key in thisArg) {
if (!thisArg.hasOwnProperty(key))
continue;
if (!pathMap.hasOwnProperty(key)) {
pathMap[key] = 1;
continue;
}
pathMap[key] = pathMap[key] + 1;
}
}
var isDeep = false;
var startIndex = 1;
var dst = {};
var pathMap = {};
if (typeof deep === 'boolean') {
isDeep = deep;
startIndex = 1;
} else {
startIndex = 0;
}
for (var i = startIndex, len = arguments.length; i < len; i++) {
var thisArg = arguments[i];
buildPathMap(pathMap, thisArg);
}
var objCount = arguments.length - startIndex;
var lastObj = arguments[arguments.length - 1];
for (var key in pathMap) {
if (!pathMap.hasOwnProperty(key))
continue;
var val = pathMap[key];
if (val >= objCount)
dst[key] = lastObj[key];
}
return dst;
}
root.matching = matching;
/**
* @function {extract} Extracts elements from an Array of Objects (not parts from a String). This is not the same as @@@function:devoir.utils.extract
* @param {String} {key} Key to extract from all objects
* @param {Object} {[args...]} Array(s) to extract from
* @return {Object} Array of extracted properties. If the property wasn't found the array element will be 'undefined'.
* @see function:devoir.data.toLookup
* @example {javascript}
var myParts = D.data.extract('id', [
{
id:'derp',
field: 'derp'
},
{
id:'dog',
field: 'dog'
},
{
id:'cat',
field: 'cat'
}
], [
{
id:'another',
field: 'another'
},
{
id:'field',
field: 'field'
}
]);
myParts === ['derp','dog','cat','another','field'];
*/
function extract(key) {
var thisArray = [];
for (var i = 1, len = arguments.length; i < len; i++) {
var args = arguments[i];
if (!args)
continue;
for (var j in args) {
if (!args.hasOwnProperty(j))
continue;
var val = D.get(args[j], key);
thisArray.push(val);
}
}
return thisArray;
}
root.extract = extract;
/**
* @function {toLookup} This takes an Array and returns a reference map for quick lookup.
* @param {String} {key} Key to match on all objects. If key is undefined or null, the index will be used instead.
* @param {Array} {data} Array to create map from
* @return {Object} Each key in the object will be the value in the Array specified by 'key'
* @see function:devoir.data.extract
* @example {javascript}
var myMap = D.data.toLookup('id', [
{
id:'derp',
field: 'derp'
},
{
id:'dog',
field: 'dog'
},
{
id:'cat',
field: 'cat'
}
]);
myMap === {
'derp': {
id:'derp',
field: 'derp'
},
'dog': {
id:'dog',
field: 'dog'
},
'cat': {
id:'cat',
field: 'cat'
}
};
*/
function toLookup(key, data) {
if (!data)
return {};
var obj = {},
keys = Object.keys(data);
for (var i = 0, il = keys.length; i < il; i++) {
var id,
k = keys[i],
v = data[k];
if (key) {
id = D.get(v, key);
} else {
id = ('' + v);
}
if (!id)
continue;
obj[id] = v;
}
return obj;
}
root.toLookup = toLookup;
/**
* @function {mergeKeys} Merge/Facet keys of all objects passed in
* @param {Object} {[objects...]} Object(s) to gather unique keys from
* @return {Object} An object where each key is a key in at least one of the objects passed in.
The value for each key is the number of times that key was encountered.
* @example {javascript}
var obj1 = {hello:'world',derp:'cool'}, obj2 = {hello:'dude',cool:'beans'};
$mn.data.mergeKeys(obj1, obj2); //= {hello:2,derp:1,cool:1}
*/
function mergeKeys() {
var obj = {};
for (var i = 0, il = arguments.length; i < il; i++) {
var data = arguments[i];
var keys = Object.keys(data);
for (var j = 0, jl = keys.length; j < jl; j++) {
var k = keys[j];
if (!obj.hasOwnProperty(k))
obj[k] = 0;
obj[k]++;
}
}
return obj;
}
root.mergeKeys = mergeKeys;
/**
* @function {toArray} Convert Object(s) to an Array/Array of keys
* @param {[keys]}
* @type {Boolean} Specifies if the result will be an array of keys (true), or an array of objects (false)
* @type {Object} The first object to convert
* @end
* @param {Object} {[args...]} Objects to add to Array
* @return {Array} If *keys* is false or an Object, than all array elements will be the properties of the supplied object(s).
If *keys* is true, than all array elements will be the keys of all the properties of the supplied object(s).
* @see {devoir.data.extract} {devoir.data.toLookup}
* @example {javascript}
var myArray = D.data.toArray(
{
id:'derp',
field: 'test',
caption: 'Hello world!'
});
myArray === ['derp','test','Hello World!'];
myArray = myArray = D.data.toArray(true,
{
id:'derp',
field: 'test',
caption: 'Hello world!'
});
myArray === ['id','field','caption'];
*/
function toArray(keys) {
var startIndex = 1;
var thisKeys = keys;
if (!D.utils.dataTypeEquals(thisKeys, 'boolean')) {
thisKeys = false;
startIndex = 0;
}
var thisArray = [];
for (var i = startIndex, len = arguments.length; i < len; i++) {
var args = arguments[i];
if (!args)
continue;
for (var j in args) {
if (!args.hasOwnProperty(j))
continue;
if (thisKeys === true && !(args instanceof Array))
thisArray.push(j);
else
thisArray.push(args[j]);
}
}
return thisArray;
}
root.toArray = toArray;
/**
* @function {walk} Walk an object, calling a callback for each property. If callback returns **false** the walk will be aborted.
* @param {Object} {data} Object to walk
* @param {callback} Callback to be called for every property on the object tree
* @type {Function}
* @param {String} {thisPath} full key path in dot notation (i.e. 'property.child.name')
* @param {Object} {value} Current property value
* @param {String} {key} Key name of current property
* @param {Object} {data} Current property parent object
* @return {Boolean} **false** to abort walking
* @end
* @end
* @return {Boolean|undefined} **false** if walk was canceled (from callback). Undefined otherwise.
*/
function walk(data, callback, path, parentData, originalData, parentContext, depth, visitedIDMap) {
if (!(callback instanceof Function))
return;
if (!data)
return;
if (typeof data !== 'object')
return;
if (!visitedIDMap)
visitedIDMap = {};
if (originalData === undefined)
originalData = data;
if (path === undefined)
path = '';
if (!depth)
depth = 0;
if (depth > 64)
throw new Error('Maximum walk depth exceeded');
var context = {
parent: parentContext
};
for (var k in data) {
if (!data.hasOwnProperty(k)) continue;
var thisPath;
if (data instanceof Array)
thisPath = path + '[' + k + ']';
else {
thisPath = (path) ? [path, k].join('.') : k;
}
var v = data[k];
if (v !== null && typeof v === 'object') {
var objectID = D.id(v);
if (visitedIDMap[objectID]) //No infinite recursion
continue;
visitedIDMap[objectID] = v;
if (root.walk(v, callback, thisPath, data, originalData, context, depth + 1, visitedIDMap) === false)
return false;
}
context.depth = depth;
context.scope = data;
context.parentScope = parentData;
context.originalData = originalData;
context.fullPath = thisPath;
if (callback.apply(context, [thisPath, v, k, data, parentData, originalData, depth]) === false)
return false;
}
}
root.walk = walk;
/**
* @function {flatten} Flatten an object where the returned object's keys are full path notation, i.e:
{
my: {},
my.path: {},
my.path.to: {},
my.path.to.key: [],
my.path.to.key[0]: 'Derp',
my.path.to.key[1]: 'Hello'
}
* @param {Object} {obj} Object to flatten
* @param {Number} {[maxDepth]} If specified, don't traverse any deeper than this number of levels
* @return {Object} Flattened object
*/
function flatten(obj, maxDepth) {
var ref = {};
root.walk(obj, function(path, v, k) {
if (maxDepth && this.depth <= maxDepth)
ref[path] = v;
else if (!maxDepth)
ref[path] = v;
});
return ref;
}
root.flatten = flatten;
/**
* @function {ifAny} See if any of the elements in the provided array match the comparison arguments provided
* @param {Array|Object} {testArray} Array/Object to iterate
* @param {*} {[args...]} Any object/value to test with strict comparison against elements of "testArray" (testArray[0]===arg[1]||testArray[0]===arg[2]...)
* @return {Boolean} **true** if ANY elements match ANY provided arguments, **false** otherwise
*/
function ifAny(testArray) {
if (!testArray)
return false;
if (root.instanceOf(testArray, 'string', 'number', 'boolean', 'function'))
return false;
if (!(testArray instanceof Array) && !(testArray instanceof Object))
return false;
var keys = Object.keys(testArray);
for (var i = 0, il = keys.length; i < il; i++) {
var key = keys[i],
elem = testArray[key];
for (var j = 1, jl = arguments.length; j < jl; j++) {
if (elem === arguments[j])
return true;
}
}
return false;
}
root.ifAny = ifAny;
return root;
});