Implementing a set that treats object equality on value not reference - javascript

I want to be able to implement a Set in javascript that allows me to do something like this:
const s = Set([[1,2,3], [1,2,3], 1, 2, 1]);
s.add([1,2,3]);
console.log(s);
// {[1,2,3], 1, 2}
Of course, since the === operator is used on the set, any object will not equal itself unless a reference to the same object is passed, and so instead of the above we would currently get:
Set(5) { [ 1, 2, 3 ], [ 1, 2, 3 ], 1, 2, [ 1, 2, 3 ] }
Does the following seem like a good way to implement this? What might I be missing or can improve on?
class MySet extends Set {
constructor(...args) {
super();
for (const elem of args) {
if (!this.has(elem)) super.add(elem);
}
}
has(elem) {
if (typeof elem !== 'object') return super.has(elem);
for (const member of this) {
if (typeof member !== 'object') continue;
if (JSON.stringify(member) === JSON.stringify(elem))
return true;
}
return false;
}
add(elem) {
return (this.has(elem)) ? this : super.add(elem);
}
delete(elem) {
if (typeof elem !== 'object') return super.delete(elem);
for (const member of this) {
if (typeof member !== 'object') continue;
if (JSON.stringify(member) === JSON.stringify(elem))
return super.delete(member);
}
return false;
}
}

Assuming the provided objects don't contain values that cannot be stringified to JSON (function, undefined, symbol, etc.) You can use JSON.stringify().
One problem you might encounter is that stringifying { a: 1, b: 2 } doesn't produce the same result as { b: 2, a: 1 }. A fairly easy way to solve this would be to stringify the object and make sure the resulting JSON has properties placed in alphabetical order.
For this we can look to the answer provided in sort object properties and JSON.stringify.
I also think you are over complicating things by only stringifying values if they are an object. Instead you could just stringfy everything, null would result in "null", "string" would result in '"string"', etc. This simplifies the code by a lot. The only restriction then becomes that all values must be a valid JSON value.
// see linked answer
function JSONstringifyOrder(obj, space)
{
const allKeys = new Set();
JSON.stringify(obj, (key, value) => (allKeys.add(key), value));
return JSON.stringify(obj, Array.from(allKeys).sort(), space);
}
class MySet extends Set {
// The constructor makes uses of add(), so we don't need
// to override the constructor.
has(item) {
return super.has(JSONstringifyOrder(item));
}
add(item) {
return super.add(JSONstringifyOrder(item));
}
delete(item) {
return super.delete(JSONstringifyOrder(item));
}
}
const set = new MySet([[1,2,3], [1,2,3], 1, 2, 1]);
set.add([1,2,3]);
set.add({ a: { s: 1, d: 2 }, f: 3 });
set.add({ f: 3, a: { d: 2, s: 1 } });
// Stack Overflow snippets cannot print Set instances to the console
console.log(Array.from(set));
// or unserialized
Array.from(set, json => JSON.parse(json)).forEach(item => console.log(item));

Related

How to check if an object has other than specific properties

I have an object obj which has n number of possible properties
lets say some of them are known,
const someKnownProps = ["props.abc", "xyz"]; // or more
I want to know if obj has other than known properties in it.
To clarify:
obj can look like this:
obj = {
props: {
abc: {
def: 1
},
ghi: {
jkl: 2
}
},
xyz: 3
}
Doing Object.keys only return first level children,
in this case it will return props not props.abc
You can use Object.keys to get all keys and filter the keys which aren't included in the someKnownProps array.
const obj = {
"props.abc": 1,
"xyz": 2,
"three": 3,
"four": 4,
}
const someKnownProps = ["props.abc", "xyz"]; // or more
const unknownKeys = Object.keys(obj).filter(key => !someKnownProps.includes(key))
console.log(unknownKeys)
There are two (unrelated) tasks involved in this question:
Traversal of an object's properties
Comparison of a set of traversed object properties to a list of strings representing dot-notation-formatted object property accessors
While I'm sure the former has been previously discussed on SO, I'll provide an implementation of such an algorithm below in order to address the details of this question.
This is essentially a specific case of recursion where each cycle starts with these inputs:
an object
a dot-notation-formatted path
a Set of existing such paths
The code below includes inline comments explaining what's happening, and there are some console.log statements at the end to help you visualize some example results based on the data in your question. If something is unclear after reviewing the code, feel free to leave a comment.
'use strict';
/** #returns whether value is a non-null, non-array object */
function isObject (value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
/** #returns the enumerable (optionally including inherited) keys of an object */
function getKeys (obj, includeInherited = false) {
if (!includeInherited) return Object.keys(obj);
const keys = new Set();
let o = obj;
while (o !== null) {
for (const key of Object.keys(o)) keys.add(key);
o = Object.getPrototypeOf(o);
}
return [...keys];
}
/**
* #returns an array of strings representing all traversible branches
* of child objects, each formatted as a combined path of dot-notation
* property accessors
*/
function findObjectPaths (
obj,
{
includeInherited = false,
currentPath = '',
paths = new Set(),
skipReturn = false,
} = {},
) {
for (const key of getKeys(obj, includeInherited)) {
// Append the current dot-notation property accessor
// to the existing path of this object:
const path = `${currentPath}.${key}`;
// Add it to the set:
paths.add(path);
const o = obj[key];
// Recurse if the child value is an object:
if (isObject(o)) {
findObjectPaths(o, {
includeInherited,
currentPath: path,
paths,
skipReturn: true,
});
}
}
// If this is not a sub-cycle (it's the top-level invocation), then convert
// the set to an array and remove the first "." from each string
if (!skipReturn) return [...paths].map(p => p.slice(1));
}
// Use:
const obj = {
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
};
let someKnownProps = ['props.abc', 'xyz'];
let objectPaths = findObjectPaths(obj);
let hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // true
// An example of all of the paths in the object above:
someKnownProps = [
'props',
'props.abc',
'props.abc.def',
'props.ghi',
'props.ghi.jkl',
'xyz',
];
objectPaths = findObjectPaths(obj);
hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // false
// Finally, comparing the results of inherited vs non-inherited enumeration:
const objWithoutOwnProps = Object.create({
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
});
console.log(
'Non-inherited props:',
findObjectPaths(objWithoutOwnProps),
);
console.log(
'Inherited props:',
findObjectPaths(objWithoutOwnProps, {includeInherited: true}),
);
Similar to what Mina said:
let obj = {one: 1, two: 2, three: 3};
let knownKeys = ['one', 'two'];
for (let key in obj) {
if (!knownKeys.includes(key)) {
console.log(key);
}
}

What's the most efficient way to compare values of two objects?

Let's say I have two objects, which might have some properties in common. The overall idea is that one is used as a schema to compare it to the other. I would like to compare values of common properties and construct a new object representing these comparisons of each individual property. In the result, any properties in common should have the value of comparing the source properties for equality (e.g. result.a.b.c = (obj1.a.b.c == obj2.a.b.c)). Any properties that exist on only one of the two objects would be copied to the result with their original values.
For example, consider the following objects to compare:
const schema = {
num: 123,
str: 'hello',
nested: {
num: 123,
str: 'hello',
},
},
doc = {
nums: [1,2,3],
str: 'hello',
nested: {
num: 123,
str: 'world',
foo: 'bar',
},
};
The comparison result should be:
{
nums: [1,2,3], // obj2.nums
num: 123, // obj1.num
str: true, // obj1.str == obj2.str
nested: {
num: true, // obj1.nested.num == obj2.nested.num
str: false, // obj1.nested.str == obj2.nested.str
foo: 'bar', // obj2.nested.foo
},
}
How would I perform this comparison in the most efficient time and space way?
Currently I have this implementation:
function deepCompare(obj1, obj2) {
const validatedObject = {}
Object.keys(obj1).forEach(e => {
if(object[e].constructor.name === 'Object') {
validatedObject[e] = deepCompare(obj1[e], obj2[e])
} else {
validatedObject[e] = obj1[e] === obj2[e]
}
})
return validatedObject
}
Are there any ways to do this more efficiently without using JSON.stringify, because I might have property that is missing, but I still want to return the fact that the values are correct despite the schema of those objects?
Maybe for in or for of loop? But how would I test their effectiveness? It's okay for small objects, but sometimes they get really big and I want to compare them in the most efficient way possible.
NOTE for editors.
I understand that this might seem like a duplicate, but it in fact is not, here I'm looking for a most efficient way to compare values, iteratively, recursively or some other way, not objects themselves
the logic is if obj1 has a key and obj2 has the same key, we compare the values of those keys
Then you should use
function compare(a, b) {
if (a === b) return true;
if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) {
return Object.keys(a).every(k =>
!(k in b) || compare(a[k], b[k])
);
}
return false;
}
I think your solution is good enough, it's only missing some default checks (you don't need to iterate through the objects keys if you have the same refference for example).
Also, there's this package that claims is the fastest deep equal checker.
You can look over the code to see if you missed any other condition (it's still a recursive solution for the objects, but there are some other data types treated there if you're interested).
There is also this article if you prefer written material instead of code that has more checks before recursive calls
What you want to do is first find the common object keys. Then as you loop through the properties you want to return false as soon as you find a mismatch. In the snippet below I'm using an every loop which will stop as soon as one iteration returns false. If you return a value in the if block you do not need the else block.
function deepCompare(obj1, obj2) {
const commonKeys = Object.keys(obj1).filter(x => Object.keys(obj2).includes(x));
let valid = true;
commonKeys.every(e => {
if (obj1[e].constructor.name === 'Object') {
console.log('object', e, obj1[e], obj2[e])
valid = deepCompare(obj1[e], obj2[e])
return valid // true goes to next, false ends the .every loop
}
console.log('not object', e, obj1[e] === obj2[e] ? 'match' : 'mismatch')
valid = obj1[e] === obj2[e]
return valid // true goes to next, false ends the .every loop
})
return valid // returns the first false or true if everything passes
}
const obj1 = {
num: 123,
str: 'hello',
nested: {
num: 123,
str: 'hello',
}
}
const obj3 = {
num: 1233,
str: 'hello'
}
const obj4 = {
num: 123,
str: 'hello',
nested: {
num: 123,
str: 'hellure',
}
}
console.log(deepCompare(obj1, obj1.nested))
console.log(deepCompare(obj1, obj3))
console.log(deepCompare(obj1, obj4))

How to modify each value of a nested object?

How can I multiply every nested value of this object by X (e.g. 0.5)?
const myObject = {
base: {
serving: {
size: 100
}
},
fat: {
acids: {
monoUnsaturatedFattyAcids: 12
polyUnsaturatedFattyAcids: 3
saturatedFattyAcids: 2
},
}
}
The object is sometimes nested up to 10 levels deep.
You could define a generator that will provide an iterator over every nested key/value pair (together with the nested object), so that you can do what you want with it inside a for loop:
function * iter(obj) {
for (let [key, value] of Object.entries(obj)) {
if (Object(value) !== value) yield [obj, key, value];
else yield * iter(value);
}
}
// demo
const myObject = {
base: {
serving: {
size: 100
}
},
fat: {
acids: {
monoUnsaturatedFattyAcids: 12,
polyUnsaturatedFattyAcids: 3,
saturatedFattyAcids: 2
},
}
};
for (let [obj, key, value] of iter(myObject)) {
if (typeof value === "number") obj[key] *= 0.5; // multiply by 0.5
}
// The object has been mutated accordingly
console.log(myObject);
function modifyValues(obj) {
for (let key in obj) {
if (typeof obj[key] === "object") {
modifyValues(obj[key]);
}else {
obj[key] = obj[key] * 0.5;
}
}
return obj;
}
console.log(modifyValues({
"a": 5,
"b": {
"c": 10
},
"d": {
"e": 15,
"f": {
"g": 20
}
}
}))
As suggested here's an explanation:
You can try something like a function that recursively iterates through the properties of an object and depending on said property its type, multiply or call our recursive function again.
These are the steps the function takes:
We use the Object.keys method to get an array containing all property names as strings of our object
We iterate through our keys array
for every key we check if obj[key] its value is either a number or something else.
note about obj[key]: by using square braces you can access properties of an object by passing a string. e.g obj['base'] is equivalent to obj.base
if it's type is indeed number, multiply obj[key] by 0.5! Don't forget to actually assign the value to the property.
if it ain't a number, call the function again, but this time we use the object stored in obj[key].
e.g. when you pass myObject to the function, the first key will be base, obj.base contains an object, so we call recursiveMultiplication(obj.base) and the cycle continues.
until every recursiveMultiplication call runs out of keys to iterate through.
When all is said and done, the original object should contain mutated values.
If you don't wish to mutate you should clone the object using something like rfdc. using {...spread} won't cut it for nested objects.
const myObject = {
base: {
serving: {
size: 100
}
},
fat: {
acids: {
monoUnsaturatedFattyAcids: 12,
polyUnsaturatedFattyAcids: 3,
saturatedFattyAcids: 2
}
}
};
const recursiveMultiplication = (obj) => {
Object.keys(obj).forEach(key => typeof obj[key] === "number" ? obj[key] = obj[key] * 0.5 : recursiveMultiplication(obj[key]))
return obj;
};
console.log(recursiveMultiplication(myObject));
I think the best is to use a library to ease the traverse of the nested object.
const _ = require('lodash')
const myObject = {
base: {
serving: {
size: 100
}
},
fat: {
acids: {
monoUnsaturatedFattyAcids: 12,
polyUnsaturatedFattyAcids: 3,
saturatedFattyAcids: 2
},
}
}
const newObjValue = _.cloneDeepWith(myObject, x => typeof x === 'number'? x*0.5: undefined)
console.log(newObjValue)
Here is quick solution. I have not added cases for arrays. need to add them if you expect arrays at the top level or nested inside.
const myObject = {
base: {
serving: {
size: 100
}
},
fat: {
acids: {
monoUnsaturatedFattyAcids: 12,
polyUnsaturatedFattyAcids: 3,
saturatedFattyAcids: 2
},
}
}
function isArray ( obj ) {
return isObject(obj) && (obj instanceof Array);
}
function isObject ( obj ) {
return obj && (typeof obj === "object");
}
function recursiveMultiplyNumberFields(myObject, X){
//Assuming that at the top level, what you pass on is a dictionar object. If it could be arrays as well, need to handle more cases
for (key in myObject){
if (isNaN(myObject [key])==false){
myObject[key] = X*myObject[key];
}
else if(isArray(myObject [key])){
/*not taken care as of now. Need to do if ararys are expected inside*/
}
else if(typeof myObject [key] === 'object' && myObject [key] !== null){
recursiveMultiplyNumberFields(myObject [key],X)
}
else{
//not a number and not a object. So need not do anything
}
}
}
console.log("Before mult",myObject);
recursiveMultiplyNumberFields(myObject,5);
console.log("After mult",myObject);

