Last active
February 7, 2023 22:57
-
-
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...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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