Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active February 7, 2023 22:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/a498352824f13a37a99d9c92b022ef95 to your computer and use it in GitHub Desktop.
Save dfkaye/a498352824f13a37a99d9c92b022ef95 to your computer and use it in GitHub Desktop.
partial-equal in JavaScript: compare an object's values to a minimal "schema", ignoring extra fields on the compared object, and more...
// 5 February 2023
// partial-equal in JavaScript
// inspired by https://brandur.org/fragments/partial-equal
// compare an object's values to a minimal "schema" which can be a primitive,
// allows extra fields on the compared object but expected fields must match
// expected values, including symbols as field names and field values;
// first failure terminates, logs a warning.
// 6 Feb 2023
// first impl.
// console warnings contain template literals and interpolations with
// ${String(symbol)}. This prevents "can't convert symbol to string" TypeError
// messages.
// For more about string coercion and symbols in template literals, visit
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion
// 7 Feb 2023
// More tests, readability fixes.
// The helper function nearly() became necessary to help readability in the
// function partial().
// "Nearly equal" means items are strictly equal unless they are symbols,
// in which case they are "stringly" equal.
function nearly(s, o) {
var a = typeof s == "symbol"
? String(s)
: s;
var b = typeof o == "symbol"
? String(o)
: o;
return a === b;
}
// The partial() function accepts two arguments which may be anything.
// The main idea is to compare two objects of the same shape while allowing
// the compared object to include extraneous fields.
// Returns true if the compared object's fields contain the same values as
// the shape object.
// If the shape is a primitive, the compared value must be the same value
// in order to pass.
function partial(shape, any) {
function visit(s, o) {
// exit if values are strictly equal.
if (s === o) {
return true;
}
// if either value is a primitive, check near equality.
if ((Object(s) !== s || Object(o) !== o) && !nearly(s, o)) {
console.warn(`expected value to be "${String(s)}" but saw "${String(o)}"`);
return false;
}
// otherwise iterate over field names.
return Object.getOwnPropertySymbols(s)
.concat(Object.getOwnPropertyNames(s))
.every(function (key, i, a) {
// if values at this key are objects, visit them recursively.
if (Object(s[key]) === s[key] && Object(o[key]) === o[key]) {
return visit(s[key], o[key]);
}
var missing, fieldsUnequal;
if (!(key in o)) {
console.warn(`missing name or symbol, "${String(key)}"`);
missing = true;
}
else if (!nearly(s[key], o[key])) {
console.warn(`expected value for "${key}" to be "${String(s[key])}" but saw "${String(o[key])}"`);
fieldsUnequal = true;
}
return !(missing || fieldsUnequal);
});
}
return visit(shape, any);
}
/* test it out */
var base = {
['hey']: 1,
['no']: false,
check: {name: 'object'},
[Symbol("fake")]: Symbol("fake"),
["symbol"]: Symbol("symbol")
};
var test = Object.assign({}, base, {
44: 45,
check: {name: 'obect'},
[Symbol("fake")]: 'kn',
["symbol"]: Symbol("symbol")
});
console.log("different values at check.name should return false:", partial(base, test));
test.check.name = "object";
console.log("same value at check.name should return true:", partial(base, test));
console.log("unequal numbers should return false:", partial(1,0));
console.log("equal numbers should return true:", partial(1, 1));
console.log("unequal symbols should return false:", partial(Symbol("dog"), Symbol("cat")));
console.log("equal symbols should return true:", partial(Symbol("42"), Symbol("42")));
console.log("unequal nothing should return false:", partial(undefined, null));
console.log("equal nothing should return true:", partial(null, null));
console.log("missing field should return false:", partial({[Symbol("here")]: Symbol("here")}, {}));
// 7 Feb 2023:
// Surprising result: the two Symbol("k") field names are considered unique, meaning it
// is considered as missing from the test object.
console.log("symbol keys unequal should return false:", partial({[Symbol("k")]: symbol}, {[Symbol("k")]: symbol}));
// This symbol is generated once, and therefore is present in both objects.
var symbol = Symbol("k");
console.log("symbol key equal should return true:", partial({[symbol]: symbol}, {[symbol]: symbol}));
// object, null, primitive
var object = {toString() { return "test object"}};
console.log("object vs primitive should return false:", partial(object, 2));
console.log("null vs primitive should return false:", partial(null, 2));
console.log("null vs object should return false:", partial(null, object));
/*
expected value for "name" to be "object" but saw "obect"
different values at check.name should return false: false
same value at check.name should return true: true
expected value to be "1" but saw "0"
unequal numbers should return false: false
equal numbers should return true: true
expected value to be "Symbol(dog)" but saw "Symbol(cat)"
unequal symbols should return false: false
equal symbols should return true: true
expected value to be "undefined" but saw "null"
unequal nothing should return false: false
equal nothing should return true: true
missing name or symbol, "Symbol(here)"
missing field should return false: false
missing name or symbol, "Symbol(k)"
symbol keys unequal should return false: false
symbol key equal should return true: true
expected value to be "test object" but saw "2"
object vs primitive should return false: false
expected value to be "null" but saw "2"
null vs primitive should return false: false
expected value to be "null" but saw "test object"
null vs object should return false: false
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment