Related
I have two deal with two Set instances.
const set1 = new Set([
{ name: 'a' },
{ name: 'b', lastname: 'bb' },
{ name: 'c' },
{ name: 'd' },
]);
const set2 = new Set([
{ name: 'b' },
{ name: 'd' },
]);
Any object within a set will feature several and also distinct keys and values. The goal is to find structurally equal objects (same keys and values) in both sets, which is ... The intersection of equal data items in/of set1 and set2.
In the following example the expected result is [ { name: 'd' } ] ...
console.log([...set1].filter(item => set2.has(item)));
... but it logs an empty array / [] instead.
An object features more than 20 keys so one has to compare them one by one, which can not be done in a hard coded way.
How could one achieve a generic approach for an intersection of two lists of structurally equal data items?
You can do something like this:
const set1 = new Set([
{name: 'a'},
{name: 'b', lastname: 'bb'},
{name: 'c'},
{name: 'd'}
]);
const set2 = new Set([
{name: 'b'},
{name: 'd'}
]);
set1.forEach((value) => {
if (![...set2].some((o) => Object.entries(o).every(([k, v], _, arr) => (Object.keys(value).length === arr.length && value[k] === v)))) {
set1.delete(value);
}
})
console.log([...set1]);
What this does, is to iterate through set1 and if the item at the current iteration is not the same as any item in set2 (![...set2].some(..)), it is deleted.
The items are considered the same if they have the same number of keys and if the values at the same key are strictly equal.
This only works if the values of the objects in the sets are primitives, if they are not, you'll have to change value[k] === v to an appropriate comparison.
One could write a generic solution which compares pure, thus JSON conform, data structures regardless of any object's nesting depth/level and (creation time) key order.
Such a function would be self recursive for Array item (order matters) and Object property (key order does not matter) comparison. Otherwise values are compared strictly.
function isDeepDataStructureEquality(a, b) {
let isEqual = Object.is(a, b);
if (!isEqual) {
if (Array.isArray(a) && Array.isArray(b)) {
isEqual = (a.length === b.length) && a.every(
(item, idx) => isDeepDataStructureEquality(item, b[idx])
);
} else if (
a && b
&& (typeof a === 'object')
&& (typeof b === 'object')
) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
isEqual = (aKeys.length === bKeys.length) && aKeys.every(
(key, idx) => isDeepDataStructureEquality(a[key], b[key])
);
}
}
return isEqual;
}
const objA = { // `objA` equals `objB`.
name: 'foo',
value: 1,
obj: {
z: 'z',
y: 'y',
a: {
name: 'bar',
value: 2,
obj: {
x: 'x',
w: 'w',
b: 'b',
},
arr: ['3', 4, 'W', 'X', {
name: 'baz',
value: 3,
obj: {
k: 'k',
i: 'i',
c: 'c',
},
arr: ['5', 6, 'B', 'A'],
}],
},
},
arr: ['Z', 'Y', 1, '2'],
};
const objB = { // `objB` equals `objA`.
arr: ['Z', 'Y', 1, '2'],
obj: {
z: 'z',
y: 'y',
a: {
obj: {
x: 'x',
w: 'w',
b: 'b',
},
arr: ['3', 4, 'W', 'X', {
obj: {
k: 'k',
i: 'i',
c: 'c',
},
name: 'baz',
value: 3,
arr: ['5', 6, 'B', 'A'],
}],
name: 'bar',
value: 2,
},
},
name: 'foo',
value: 1,
};
const objC = { // `objC` equals neither `objA` nor `objB`.
arr: ['Z', 'Y', 1, '2'],
obj: {
z: 'z',
y: 'y',
a: {
obj: {
x: 'x',
w: 'w',
b: 'b',
},
arr: ['3', 4, 'W', 'X', {
obj: {
k: 'k',
i: 'i',
c: 'C', // the single difference to `objA` and `objB`.
},
name: 'baz',
value: 3,
arr: ['5', 6, 'B', 'A'],
}],
name: 'bar',
value: 2,
},
},
name: 'foo',
value: 1,
};
console.log(
'isDeepDataStructureEquality(objA, objB) ?..',
isDeepDataStructureEquality(objA, objB)
);
console.log(
'isDeepDataStructureEquality(objA, objC) ?..',
isDeepDataStructureEquality(objA, objC)
);
console.log(
'isDeepDataStructureEquality(objB, objC) ?..',
isDeepDataStructureEquality(objB, objC)
);
console.log(
'isDeepDataStructureEquality(objB, objA) ?..',
isDeepDataStructureEquality(objB, objA)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Based on the above implementation of isDeepDataStructureEquality one can solve the OP's task, that actually looks for the intersection of two list structures, by additionally providing a getIntersectionOfDeeplyEqualDataStructures functionality ...
function getIntersectionOfDeeplyEqualDataStructures(a, b) {
return [...(a ?? [])]
.reduce((collector, sourceItem) => {
const { target, intersection } = collector;
const targetIndex = target.findIndex(targetItem =>
isDeepDataStructureEquality(targetItem, sourceItem)
);
if (targetIndex >= 0) {
// collect the intersection of
// both, source (a) and target (b).
intersection.push(target[targetIndex]);
}
return collector;
}, {
target: [...(b ?? [])],
intersection: [],
}).intersection;
}
const set1 = new Set([
{ name: 'a' },
{ name: 'b', lastname: 'bb' },
{ name: 'c' },
{ name: 'd' }
]);
const set2 = new Set([
{ name: 'b' },
{ name: 'd' },
]);
console.log(
"getIntersectionOfDeeplyEqualDataStructures(set1, set2) ...",
getIntersectionOfDeeplyEqualDataStructures(set1, set2)
);
const set3 = new Set([
{ name: 'a' },
{ name: 'b', lastname: 'bb' },
{ name: 'c' },
{
name: 'd',
list: ['foo', 1, null, false, 0, {
foo: { bar: { baz: 'bizz', buzz: '' } }
}],
},
]);
const set4 = new Set([
{
list: ['foo', 1, null, false, 0, {
foo: { bar: { buzz: '', baz: 'bizz' } }
}],
name: 'd',
},
{ name: 'C' },
{ lastname: 'bb', name: 'b' },
{ name: 'aa' }
]);
console.log(
"getIntersectionOfDeeplyEqualDataStructures(set3, set4) ...",
getIntersectionOfDeeplyEqualDataStructures(set3, set4)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
function isDeepDataStructureEquality(a, b) {
let isEqual = Object.is(a, b);
if (!isEqual) {
if (Array.isArray(a) && Array.isArray(b)) {
isEqual = (a.length === b.length) && a.every(
(item, idx) => isDeepDataStructureEquality(item, b[idx])
);
} else if (
a && b
&& (typeof a === 'object')
&& (typeof b === 'object')
) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
isEqual = (aKeys.length === bKeys.length) && aKeys.every(
(key, idx) => isDeepDataStructureEquality(a[key], b[key])
);
}
}
return isEqual;
}
</script>
Edit
As for Titus' approach ...
set1.forEach(value => {
if (
![...set2].some(o =>
Object.entries(o).every(([k, v], _, arr) =>
(Object.keys(value).length === arr.length && value[k] === v)
)
)
) {
set1.delete(value);
}
});
... which works for flat objects only, though already agnostic to key insertion order, one could optimize the code by ...
... not creating the keys array of the most outer currently processed object again and again with every nested some and every iteration.
thus, something like ... const valueKeys = Object.keys(value); ... before the if clause, already helps improving the code.
... inverting the nested some and every logic which does result in a more efficient way of ... deleting every flat data-item from the processed set which does not equal any flat data-item from the secondary set.
On top of that, one could implement a function statement which not only helps code-reuse but also makes the implementation independent from outer scope references.
For instance, the primary set which is operated and going to be mutated can be accessed as such a function's third parameter. But most important for outer scope independency is the also available thisArg binding for any set's forEach method. Thus any function statement or function expression can access e.g. the other/secondary set via this in case the latter was passed as the forEach's 2nd parameter.
Also an improved wording supports a better readability of the code ...
//the function naming of cause is exaggerated.
function deleteItemFromSourceWhichDoesNotEqualAnyItemFromBoundTarget(sourceItem, _, sourceSet) {
const targetSet = this;
const sourceKeys = Object.keys(sourceItem);
if (
// ... for any data-item from the (bound) target-set ...
[...targetSet].every(targetItem =>
// ... which does not equal the currently processed data-item from the source-set ...
Object.entries(targetItem).some(([targetKey, targetValue], _, targetEntries) =>
sourceKeys.length !== targetEntries.length || sourceItem[targetKey] !== targetValue
)
)
) {
// ... delete the currently processed data-item from the source-set.
sourceSet.delete(sourceItem);
}
}
const set1 = new Set([
{ name: 'a' }, // - to be kept.
{ name: 'b', lastname: 'bb' }, // - to be kept.
{ name: 'c' }, // - to be deleted.
{ name: 'd', nested: { name: 'a' } }, // - to be kept, but fails ...
]); // ... due to not being flat.
const set2 = new Set([
{ name: 'd', nested: { name: 'a' } }, // - should equal, but doesn't.
{ name: 'a' }, // - does equal.
{ lastname: 'bb', name: 'b' }, // - does equal.
{ name: 'e' }, // - doesn't equal.
]);
// `set1` is going to be mutated.
set1.forEach(deleteItemFromSourceWhichDoesNotEqualAnyItemFromBoundTarget, set2);
console.log(
'mutated `set1` now (almost) being equal to the intersection of initial `set1` and `set2` ...',
[...set1]
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
const set1 = new Set([
{name: 'a'},
{name: 'b', lastname: 'bb'},
{name: 'c'},
{name: 'd'}
]);
const set2 = new Set([
{name: 'b'},
{name: 'd'}
]);
const names = [...set2].map(s2 => s2.name);
console.log([...set1].filter(item => names.includes(item.name)));
const set1 = new Set([
{name: 'a'},
{name: 'b', lastname: 'bb'},
{name: 'c'},
{name: 'd'},
{name: 'e'}
]);
const set2 = new Set([
{name: 'c', lastname: 'ccc'},
{name: 'd'},
{name: 'b', lastname: 'cc'},
{name: 'e'}
]);
console.log([...set1].filter(item => {
const s2Arr = [...set2];
const itemKeys = Object.keys(item);
for(let i = 0; i < s2Arr.length; i++){
const s2Obj = s2Arr[i];
const s2ObjKeys = Object.keys(s2Obj);
if(s2ObjKeys.length == itemKeys.length){
let oneSame = true;
for(let j = 0; j < s2ObjKeys.length; j++){
const s2ObjKey = s2ObjKeys[j];
if(item[s2ObjKey] != s2Obj[s2ObjKey]){
oneSame = false;
}
}
if(oneSame)
return true;
}
}
return false;
}));
I have an array of object - like this -
test: [
{
id:'1',
name:'A'
},
{
id:'2',
name:'B'
},
]
Suppose I have a value 2 that exists in object Test as id. I want to get whole object from array if id value exists in whole array
input - 2,
expected output - {id:'2' , name:'B'}
How Can we get it ? is it any possible solution ?
Simply use find-
const val = [
{
id: '1',
name: 'A',
},
{
id: '2',
name: 'B',
},
];
const res = val.find(obj => obj.id === '2');
console.log(res);
There can be multiple ways to do this. Here is how I did it.
let test = [
{
id: '1',
name: 'A'
},
{
id: '2',
name: 'B'
}
];
let result = (param) => test.filter(el => {
return el.id == param
});
console.log(result(2))
I want to create an object with the given structure:
const questions = [
{
id: '1',
instruction: 'Question1',
options: {
'a': 'SomeText1',
'b': 'SomeText2',
'c': 'SomeText3'
},
correct: ['c']
},
{
id: '2',
instruction: 'Question2',
options: {
'a': 'SomeText1',
'b': 'SomeText2',
'c': 'SomeText3',
'd': 'SomeText4'
},
correct: ['a']
},
{
id: '3',
instruction: 'Question3,
options: {
'a': 'SomeText1',
'b': 'SomeText2'
},
correct: ['b']
}
]
I have an arrays containing necessary information to fill this object and create it using .map() .
const questions =
[ 'Question1',
'Question2',
'Question3'
]
const answers =
[
'Answer1',
'Answer2',
'Answer3'
]
const options = [
[ 'Option1', 'Option2', 'Option3' ],
[ 'Option4', 'Option5', 'Option6' ],
[ 'Option7', 'Option8', 'Option9' ]
]
function toJson()
const alphabet = ['a', 'b', 'c', 'd', 'e'];
const json = questions.map((question, index) => (
{
id: index + 1,
instruction: question,
options: Object.assign({}, options[index]),
correct: answers[index]
}
))
}
I have only problem with options key. As You see I want to have a letters as keys, depending on how many answers question has.
This function gives me numbers as keys when I use Object.assign(), and I don't know how to replace them with letters from alphabet array.
EDIT:
So the solution for the options key in desired object is:
options: Object.assign(
{},
...options[index].map((a, i) => ({ [alphabet[i]]: a }))
),
Now I'm able to create an object with consecutive alphabet letters with assigned answer.
options[index] returns an array. It contains values by index. By passing it to Object.assign, you add all values by their array index as a string: "0", "1", etc.
If we map the array in to a list of { "a": option } first, and spread the result in to the Object.assign call, we can change those indexes to the letters you want:
const questions =
[ 'Question1',
'Question2',
'Question3'
]
const answers =
[
'Answer1',
'Answer2',
'Answer3'
]
const options = [
[ 'Option1', 'Option2', 'Option3' ],
[ 'Option4', 'Option5', 'Option6' ],
[ 'Option7', 'Option8', 'Option9' ]
]
function toJson() {
const alphabet = ['a', 'b', 'c', 'd', 'e'];
const json = questions.map((question, index) => (
{
id: index + 1,
instruction: question,
options: Object.assign(
{},
...options[index].map((a, i) => ({ [alphabet[i]]: a }))
),
correct: answers[index]
}
));
return json;
}
console.log(toJson());
I suggest using something like zip and objectFromPairs (both snippets from 30secondsofcode, a project/website I am a maintainer of). From the website:
zip
Creates an array of elements, grouped based on the position in the original arrays.
Use Math.max.apply() to get the longest array in the arguments. Creates an array with that length as return value and use Array.from() with a map-function to create an array of grouped elements. If lengths of the argument-arrays vary, undefined is used where no value could be found.
objectFromPairs
Creates an object from the given key-value pairs.
Use Array.reduce() to create and combine key-value pairs.
The only extra step I took was to trim each zipped array to the length of options[index].
const questions = ['Question1',
'Question2',
'Question3'
]
const answers = [
'Answer1',
'Answer2',
'Answer3'
]
const options = [
['Option1', 'Option2', 'Option3'],
['Option4', 'Option5', 'Option6'],
['Option7', 'Option8', 'Option9']
]
const zip = (...arrays) => {
const maxLength = Math.max(...arrays.map(x => x.length));
return Array.from({
length: maxLength
}).map((_, i) => {
return Array.from({
length: arrays.length
}, (_, k) => arrays[k][i]);
});
};
const objectFromPairs = arr => arr.reduce((a, [key, val]) => ((a[key] = val), a), {});
function toJson() {
const alphabet = ['a', 'b', 'c', 'd', 'e'];
const json = questions.map((question, index) => ({
id: index + 1,
instruction: question,
options: objectFromPairs(zip(alphabet, options[index]).slice(0, options[index].length)),
correct: answers[index]
}))
console.log(json);
}
toJson();
the below should work (i believe)
options: alphabet.reduce((acc, letter, i) => {
let option = options[index][i] || 'DefaultText' + i;
acc[letter] = option;
return acc;
}, {})
Edit: Corrected typos
EDIT:
So the solution for the options key in desired object is:
options: Object.assign(
{},
...options[index].map((a, i) => ({ [alphabet[i]]: a }))
),
so I have an array of objects inside like this:
let ary = [
{
a : 'something',
b: '2',
c: 'something'
},
{
a : 'something',
b: '2',
c: 'something'
},
{
a : 'something',
b: '2',
c: 'something'
}
]
I need to add up all key b's value on each object so I first change them to number with parseFloat like this:
ary.forEach( item => {
item.b = parseFloat(item.b)
})
Next I use reduce() inside a .map function.
let total = ary.map((item, index) => {
return ary.reduce((a, b) => ({x: a.b + b.b}))
})
but I did not get what I want, it return an array of objects with both same total value.
array = [
{x : total},
{x : total}
]
How should I get just single total value? like
total = 2+2+2 and each 2 is from the b key in ary.
You can try with:
const total = ary.reduce((sum, item) => (sum + parseFloat(item.b)), 0);
With one loop you'll invoke parseFloat and summ all b properties.
You could chain some methods by separating the value of the objects, take numbers and add all values with reduce.
var array = [{ a: 'something', b: '2', c: 'something' }, { a: 'something', b: '2', c: 'something' }, { a: 'something', b: '2', c: 'something' }],
sum = array
.map(({ b }) => b)
.map(Number)
.reduce((a, b) => a + b);
console.log(sum);
Assume we have the following arrays of objects to be compared based on property id:
a = [{'id':'1', 'name':'a1'}, {'id':'2', 'name':'a2'}, {'id':'3', 'name':'a3'}]
and
b = [[{'id':'2', 'name':'a2'}, ]
How can I subtract b from a? So that we have c = a - b which should be equal to [ {'id':'1', 'name':'a1'}, {'id':'3', 'name':'a3'}].
I have tried using this:
var c= a.filter(function(item) {
return !b.includes(item.id);
});
but still not working.
How about this solution? It assumes that 'b' is also an array so for each element of 'a' you check if there is a matching object in 'b'. If there is a matching object then return a false in the filter function so that that element is discarded.
var a = [{
'id': '1',
'name': 'a1'
}, {
'id': '2',
'name': 'a2'
}, {
'id': '3',
'name': 'a3'
}]
var b = [{
'id': '2',
'name': 'a2'
}]
var c = a.filter(function(objFromA) {
return !b.find(function(objFromB) {
return objFromA.id === objFromB.id
})
})
console.log(c)
Here is a nice one line answer :)
Basically, you can filter, as you were trying to do already. Then you can also filter b for each a element and if the length of the filtered b is zero, then you return true because that means the a element is unique to a.
var a = [{
'id': '1',
'name': 'a1'
}, {
'id': '2',
'name': 'a2'
}, {
'id': '3',
'name': 'a3'
}];
var b = [{
'id': '2',
'name': 'a2'
}];
c = a.filter( x => !b.filter( y => y.id === x.id).length);
console.log(c);
Easy with new ES6 Syntax
Second and Third way are more performant i guess....
a.filter(i => !b.filter(y => y.id === i.id).length); // One Way
a.filter(i => !b.find(f => f.id === i.id)); // Second Way
a.filter(i => b.findIndex(f => f.id === i.id)) // Third Way
First, you build just a map of the ids you want to delete.
Then, you filter your first array with it, like that:
var a = [{
'id': '1',
'name': 'a1'
}, {
'id': '2',
'name': 'a2'
}, {
'id': '3',
'name': 'a3'
}];
var b = [{
'id': '2',
'name': 'a2'
}];
var idsToDelete = b.map(function(elt) {return elt.id;});
var result = a.filter(function(elt) {return idsToDelete.indexOf(elt.id) === -1;});
console.log(result)