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 the following data array:
const data = [
{
value: [
'a',
'b',
'a',
'a'
]
},
{
value: [
'c',
'c',
'd',
'c'
]
}
];
So there's is 4 combination here based on index:
combination 1 : a - c (index 0 in each value arrays)
combination 2 : b - c (index 1 in each value arrays)
combination 3 : a - d (index 2 in each value arrays)
combination 4 : a - c (index 3 in each value arrays)
As you can see the first and the last combinations are the same, so i want to remove the second occurrence from each array, the result should be:
[
{
value: [
'a',
'b',
'a'
]
},
{
value: [
'c',
'c',
'd'
]
}
]
You can zip the values arrays from both objects to form an array which looks like:
["a-c", "b-c", ...]
As these are now strings, you can turn this array into a Set using new Set(), which will remove all duplicate occurrences. You can then turn this set back into an array which you can then use .reduce() on to build you array of objects from. For each value you can obtain the list of values by using .split() on the '-', and from that, populate your reduced array.
See example below:
const data = [{ value: [ 'a', 'b', 'a', 'a' ] }, { value: [ 'c', 'c', 'd', 'c' ] } ];
const unq = [...new Set(
data[0].value.map((_,c)=> data.map(({value})=>value[c]).join('-'))
)];
const res = unq.reduce((acc, str) => {
const values = str.split('-');
values.forEach((value, i) => acc[i].value.push(value));
return acc;
}, Array.from({length: data.length}, _ => ({value: []})))
console.log(res);
Limitations of the above method assume that you won't have a - character as your string value. If this is an issue, you can consider using a different delimiter, or find unique values within your array using .filter() instead of a Set.
You could save a lookup object for unique pairs of value based with index
Given your input is, below solution could help you
const data = [
{
value: ["a", "b", "a", "a"],
},
{
value: ["c", "c", "d", "c"],
},
]
const lookup = {}
data[0].value.forEach((_, index) => {
lookup[`${data[0].value[index]}-${data[1].value[index]}`] = true
})
const res = Object.keys(lookup).reduce(
(acc, key) => {
const [val1, val2] = key.split("-")
acc[0].value.push(val1)
acc[1].value.push(val2)
return acc
},
[{ value: [] }, { value: [] }]
)
console.log(res)
Below is a two step solution with a generator function and a single pass.
const data = [ { value: [ 'a', 'b', 'a', 'a' ] }, { value: [ 'c', 'c', 'd', 'c', ] } ];
const zipDataValues = function* (data) {
const iterators = data.map(item => item.value[Symbol.iterator]())
let iterations = iterators.map(iter => iter.next())
while (iterations.some(iteration => !iteration.done)) {
yield iterations.map(iteration => iteration.value)
iterations = iterators.map(iter => iter.next())
}
}
const filterOutDuplicateCombos = function (values) {
const combosSet = new Set(),
resultData = [{ value: [] }, { value: [] }]
for (const [valueA, valueB] of values) {
const setKey = [valueA, valueB].join('')
if (combosSet.has(setKey)) {
continue
}
combosSet.add(setKey)
resultData[0].value.push(valueA)
resultData[1].value.push(valueB)
}
return resultData
}
console.log(
filterOutDuplicateCombos(zipDataValues(data))
) // [ { value: [ 'a', 'b', 'a' ] }, { value: [ 'c', 'c', 'd' ] } ]
Here is a reference on generators and iterators
Filter combinations + sorting by the first occurrence:
const data = [{
value: ['a', 'b', 'a', 'a']
},{
value: ['c', 'c', 'd', 'c']
}];
var res = {}, i, t;
for (i = 0; i < data[0].value.length; ++i) {
res[data[0].value[i]] = res[data[0].value[i]] || {};
res[data[0].value[i]][data[1].value[i]] = true;
}
data[0].value = [];
data[1].value = [];
for (i in res) {
for (t in res[i]) {
data[0].value[data[0].value.length] = i;
data[1].value[data[1].value.length] = t;
}
}
console.log(data);
How can I combine this reduce and forEach so that we only traverse the list one time.
obj.arr = (obj.arr || []).reduce((newArr, arr2) => {
if (arr2.name !== anyProperty) {
newArr.push(arr2);
}
return newArr;
}, []);
obj.arr.forEach((arr2) => {
obj.arr[arr2.name] = arr2;
});
Check this code:
let anyProperty = 'test';
let obj = {
arr: [
{ name: 'a', value: 1 },
{ name: 'test', value: 2 },
{ name: 'b', value: 3 },
]
};
obj.arr = Object.fromEntries((obj.arr || [])
.filter(item => item.name !== anyProperty)
.map(item => [item.name, item]));
console.log(obj.arr);
I have an array filled with objects. The following example shows the structure of the objects.
let array = [
{
data: [{name:'a', value:20}, {name:'b', value:10}, {name:'c', value:5}]
},
{
data: [{name:'d', value:20}, {name:'a', value:10}, {name:'e', value:40}]
},
{
data: [{name:'b', value:30}, {name:'a', value:5}]
}
];
I'm trying to iterate through all the data values and summarize all the identical letters and sum up there values in a new array. So the new array should look like this:
let array = [{name:'a', value:35}, {name:'b', value:40}, {name:'c', value:5}, {name:'d', value:20}, {name:'e', value:40}];
This is my current approach but I don't get it to work.
let prevData = '';
let summarizedArray = [];
for(let i = 0; i < array.length; i++) {
for(let j = 0; j < array[i].data.length; j++) {
if(prevData === array[i].data[j].name) {
let summarized = {
name: array[i].data[j].name;
value: prevData.value + array[i].data[j].value;
}
summarizedArray.push(summarized);
}
prevData = array[i].data[j];
}
}
// Edited Example:
let array = [
{
data: [{name:'a', value1:20, value2:90, value3:'foo'},
{name:'b', value1:30, value2:20, value3:'boo'}]
},
data: [{name:'c', value1:5, value2:10, value3:'goo'},
{name:'a', value1:30, value2:20, value3:'foo'}]
},
{
];
The values should be bundled by same names. The values of Value1 and Value2 should be added up and Value3 is always the same for each name.
So the result should look like this:
let result = [{name:'a', value1:50, value2:110, value3:'foo'},
{name:'b', value1:30, value2:20, value3:'boo'},
{name:'c', value1:5, value2:10, value3:'goo'}
];
You could take a Map and collect all values. Later get an array of object of the collected values.
let array = [{ data: [{ name: 'a', value: 20 }, { name: 'b', value: 10 }, { name: 'c', value: 5 }] }, { data: [{ name: 'd', value: 20 }, { name: 'a', value: 10 }, { name: 'd', value: 40 }] }, { data: [{ name: 'b', value: 30 }, { name: 'a', value: 5 }] }],
result = Array.from(
array.reduce(
(m, { data }) => data.reduce(
(n, { name, value }) => n.set(name, (n.get(name) || 0) + value),
m
),
new Map
),
([name, value]) => ({ name, value })
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
For a more convoluted object, you could take single properties to add, after a check for the type.
var array = [{ data: [{ name: 'a', value1: 20, value2: 90, value3: 'foo' }, { name: 'b', value1: 30, value2: 20, value3: 'boo' }] }, { data: [{ name: 'c', value1: 5, value2: 10, value3: 'goo' }, { name: 'a', value1: 30, value2: 20, value3: 'foo' }] }],
result = Array.from(
array.reduce(
(m, { data }) => {
data.forEach(o => {
var temp = m.get(o.name);
if (!temp) {
m.set(o.name, temp = {});
}
Object.entries(o).forEach(([k, v]) => {
if (k === 'name') return;
if (typeof v === 'number') {
temp[k] = (temp[k] || 0) + v;
} else {
temp[k] = v;
}
});
});
return m;
},
new Map
),
([name, value]) => Object.assign({ name }, value)
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
If I have this:
var myObj = [
{ name: 'A', number: 'b1',level: 0 },
{ name: 'B', number: 'b2',level: 0 },
];
How can I extract all the names like:
"names": {
'A',
'B'
}
You could use this function to get an array of values, not object properties (which are not suited for that):
function getColumn(arr, column) {
return arr.map(function (rec) { return rec[column] });
}
// Sample data
var myObj = [
{ name: 'A', number: 'b1',level: 0 },
{ name: 'B', number: 'b2',level: 0 },
];
// Get names
var names = getColumn(myObj, 'name');
// Output
console.log(names);