442 lines
14 KiB
JavaScript
442 lines
14 KiB
JavaScript
((typeof define === "function" && define.amd && function (m) { define("samsam", m); }) ||
|
|
(typeof module === "object" &&
|
|
function (m) { module.exports = m(); }) || // Node
|
|
function (m) { this.samsam = m(); } // Browser globals
|
|
)(function () {
|
|
var o = Object.prototype;
|
|
var div = typeof document !== "undefined" && document.createElement("div");
|
|
|
|
function isNaN(value) {
|
|
// Unlike global isNaN, this avoids type coercion
|
|
// typeof check avoids IE host object issues, hat tip to
|
|
// lodash
|
|
var val = value; // JsLint thinks value !== value is "weird"
|
|
return typeof value === "number" && value !== val;
|
|
}
|
|
|
|
function getClass(value) {
|
|
// Returns the internal [[Class]] by calling Object.prototype.toString
|
|
// with the provided value as this. Return value is a string, naming the
|
|
// internal class, e.g. "Array"
|
|
return o.toString.call(value).split(/[ \]]/)[1];
|
|
}
|
|
|
|
/**
|
|
* @name samsam.isArguments
|
|
* @param Object object
|
|
*
|
|
* Returns ``true`` if ``object`` is an ``arguments`` object,
|
|
* ``false`` otherwise.
|
|
*/
|
|
function isArguments(object) {
|
|
if (getClass(object) === 'Arguments') { return true; }
|
|
if (typeof object !== "object" || typeof object.length !== "number" ||
|
|
getClass(object) === "Array") {
|
|
return false;
|
|
}
|
|
if (typeof object.callee == "function") { return true; }
|
|
try {
|
|
object[object.length] = 6;
|
|
delete object[object.length];
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @name samsam.isElement
|
|
* @param Object object
|
|
*
|
|
* Returns ``true`` if ``object`` is a DOM element node. Unlike
|
|
* Underscore.js/lodash, this function will return ``false`` if ``object``
|
|
* is an *element-like* object, i.e. a regular object with a ``nodeType``
|
|
* property that holds the value ``1``.
|
|
*/
|
|
function isElement(object) {
|
|
if (!object || object.nodeType !== 1 || !div) { return false; }
|
|
try {
|
|
object.appendChild(div);
|
|
object.removeChild(div);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @name samsam.keys
|
|
* @param Object object
|
|
*
|
|
* Return an array of own property names.
|
|
*/
|
|
function keys(object) {
|
|
var ks = [], prop;
|
|
for (prop in object) {
|
|
if (o.hasOwnProperty.call(object, prop)) { ks.push(prop); }
|
|
}
|
|
return ks;
|
|
}
|
|
|
|
/**
|
|
* @name samsam.isDate
|
|
* @param Object value
|
|
*
|
|
* Returns true if the object is a ``Date``, or *date-like*. Duck typing
|
|
* of date objects work by checking that the object has a ``getTime``
|
|
* function whose return value equals the return value from the object's
|
|
* ``valueOf``.
|
|
*/
|
|
function isDate(value) {
|
|
return typeof value.getTime == "function" &&
|
|
value.getTime() == value.valueOf();
|
|
}
|
|
|
|
/**
|
|
* @name samsam.isNegZero
|
|
* @param Object value
|
|
*
|
|
* Returns ``true`` if ``value`` is ``-0``.
|
|
*/
|
|
function isNegZero(value) {
|
|
return value === 0 && 1 / value === -Infinity;
|
|
}
|
|
|
|
/**
|
|
* @name samsam.equal
|
|
* @param Object obj1
|
|
* @param Object obj2
|
|
*
|
|
* Returns ``true`` if two objects are strictly equal. Compared to
|
|
* ``===`` there are two exceptions:
|
|
*
|
|
* - NaN is considered equal to NaN
|
|
* - -0 and +0 are not considered equal
|
|
*/
|
|
function identical(obj1, obj2) {
|
|
if (obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))) {
|
|
return obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2);
|
|
}
|
|
}
|
|
|
|
function isSet(val) {
|
|
if (typeof Set !== 'undefined' && val instanceof Set) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function isSubset(s1, s2, compare) {
|
|
var values1 = Array.from(s1);
|
|
var values2 = Array.from(s2);
|
|
|
|
for (var i = 0; i < values1.length; i++) {
|
|
var includes = false;
|
|
|
|
for (var j = 0; j < values2.length; j++) {
|
|
if (compare(values2[j], values1[i])) {
|
|
includes = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!includes) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @name samsam.deepEqual
|
|
* @param Object obj1
|
|
* @param Object obj2
|
|
*
|
|
* Deep equal comparison. Two values are "deep equal" if:
|
|
*
|
|
* - They are equal, according to samsam.identical
|
|
* - They are both date objects representing the same time
|
|
* - They are both arrays containing elements that are all deepEqual
|
|
* - They are objects with the same set of properties, and each property
|
|
* in ``obj1`` is deepEqual to the corresponding property in ``obj2``
|
|
*
|
|
* Supports cyclic objects.
|
|
*/
|
|
function deepEqualCyclic(obj1, obj2) {
|
|
|
|
// used for cyclic comparison
|
|
// contain already visited objects
|
|
var objects1 = [],
|
|
objects2 = [],
|
|
// contain pathes (position in the object structure)
|
|
// of the already visited objects
|
|
// indexes same as in objects arrays
|
|
paths1 = [],
|
|
paths2 = [],
|
|
// contains combinations of already compared objects
|
|
// in the manner: { "$1['ref']$2['ref']": true }
|
|
compared = {};
|
|
|
|
/**
|
|
* used to check, if the value of a property is an object
|
|
* (cyclic logic is only needed for objects)
|
|
* only needed for cyclic logic
|
|
*/
|
|
function isObject(value) {
|
|
|
|
if (typeof value === 'object' && value !== null &&
|
|
!(value instanceof Boolean) &&
|
|
!(value instanceof Date) &&
|
|
!(value instanceof Number) &&
|
|
!(value instanceof RegExp) &&
|
|
!(value instanceof String)) {
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* returns the index of the given object in the
|
|
* given objects array, -1 if not contained
|
|
* only needed for cyclic logic
|
|
*/
|
|
function getIndex(objects, obj) {
|
|
|
|
var i;
|
|
for (i = 0; i < objects.length; i++) {
|
|
if (objects[i] === obj) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// does the recursion for the deep equal check
|
|
return (function deepEqual(obj1, obj2, path1, path2) {
|
|
var type1 = typeof obj1;
|
|
var type2 = typeof obj2;
|
|
|
|
// == null also matches undefined
|
|
if (obj1 === obj2 ||
|
|
isNaN(obj1) || isNaN(obj2) ||
|
|
obj1 == null || obj2 == null ||
|
|
type1 !== "object" || type2 !== "object") {
|
|
|
|
return identical(obj1, obj2);
|
|
}
|
|
|
|
// Elements are only equal if identical(expected, actual)
|
|
if (isElement(obj1) || isElement(obj2)) { return false; }
|
|
|
|
var isDate1 = isDate(obj1), isDate2 = isDate(obj2);
|
|
if (isDate1 || isDate2) {
|
|
if (!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
|
|
if (obj1.toString() !== obj2.toString()) { return false; }
|
|
}
|
|
|
|
var class1 = getClass(obj1);
|
|
var class2 = getClass(obj2);
|
|
var keys1 = keys(obj1);
|
|
var keys2 = keys(obj2);
|
|
|
|
if (isArguments(obj1) || isArguments(obj2)) {
|
|
if (obj1.length !== obj2.length) { return false; }
|
|
} else {
|
|
if (type1 !== type2 || class1 !== class2 ||
|
|
keys1.length !== keys2.length) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isSet(obj1) || isSet(obj2)) {
|
|
if (!isSet(obj1) || !isSet(obj2) || obj1.size !== obj2.size) {
|
|
return false;
|
|
}
|
|
|
|
return isSubset(obj1, obj2, deepEqual);
|
|
}
|
|
|
|
var key, i, l,
|
|
// following vars are used for the cyclic logic
|
|
value1, value2,
|
|
isObject1, isObject2,
|
|
index1, index2,
|
|
newPath1, newPath2;
|
|
|
|
for (i = 0, l = keys1.length; i < l; i++) {
|
|
key = keys1[i];
|
|
if (!o.hasOwnProperty.call(obj2, key)) {
|
|
return false;
|
|
}
|
|
|
|
// Start of the cyclic logic
|
|
|
|
value1 = obj1[key];
|
|
value2 = obj2[key];
|
|
|
|
isObject1 = isObject(value1);
|
|
isObject2 = isObject(value2);
|
|
|
|
// determine, if the objects were already visited
|
|
// (it's faster to check for isObject first, than to
|
|
// get -1 from getIndex for non objects)
|
|
index1 = isObject1 ? getIndex(objects1, value1) : -1;
|
|
index2 = isObject2 ? getIndex(objects2, value2) : -1;
|
|
|
|
// determine the new pathes of the objects
|
|
// - for non cyclic objects the current path will be extended
|
|
// by current property name
|
|
// - for cyclic objects the stored path is taken
|
|
newPath1 = index1 !== -1
|
|
? paths1[index1]
|
|
: path1 + '[' + JSON.stringify(key) + ']';
|
|
newPath2 = index2 !== -1
|
|
? paths2[index2]
|
|
: path2 + '[' + JSON.stringify(key) + ']';
|
|
|
|
// stop recursion if current objects are already compared
|
|
if (compared[newPath1 + newPath2]) {
|
|
return true;
|
|
}
|
|
|
|
// remember the current objects and their pathes
|
|
if (index1 === -1 && isObject1) {
|
|
objects1.push(value1);
|
|
paths1.push(newPath1);
|
|
}
|
|
if (index2 === -1 && isObject2) {
|
|
objects2.push(value2);
|
|
paths2.push(newPath2);
|
|
}
|
|
|
|
// remember that the current objects are already compared
|
|
if (isObject1 && isObject2) {
|
|
compared[newPath1 + newPath2] = true;
|
|
}
|
|
|
|
// End of cyclic logic
|
|
|
|
// neither value1 nor value2 is a cycle
|
|
// continue with next level
|
|
if (!deepEqual(value1, value2, newPath1, newPath2)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
}(obj1, obj2, '$1', '$2'));
|
|
}
|
|
|
|
function arrayContains(array, subset, compare) {
|
|
if (subset.length === 0) { return true; }
|
|
var i, l, j, k;
|
|
for (i = 0, l = array.length; i < l; ++i) {
|
|
if (compare(array[i], subset[0])) {
|
|
for (j = 0, k = subset.length; j < k; ++j) {
|
|
if ((i + j) >= l) { return false; }
|
|
if (!compare(array[i + j], subset[j])) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @name samsam.match
|
|
* @param Object object
|
|
* @param Object matcher
|
|
*
|
|
* Compare arbitrary value ``object`` with matcher.
|
|
*/
|
|
function match(object, matcher) {
|
|
if (matcher && typeof matcher.test === "function") {
|
|
return matcher.test(object);
|
|
}
|
|
|
|
if (typeof matcher === "function") {
|
|
return matcher(object) === true;
|
|
}
|
|
|
|
if (typeof matcher === "string") {
|
|
matcher = matcher.toLowerCase();
|
|
var notNull = typeof object === "string" || !!object;
|
|
return notNull &&
|
|
(String(object)).toLowerCase().indexOf(matcher) >= 0;
|
|
}
|
|
|
|
if (typeof matcher === "number") {
|
|
return matcher === object;
|
|
}
|
|
|
|
if (typeof matcher === "boolean") {
|
|
return matcher === object;
|
|
}
|
|
|
|
if (typeof(matcher) === "undefined") {
|
|
return typeof(object) === "undefined";
|
|
}
|
|
|
|
if (matcher === null) {
|
|
return object === null;
|
|
}
|
|
|
|
if (isSet(object)) {
|
|
return isSubset(matcher, object, match);
|
|
}
|
|
|
|
if (getClass(object) === "Array" && getClass(matcher) === "Array") {
|
|
return arrayContains(object, matcher, match);
|
|
}
|
|
|
|
if (isDate(matcher)) {
|
|
return isDate(object) && object.getTime() === matcher.getTime();
|
|
}
|
|
|
|
if (matcher && typeof matcher === "object") {
|
|
if (matcher === object) {
|
|
return true;
|
|
}
|
|
var prop;
|
|
for (prop in matcher) {
|
|
var value = object[prop];
|
|
if (typeof value === "undefined" &&
|
|
typeof object.getAttribute === "function") {
|
|
value = object.getAttribute(prop);
|
|
}
|
|
if (matcher[prop] === null || typeof matcher[prop] === 'undefined') {
|
|
if (value !== matcher[prop]) {
|
|
return false;
|
|
}
|
|
} else if (typeof value === "undefined" || !match(value, matcher[prop])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
throw new Error("Matcher was not a string, a number, a " +
|
|
"function, a boolean or an object");
|
|
}
|
|
|
|
return {
|
|
isArguments: isArguments,
|
|
isElement: isElement,
|
|
isDate: isDate,
|
|
isNegZero: isNegZero,
|
|
identical: identical,
|
|
deepEqual: deepEqualCyclic,
|
|
match: match,
|
|
keys: keys
|
|
};
|
|
});
|