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'm trying to find the intersection between n >=2 object arrays which share the same key while also combining the object arrays. I'm looking for the most efficient way to do this. The max length of any of the arrays would be around ~2500 objects long.
An example of what I'm looking for :
object1 = [
{
key: 'key1',
x: 'x',
y: 'y',
z: 'z'
},
{
key: 'key2',
x: 'x',
y: 'y',
z: 'z'
}
]
object2 = [
{
key: 'key1',
a: 'a',
b: 'b'
},
{
key: 'key3',
a: 'a',
b: 'b'
}
]
object3 = [
{
key: 'key1',
c: 'c'
},
{
key: 'key4',
c: 'c'
}
]
with the desired output :
object = [
{
key: 'key1',
x: 'x',
y: 'y',
z: 'z',
a: 'a',
b: 'b',
c: 'c'
}
]
To combine two object arrays, I've used the following function :
let map = new Map(object1.map(o => [o['key'], o]));
return object2.reduce((acc, o) => {
let match = map.get(o['key']);
return match ? acc.concat({ ...o, ...match }) : acc;
}, []);
But I'm not sure how to apply this to more arrays in a way that's not resource and time heavy. Any tips would be appreciated. I'm also open for using helper functions like underscore or lodash.
The advantage (i hope) with this code is that it will preprocess all objects first to find intersecting keys before attempting to reduce and combine each one. In the first iteration fkeys flattens all objects together so we can do just one loop.
Separating those 2 processes should reduce overhead. My guess though is that there's a more elegant way to do this. I added in another intersecting key (key2) for demonstration in snippet
let objs = [object1, object2, object3],
fkeys = Object.keys(Object.fromEntries(Object.entries([...objs.flat()].map(o => o.key).reduce((b, a) => {
if (b.hasOwnProperty(a)) b[a] = b[a] + 1;
else b[a] = 1;
return b
}, {})).filter(k => k[1] === objs.length))),
intersections = Object.values([...objs.flat()].filter(o => fkeys.includes(o.key)).reduce((b, a) => {
if (b.hasOwnProperty(a.key)) b[a.key] = { ...b[a.key], ...a};
else b[a.key] = { ...a};
return b}, {}))
object1 = [{
key: 'key1',
x: 'x',
y: 'y',
z: 'z'
},
{
key: 'key2',
g: 'g',
h: 'h',
j: 'j'
}
]
object2 = [{
key: 'key1',
a: 'a',
b: 'b'
},
{
key: 'key2',
y: 'y',
z: 'z'
},
{
key: 'key3',
a: 'a',
b: 'b'
}
]
object3 = [{
key: 'key1',
c: 'c'
},
{
key: 'key2',
r: 'r'
},
{
key: 'key4',
c: 'c'
}
]
let objs = [object1, object2, object3],
fkeys = Object.keys(Object.fromEntries(Object.entries([...objs.flat()].map(o => o.key).reduce((b, a) => {
if (b.hasOwnProperty(a)) b[a] = b[a] + 1;
else b[a] = 1;
return b
}, {})).filter(k => k[1] === objs.length))),
intersections = Object.values([...objs.flat()].filter(o => fkeys.includes(o.key)).reduce((b, a) => {
if (b.hasOwnProperty(a.key)) b[a.key] = { ...b[a.key],
...a
};
else b[a.key] = { ...a
};
return b;
}, {}))
console.log(fkeys)
console.log(intersections)
How to check if array contains different values with React.js and typescript?
Example:
[{
name: 'John',
value: 1,
}, {
name: 'John',
value: 1,
}, {
name: 'Carla',
value: 15,
}]
I want to return false if all objects in array are same, and true if there is at least one different object.
You can't use a direct equality comparison since objects will never return equal.
Ie {} != {}, and {name: 'John', value: 1} != {name: 'John', value: 1}.
So firstly you have to decide what you're going to define as 'equal' for these objects.
Let's say for the sake of this that you use just the name field as the test for equality. So if two objects in the array have the same name field, then you'll call them equal. Then you'd define the function:
type NameValue = {name: string, value: string}
const areEqual = (obj1: NameValue, obj2: NameValue): boolean => obj1.name === obj2.name
Of course you can change this function to reflect whatever you define as 'equal'. There are npm packages to help you with deep equality checks too, or you can JSON.stringify both and check that equality
Then you can use Array.some(). Array.some() will return true if any element in the array passes a test. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
Testing if any element is not equal to the first should be sufficient.
const areNotAllEqual = yourArray.some((currentElement) => {
return !areEqual(currentElement, yourArray[0])
})
After having commented on and criticized especially the approaches based on JSON.stringify, I want to contribute something on that matter. Since meanwhile all modern JS engines seem to be aware of an object's key order (in how this object was created) and also seem to guarantee such an order for key-iteration one could write a recursive function, which for any deeply nested but JSON-conform JS-objects reestablishes a normalized key-order for such objects but leaves arrays untouched.
Passing such key-normalized objects to JSON.stringify then makes such objects comparable by their's stringified signature ...
function defaultCompare(a, b) {
return ((a < b) && -1) || ((a > b) && 1) || 0;
}
function comparePropertyNames(a, b) {
return a.localeCompare
? a.localeCompare(b)
: defaultCompare(a, b);
}
function getJsonDataWithNormalizedKeyOrder(data) {
let value;
if (Array.isArray(data)) {
value = data.map(getJsonDataWithNormalizedKeyOrder);
} else if (data && (typeof data === 'object')) {
value = Object
.getOwnPropertyNames(data)
.sort(comparePropertyNames)
.reduce((obj, key) => {
obj[key] = getJsonDataWithNormalizedKeyOrder(data[key])
return obj;
}, {});
} else {
value = data;
}
return value;
}
const objA = {
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 = {
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 = {
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: 2,
};
console.log(
'getJsonDataWithNormalizedKeyOrder(objA) ...',
getJsonDataWithNormalizedKeyOrder(objA)
);
console.log(
'getJsonDataWithNormalizedKeyOrder(objB) ...',
getJsonDataWithNormalizedKeyOrder(objB)
);
console.log(
'JSON.stringify(getJsonDataWithNormalizedKeyOrder(objA)) ...',
JSON.stringify(getJsonDataWithNormalizedKeyOrder(objA))
);
console.log(
'JSON.stringify(getJsonDataWithNormalizedKeyOrder(objB)) ...',
JSON.stringify(getJsonDataWithNormalizedKeyOrder(objB))
);
console.log(
'JSON.stringify(getJsonDataWithNormalizedKeyOrder(objC)) ...',
JSON.stringify(getJsonDataWithNormalizedKeyOrder(objC))
);
console.log(
'JSON.stringify(getJsonDataWithNormalizedKeyOrder(objA)).length ...',
JSON.stringify(getJsonDataWithNormalizedKeyOrder(objA)).length
);
console.log(
'JSON.stringify(getJsonDataWithNormalizedKeyOrder(objB)).length ...',
JSON.stringify(getJsonDataWithNormalizedKeyOrder(objB)).length
);
console.log(
'JSON.stringify(getJsonDataWithNormalizedKeyOrder(objC)).length ...',
JSON.stringify(getJsonDataWithNormalizedKeyOrder(objC)).length
);
console.log(`
JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objA)
) === JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objB)
) ?`,
JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objA)
) === JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objB)
)
);
console.log(`
JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objA)
) === JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objC)
) ?`,
JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objA)
) === JSON.stringify(
getJsonDataWithNormalizedKeyOrder(objC)
)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Applying the above to an approach which solves the OP's original problem in a more generic way then might look similar to the next provided lines ...
function defaultCompare(a, b) {
return ((a < b) && -1) || ((a > b) && 1) || 0;
}
function comparePropertyNames(a, b) {
return a.localeCompare
? a.localeCompare(b)
: defaultCompare(a, b);
}
function getJsonDataWithNormalizedKeyOrder(data) {
let value;
if (Array.isArray(data)) {
value = data.map(getJsonDataWithNormalizedKeyOrder);
} else if (data && (typeof data === 'object')) {
value = Object
.getOwnPropertyNames(data)
.sort(comparePropertyNames)
.reduce((obj, key) => {
obj[key] = getJsonDataWithNormalizedKeyOrder(data[key])
return obj;
}, {});
} else {
value = data;
}
return value;
}
const sampleList = [{
name: 'John',
value: 1,
}, {
value: 1,
name: 'John',
}, {
name: 'Carla',
value: 15,
}];
function hasDifferentValues(arr) {
// stringified first item reference.
const referenceItem = JSON.stringify(getJsonDataWithNormalizedKeyOrder(arr[0]));
// run `some` from a sub-array which excludes the original array's first item.
return arr.slice(1).some(item =>
referenceItem !== JSON.stringify(getJsonDataWithNormalizedKeyOrder(item))
);
}
console.log(
'hasDifferentValues(sampleList) ?',
hasDifferentValues(sampleList)
);
console.log(
'hasDifferentValues(sampleList.slice(0,2)) ?',
hasDifferentValues(sampleList.slice(0,2))
);
console.log(
'hasDifferentValues(sampleList.slice(0,1)) ?',
hasDifferentValues(sampleList.slice(0,1))
);
console.log(
'hasDifferentValues(sampleList.slice(1)) ?',
hasDifferentValues(sampleList.slice(1))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
I'd check whether stringified object array includes stringified item by referencing to a copied array where I remove the latest item. I'll use Array.every() to compare if all the items match together and then return the opposite value.
However, this can be very heavy operation if an object array is very lengthy
const arrSame = [{name: 1}, {name: 1}, {name: 1}];
const arrDiff = [{name:1}, {name: 2}, {name: 2}];
const arrDiff2 = [{name:1}, {name: 1}, {name: 2}];
const hasDifferentValues = (arr) => !arr.every((item, i, ref) => JSON.stringify([...ref].shift()).includes(JSON.stringify(item)));
console.log(hasDifferentValues(arrSame));
console.log(hasDifferentValues(arrDiff));
console.log(hasDifferentValues(arrDiff2));
This is not exactly react specific, but to check for differences you can iterate through the array using every like so.
const fooArray = [{
name: 'John',
value: 1,
nest: {
isValid: [1, 2]
}
},
{
value: 1,
name: 'John',
nest: {
isValid: [1, 1]
}
}, {
name: 'John',
value: 1,
nest: {
isValid: [1, 1]
}
}
]
// check each member against the last, see if there is a diff
const isSame = (element, index, arr) => {
if (index > 0) {
// https://stackoverflow.com/questions/1068834/object-comparison-in-javascript
// return JSON.stringify(element) === JSON.stringify(arr[index - 1])
// alternatively, you can check to see if some of the values are different
// by stringifying and checking if either are permuations of each other
// this is probably not the ideal way, but I added it for the sake of a different solution
const currentObStr = JSON.stringify(element).split("").sort().join()
const prevObStr = JSON.stringify(arr[index - 1]).split("").sort().join()
return currentObStr === prevObStr
}
return true
}
const everyElementIsSame = fooArray.every(isSame)
console.log(everyElementIsSame)
Giving an array (fixed length) of objects with the following structures:
{type: 'A', value: 1}
or
{type: 'B', text: 'b'}
What is the easiest way to find all the sequences of objects of type 'A' and return their indices?
An example:
For the following array:
[
{type: 'A', value: 1}, {type: 'A', value: 2}, {type: 'B', text: 'b1'},
{type: 'A', value: 11}, {type: 'A', value: 12}, {type: 'A', value: 13},
{type: 'B', text: 'b2'}, {type: 'A', value: 10}, {type: 'B', text: 'b3'}
]
The output should be the following array:
[
{startIndex: 0, startValue: 1, length: 2},
{startIndex: 3, startValue: 11, length: 3},
{startIndex: 7, startValue: 10, length: 1},
]
I guess the naive implementation would be to iterate with forEach and have many complex conditions but is there a simpler technique?
Thanks.
You could use reduce like this. Add a variable prev to keep track of what the previous type was. If the current type is the type you are looking for: Add an object if the previous item had a different type. Else, just increment the length property
let input = [{type:'A',value:1},{type:'A',value:2},{type:'B',text:'b1'},{type:'A',value:11},{type:'A',value:12},{type:'A',value:13},{type:'B',text:'b2'},{type:'A',value:10},{type:'B',text:'b3'}],
prev;
const output = input.reduce((acc, { type, value }, i) => {
if (type === 'A') {
if (prev !== type) {
acc.push({ startIndex: i, startValue: value, length: 1 })
} else {
acc[acc.length - 1].length++
}
}
prev = type
return acc;
}, [])
console.log(output)
You could reduce the array and chweck the value and the type for creating a new group.
var array = [{ type: 'A', value: 1 }, { type: 'A', value: 2 }, { type: 'B', text: 'b1' }, { type: 'A', value: 11 }, { type: 'A', value: 12 }, { type: 'A', value: 13 }, { type: 'B', text: 'b2' }, { type: 'A', value: 10 }, { type: 'B', text: 'b3' }],
result = array.reduce((r, { type, value }, i, a) => {
var previous = a[i - 1] || {},
last = r[r.length - 1];
if (!isFinite(value) || type !== 'A') return r;
if (previous.type !== type) {
r.push({ startIndex: i, startValue: value, length: 1 });
return r;
}
if (value === a[last.startIndex].value + last.length) last.length++;
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Just use a forEach loop. its simple to read and doesnt complicate your code.
let arr = [
{type: 'A', value: 1}, {type: 'A', value: 2}, {type: 'B', text: 'b1'},
{type: 'A', value: 11}, {type: 'A', value: 12}, {type: 'A', value: 13},
{type: 'B', text: 'b2'}, {type: 'A', value: 10}, {type: 'B', text: 'b3'}
]
function findSequences(arr, character) {
let seq = []
let seqStarted = false;
let seqI = 0;
arr.forEach((el, i) => {
if (el.type == character) {
if (seqStarted == true) {
seq[seqI].length += 1;
} else {
seqStarted = true;
seq.push({
startIndex: i,
startValue: el.value,
length: 1
});
}
} else {
if (seqStarted) {
seqStarted = false;
seqI++;
}
}
})
return seq;
}
console.log(findSequences(arr, 'A'))
I don't think it can get any simpler that just iterating over the array using a for loop and constructing your desired answer. Also, this solution has linear complexity, as you're traversing the array just once. Not sure why you'd need "many complex conditions" though.
Something like this seems pretty OK to me:
const arr = [
{type: 'A', value: 1}, {type: 'A', value: 2}, {type: 'B', text: 'b1'},
{type: 'A', value: 11}, {type: 'A', value: 12}, {type: 'A', value: 13},
{type: 'B', text: 'b2'}, {type: 'A', value: 10}, {type: 'B', text: 'b3'}
];
const result = [];
for (let [index, el] of arr.entries()) {
if (el.type === 'A') {
// account for the first entry found
if (result.length === 0) {
result.push({startIndex: index, length: 1, startValue: el.value});
} else {
const lastSequence = result[result.length - 1];
// check if the we are in a sequence
if (lastSequence.startIndex + lastSequence.length === index) {
lastSequence.length += 1;
} else {
// if we are not in a sequence - create a new one
result.push({startIndex: index, length: 1, startValue: el.value});
}
}
}
}
console.log(result);
For your specified array:
var arr =[
{type: 'A', value: 1}, {type: 'A', value: 2}, {type: 'B', text: 'b1'},
{type: 'A', value: 11}, {type: 'A', value: 12}, {type: 'A', value: 13},
{type: 'B', text: 'b2'}, {type: 'A', value: 10}, {type: 'B', text: 'b3'}
];
arr.reduce((result, current, index) => {
if(current.type == 'A'){
if(result.length == 0 || index - result[result.length - 1].length != result[result.length - 1]. startIndex){
result.push({startIndex: index, startValue: current.value, length: 1})
}
else{
result[result.length - 1].length++
}
}
return result;
}, [])
You can use declarative approach something like this instead imperative with forEach: You can use declarative approach something like this instead imperative with forEach:
const a = [
{type: 'A', value: 1}, {type: 'A', value: 2}, {type: 'B', text: 'b1'},
{type: 'A', value: 11}, {type: 'A', value: 12}, {type: 'A', value: 13},
{type: 'B', text: 'b2'}, {type: 'A', value: 10}, {type: 'B', text: 'b3'}
];
var filtered = a.map((v,i) => {
return v.type == 'A' ? {startIndex: i,startValue: v.value} : null
}).filter(x => x).reduce((acc,v) => {
if (acc.filter(x => x.startIndex + x.length == v.startIndex).length > 0){
acc[acc.length-1].length ++;
} else {
v.length = 1;
acc.push(v);
}
return acc;
},[]);
console.log(filtered);
Though there are already lots of good answers here, I thought I'd add one which addresses a more general case of segmenting a list. With this code, it's possible to specify a segmenter function, which compares two items and determines whether a new segment should be started.
Once you have these segments, getting to the final answer you require is quite simple.
const data = [
{type: 'A', value: 1}, {type: 'A', value: 2}, {type: 'B', text: 'b1'},
{type: 'A', value: 11}, {type: 'A', value: 12}, {type: 'A', value: 13},
{type: 'B', text: 'b2'}, {type: 'A', value: 10}, {type: 'B', text: 'b3'}
]
const segmentBy = segmenter => items => {
const segmentReducer = (prev = [], curr) => {
let lastSegment = [];
let lastItem = null;
try {
lastSegment = prev[prev.length - 1];
lastItem = lastSegment[lastSegment.length - 1];
} catch (e) {
return [...prev, [curr]];
}
const requiresNewSegment = segmenter(lastItem, curr);
if (requiresNewSegment) {
return [...prev, [curr]];
}
return [...prev.slice(0, prev.length - 1), [...lastSegment, curr]];
};
return items.reduce(segmentReducer, []);
};
const addIndex = a => a.map((x, i) => ({...x, index: i}))
const segmentByType = segmentBy((a, b) => a.type !== b.type);
const segments = segmentByType(addIndex(data));
const result = segments
.map(segment => ({
startIndex: segment[0].index,
startValue: segment[0].value || null,
length: segment.length
}))
.filter(x => x.startValue !== null)
console.dir(result);
So below is the array of objects and I'm trying to filter only the objects which have type:y. As you see the array of objects can also contain nested array of same schema and it can be N-level nested. So please help me with most optimized ES6 function to achieve this.
[
{
name: 'a',
type: 'x',
array:
[
{
name: 'l',
type: 'y',
},
{
name: 'm',
type: 'y',
},
{
name: 'n',
type: 'x',
array:
[
{
name: 'x',
type: 'y',
},
{
name: 'y',
type: 'y',
},
{
name: 'z',
type: 'x',
}
]
}
]
},
{
name: 'b',
type: 'y',
},
{
name: 'c',
type: 'y',
},
]
Also below is the code which I have written to achieve this. looking to improve the performance and optimize it
filterFunction(fields) {
const result = [];
(fields || []).forEach((field) => {
if (field.array) {
const x = field;
x.array = this.filterFunction(field.array);
result.push(x);
} else if (field.type !== 'x') {
result.push(field);
}
});
return result;
}
You could filter the array by using Object.assign for new objects with deeper filtered arrays.
function filter(array, type) {
return array.reduce((r, o) => {
var array = filter(o.array || [], type);
if (o.type === type || array.length) {
r.push(Object.assign({}, o, { array }));
}
return r;
}, []);
}
var array = [{ name: 'a', type: 'x', array: [{ name: 'l', type: 'y', }, { name: 'm', type: 'y', }, { name: 'n', type: 'x', array: [{ name: 'x', type: 'y', }, { name: 'y', type: 'y', }, { name: 'z', type: 'x', }] }] }, { name: 'b', type: 'y', }, { name: 'c', type: 'y' }];
console.log(filter(array, 'y'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You may use a fold like pattern:
function walkRec(input, accumulator, state = []) {
if (!(input instanceof Array)) input = [input];
for (const item of input) {
state = accumulator(item, state);
const isObject = item !== null && typeof item == 'object';
if (isObject && 'array' in item)
state = walk(item.array, accumulator, state);
}
return state;
}
if the tree you have to filter is big (lot of subtree) you may get a "Maximum call stack size exceeded" Error and you will probably prefer this kind of iteration:
function walk(input, accumulator, state = []) {
if (!(input instanceof Array)) input = [input];
while (input.length > 0) {
const item = input.shift();
state = accumulator(item, state);
const isObject = item !== null && typeof item == 'object';
if (isObject && 'array' in item) {
const children = item.array instanceof Array ? item.array : [item.array];
Array.prototype.push.apply(input, children);
}
}
return state;
}
and use it as following
walk(array, (item, state) => {
if (item && item.type == 'y')
state.push(item)
return state;
})