Can I use Destructuring to create a deep copy?

Basically I want to get a shallow deep copy that won't change my main object using destructuring.
let a = {
name: 'lala',
testArray: [1,2,3],
object: {
name: 'object',
array: [4,5,6]
}
};
const {name, testArray, object} = a;
object.array = [...object.array, 0];
console.log(a.object.array);
let b = {
object2: {
name: 'object',
array: [4,5,6]
}
};
const object2 = {...b.object2};
object2.array = [...object2.array, 0];
console.log(b.object2.array);
I made a jsfiddle (for easier reproduction) providing the code I wrote.
https://jsfiddle.net/5z71Lbja/
The problem is that the array of main object also changes when I change the "child" object using the first method(destructuring). The second method works fine but I'm curious if I can achieve the same result using destructuring.
You can't create new objects with destructuring, no. You can only pick out values that exist on the source, you can't perform transformations on them. (You can change the variable name you use, but you can't transform the value.) I've often wanted to, but you can't (at least, not at present).
There are various jump-through-the-hoops ways you could do it, but really the simplest is going to be to make a shallow copy of the array separately.
A simpler example:
const obj = {
foo: "bar",
array: [1, 2, 3]
};
const {foo} = obj;
const array = obj.array.slice(); // or: = [...obj.array];
obj.array[0] = "one";
console.log(obj.array[0]); // "one"
console.log(array[0]); // 1
Answer
You can achieve this by using a Proxy Object, either through a function, or directly.
Using a Function during Destruct:
The Proxy object takes the target object, and a handler.
A handler allows you to set certain conditions like get and set that can alter the way that data is returned to the user. We'll be using get for this.
In the below handler, we alter the get functionality. We check if the target[prop] returns an Array or Object. If it does, we create a copy in memory and return that instead of the reference. If it is not an Array or Object we simply return the primitive value (string, number, etc)
let copyHandler = {
get: function( target, prop, receiver ) {
let value = target[ prop ];
if ( Array.isArray( value ) ) return value.slice( 0 );
if ( typeof value === "object" && value.constructor.name === "Object" ) return Object.assign( {}, value );
return value;
}
}, getCopy = obj => new Proxy(obj, copyHandler);
Utilizing the getCopy function as our destructuring middle-man, we can be sure that all our values return new references:
const {
name,
testArray,
object
} = getCopy(a);
object.array = [...object.array, 0];
console.log(a.object.array); // [4,5,6]
console.log(object.array); // [4,5,6,0]
Example:
let copyHandler = {
get: function( target, prop, receiver ) {
let value = target[ prop ];
if ( Array.isArray( value ) ) return value.slice( 0 );
if ( typeof value === "object" && value.constructor.name === "Object" ) return Object.assign( {}, value );
return value;
}
}, getCopy = obj => new Proxy(obj, copyHandler);
let a = {
name: 'lala',
testArray: [ 1, 2, 3 ],
object: {
name: 'object',
array: [ 4, 5, 6 ]
}
};
const {
name,
testArray,
object
} = getCopy(a);
object.array = [...object.array, 0];
console.log(a.object.array); // [4,5,6]
console.log(object.array); // [4,5,6,0]
Alternatively, we can do it directly on Declaration/Initialization/Reception:
Directly in this sense means that we can setup the Object to return copies during destructured declaration only.
We do this similarly to above by utilizing a Proxy, and a middle-man function.
Note: The middle-man functions aren't necessary, but it helps keep things organized.
let destructHandler = {
get: function( target, prop, receiver ) {
if(!this.received) this.received = new Set();
let value = target[ prop ];
if(this.received.has(prop)) return value;
this.received.add(prop);
if ( Array.isArray( value ) ) return value.slice( 0 );
if ( typeof value === "object" && value.constructor.name === "Object" ) return Object.assign( {}, value );
return value;
}, destructable = obj => new Proxy(obj, destructHandler);
The difference here is that our get handler uses a Set to determine whether or not a property has already been grabbed once.
It will return copies upon the first request for a referential property (an Array or Object). It will still return any primitive value as normal.
This means that upon declaring/initialization/reception of the object, you can apply the destructable proxy and immediately afterwards pull out copies from that object using a destruct.
Initialization example code:
let a = destructable({
name: 'lala',
testArray: [ 1, 2, 3 ],
object: {
name: 'object',
array: [ 4, 5, 6 ]
}
});
Destructure example:
const {
name,
testArray,
object
} = a;
object.array = [...object.array, 0];
console.log(a.object.array); // [4,5,6]
console.log(object.array); // [4,5,6,0]
Example:
let destructHandler = {
get: function( target, prop, receiver ) {
if(!this.received) this.received = new Set();
let value = target[ prop ];
if(this.received.has(prop)) return value;
this.received.add(prop);
if ( Array.isArray( value ) ) return value.slice( 0 );
if ( typeof value === "object" && value.constructor.name === "Object" ) return Object.assign( {}, value );
return value;
}
}, destructable = obj => new Proxy(obj, destructHandler);
let a = destructable({
name: 'lala',
testArray: [ 1, 2, 3 ],
object: {
name: 'object',
array: [ 4, 5, 6 ]
}
});
const {
name,
testArray,
object
} = a;
object.array = [...object.array, 0];
console.log(object.array); // [4,5,6,0]
console.log(a.object.array); // [4,5,6]
Hope this helps! Happy Coding!
It is not possible directly.
let { object } = a;
object = {...object}; // Object.assign({}, object); <<
object.array = [0, ...object.array, 0];
console.log(object.array); // [0, 4, 5, 6, 0]
console.log(a.object.array); // [4, 5, 6]
I think this following code can be tried out for make a deep copy of an object, I used Destructuring.
function deepcopy(obj) {
let { ...data
} = obj;
let newObj = { ...data
}
// deleting one key (just to show that it doesn't affect original obj)
delete newObj.a
console.log("param obj ", obj);
return newObj;
}
const obj = {
a: 1,
b: 2,
c: 3
}
console.log("original obj ", obj);
console.log("copy newObj", deepcopy(obj));
console.log("original obj ", obj);
Hope this helps!!! :)

How to deeply remove keys in object?

I have this json object returned from an API that has a few quirks, and I'd like to normalize it so I can process the input the same for every response. These means getting rid of superfluous keys:
Response:
{
_links: {...},
_embedded: {
foo: [
{
id: 2,
_embedded: {
bar: []
}
}
]
}
}
So I'd like to remove all the _embedded keys and flatten it, like so:
{
_links: {...},
foo: [
{
id: 2,
bar: []
}
]
}
This is what I have at the moment, but it only works for the top level and I don't think it'll play well with arrays.
_.reduce(temp1, function(accumulator, value, key) {
if (key === '_embedded') {
return _.merge(accumulator, value);
}
return accumulator[key] = value;
}, {})
Loop in recursion on all of your keys, once you see a key which start with _
simply remove it.
Code:
var
// The keys we want to remove from the Object
KEYS_TO_REMOVE = ['_embedded'],
// The data which we will use
data = {
_links: {'a': 1},
_embedded: {
foo: [
{
id: 2,
_embedded: {
bar: []
}
},
{
id: 3,
_embedded: {
bar: [
{
id: 4,
_embedded: {
bar: []
}
}
]
}
}
]
}
};
/**
* Flatten the given object and remove the desired keys if needed
* #param obj
*/
function flattenObject(obj, flattenObj) {
var key;
// Check to see if we have flatten obj or not
flattenObj = flattenObj || {};
// Loop over all the object keys and process them
for (key in obj) {
// Check that we are running on the object key
if (obj.hasOwnProperty(key)) {
// Check to see if the current key is in the "black" list or not
if (KEYS_TO_REMOVE.indexOf(key) === -1) {
// Process the inner object without this key
flattenObj[key] = flattenObject(obj[key], flattenObj[key]);
} else {
flattenObject(obj[key], flattenObj);
}
}
}
return flattenObj;
}
console.log(flattenObject(data));
So, basically you already have almost all of the code you need. All we have to do is wrap it in a function so we can use recursion. You'll see we only add a check to see if it is an object, if it is, we already have a function that knows how to flatten that object, so we'll just call it again with the key that we need to flatten.
function flatten(temp1) { // Wrap in a function so we can use recursion
return _.reduce(temp1, function(accumulator, value, key) {
if (key === '_embedded') {
return _.merge(accumulator, value);
} else if (value !== null && typeof value === 'object') // Check if it's another object
return _.merge(accumulator, flatten(value)) // Call our function again
return accumulator[key] = value;
}, {})
}
I'll be able to test it in a bit, but this should be what you need.
Got it!
function unEmbed(data) {
return _.reduce(data, function(accumulator, value, key) {
const returnableValue = _.isObject(value) ? unEmbed(value) : value;
if (key === 'embedded') {
return _.merge(accumulator, returnableValue);
}
accumulator[key] = returnableValue;
return accumulator;
}, {});
}
Problem before I was returning return accumulator[key] = returnableValue, which worked out to be return returnableValue.

Categories

Resources