how to group objects in array that are pointing at eact other - javascript

I have an array of objects, each object is pointing at another object in the "related" field.
I need to group the object with the related field so I can output them in groups on my website.
how can I group something like this
[
{
id: 1,
name: "dog",
related: "cat",
},
{
id: 2,
name: "cat",
related: "dog",
},
{
id: 3,
name: "shark",
related: "whale",
},
];
to this ?
[
[
{
id: 1,
name: "dog",
related: "cat",
},
{
id: 2,
name: "cat",
related: "dog",
},
],
[
{
id: 3,
name: "shark",
related: "whale",
},
],
];

Here i've tried this. It'll only work for two way binding relations.
const data = [
{
id: 1,
name: "dog",
related: "cat",
},
{
id: 2,
name: "cat",
related: "dog",
},
{
id: 3,
name: "shark",
related: "whale",
},
{
id: 4,
name: "whale",
related: "shark",
},
{
id: 5,
name: "test",
related: "none",
},
];
const categoriesData = (data) => {
const dataObj = data.reduce((obj, item) => {
if (!obj[item.name]) obj[item.related] = [item];
else if (obj[item.name] && obj[item.name][0].name === item.related)
obj[item.name].push(item);
return obj;
}, {});
return Object.values(dataObj);
};
console.log(categoriesData(data));

You can reduce it:
var data=[ { id: 1, name: "dog", related: "cat", }, { id: 2, name: "cat", related: "dog", }, { id: 3, name: "shark", related: "whale", }];
var result = Object.values(data.reduce((acc, elem)=>{
const key = [elem.name, elem.related].sort((a,b)=>a.localeCompare(b)).join('|');
acc[key] = [...(acc[key] || []), elem];
return acc;
},{}));
console.log(result);

this doesn't exactly answer your question, but is more of an "outside the box" solution. your data would be much simpler, straight forward and easy to manage if you simply add a "category" field.
[
{
id: 1,
name: "dog",
related: "cat",
category: 1,
},
{
id: 2,
name: "cat",
related: "dog",
category: 1,
},
{
id: 3,
name: "shark",
related: "whale",
category: 2,
},
{
id: 3,
name: "whale",
related: "shark",
category: 2,
},
];

One way of doing this would be to create a Map holding the animals. Then for each animal in this collection (that is not removed) create a group and remove it from the collection. Find related animals and add them to the same group, also removing them from the collection.
const animals = [
{ id: 1, name: "dog", related: "cat" },
{ id: 2, name: "cat", related: "dog" },
{ id: 3, name: "shark", related: "whale" },
];
const animalsMap = new Map(animals.map(animal => [animal.name, animal]));
const animalGroups = [];
for (let [,animal] of animalsMap) {
const group = Array.of(animal);
while (
animalsMap.delete(animal.name),
animal = animalsMap.get(animal.related)
) {
group.push(animal);
}
animalGroups.push(group);
}
console.log(animalGroups);

You could gather all relations first, then build groups of related values and then map the groups by filtering the data.
This approach works with longer chains of related objects.
function getGroups(k, group) {
if (seen.has(k)) return;
seen.add(k);
group.push(k);
relations[k].forEach(v => seen.has(v) || getGroups(v, group));
}
var data = [{ id: 1, name: "dog", related: "cat" }, { id: 2, name: "cat", related: "dog" }, { id: 3, name: "shark", related: "whale" }],
relations = {},
groups = [],
seen = new Set,
result;
data.forEach(({ name, related }) => {
if (!relations[name]) relations[name] = [];
relations[name].push(related);
if (!relations[related]) relations[related] = [];
relations[related].push(name);
});
Object.keys(relations).forEach(k => {
let group = [];
getGroups(k, group);
if (group.length) groups.push(group);
});
result = groups.map(group => data.filter(({ name }) => group.includes(name)));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

Filter out array using another array of objects

I am having two arrays
const selected = [];
const current = [
{ id: 1, name: "abc" },
{ id: 2, name: "def" }
];
const result = []
I need to compare these two arrays and the result should only have the single entry instead of duplicates. In the above example result should have the following output.
Also items in the selected should be taken into consideration and should be in the beginning of the result
result = [
{ id: 1, name: "abc" },
{ id: 2, name: "def" }
];
Also when the input is following
const selected = [ {id:5, name: "xyz" }];
const current = [
{ id: 1, name: "abc" },
{ id: 2, name: "def" }
];
result = [[
{ id: 5, name: "xyz" },
{ id: 1, name: "abc" },
{ id: 2, name: "def" }
];
Also when the input is following
const selected = [ {id:1, name: "abc" }, {id:4, name: "lmn" }];
const current = [
{ id: 1, name: "abc" },
{ id: 2, name: "def" }
];
result = [[
{ id: 1, name: "abc" },
{ id: 4, name: "lmn" }
{ id: 2, name: "def" }
];
Note the comparison should be made using name field
Code that I tried
const res = [...(selected || [])].filter((s) =>
current.find((c) => s.name === c.name)
);
Sandbox: https://codesandbox.io/s/nervous-shannon-j1vn5k?file=/src/index.js:115-206
You could get all items and filter the array by checking the name with a Set.
const
filterBy = (key, s = new Set) => o => !s.has(o[key]) && s.add(o[key]),
selected = [{ id: 1, name: "abc" }, { id: 1, name: "lmn" }],
current = [{ id: 1, name: "abc" }, { id: 2, name: "def" }],
result = [...selected, ...current].filter(filterBy('name'));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Loop through selected, and if there is no object in current with a name that matches the name of the object in the current iteration push it into current.
const selected=[{id:1,name:"abc"},{id:6,name:"def"},{id:4,name:"lmn"}];
const current=[{id:1,name:"abc"},{id:2,name:"def"}];
for (const sel of selected) {
const found = current.find(cur => cur.name === sel.name);
if (!found) current.push(sel);
}
console.log(current);
This is a good use for .reduce, avoids multiple loops/finds and doesn't need filtering with side-effects.
const selected = [ {id:1, name: "abc" }, {id:4, name: "lmn" }];
const current = [
{ id: 1, name: "abc" },
{ id: 2, name: "def" }
];
const result = Object.values(
[...selected, ...current].reduce((obj, item) => {
obj[item.name] = obj[item.name] || item;
return obj;
}, {})
)
console.log(result);

Group array of objects by multiple nested values

I have an array of objects which presents tasks. These tasks are categorized (primary / secondary category).
let tasks = [
{
id: 1,
name: 'Cleanup desk',
primary_category: {
id: 1,
name: 'Indoor'
},
secondary_category: {
id: 2,
name: 'Surfaces'
}
},
{
id: 2,
name: 'Cleanup office floors',
primary_category: {
id: 1,
name: 'Indoor'
},
secondary_category: {
id: 3,
name: 'Ground'
}
},
{
id: 3,
name: 'Water plants',
primary_category: {
id: 2,
name: 'Outdoor'
},
secondary_category: {
id: 3,
name: 'Irrigation'
}
}
];
I now try to create a categories accordion in my frontend and therefore need to group my array differently. The structure should look like:
1) primary category
> secondary category
> tasks
> secondary category
> tasks
2) primary category
> secondary category
> tasks
Therefore I'm trying to achieve a structure similar to this:
let tasks_categorized = [
{
id: 1,
name: 'Indoor',
secondary_categories: [
{
id: 2,
name: 'Surfaces',
tasks: [
{
id: 1,
name: 'Cleanup desk'
}
]
},
{
id: 3,
name: 'Ground',
tasks: [
{
id: 2,
name: 'Cleanup office floors'
}
]
}
]
},
{
id: 2,
name: 'Outdoor',
secondary_categories: [
{
id: 3,
name: 'Irrigation',
tasks: [
{
id: 3,
name: 'Water plants'
}
]
}
]
}
];
I tried using groupBy by lodash but this does not allow grouping by multiple nested key-value pairs. Does anybody know an approach to solve this?
Thank you in advance!
The following provided approach is going to achieve the expected result within a single reduce cycle without any further nested loops.
It does so by implementing a reducer function which creates and/or aggregates at time a prioritized category task while iterating another task array. But most importantly it keeps track of a task item's related primary and secondary categories via a Map based lookup. This lookup reference together with a result array are properties of this function's return value which has to be partly provided as the reduce method's initial value as follows ... { result: [] }.
function createAndAggregatePrioritizedCategoryTask(
{ lookup = new Map, result }, item
) {
const { primary_category, secondary_category, ...taskRest } = item;
const { id: primaryId, name: primaryName } = primary_category;
const { id: secondaryId, name: secondaryName } = secondary_category;
const primaryKey = [primaryId, primaryName].join('###');
const secondaryKey = [primaryKey, secondaryId, secondaryName].join('###');
let primaryCategory = lookup.get(primaryKey);
if (!primaryCategory) {
// create new primary category item.
primaryCategory = {
id: primaryId,
name: primaryName,
secondary_categories: [],
};
// store newly created primary category reference in `lookup`.
lookup.set(primaryKey, primaryCategory);
// push newly created primary category reference to `result`.
result.push(primaryCategory);
}
let secondaryCategory = lookup.get(secondaryKey);
if (!secondaryCategory) {
// create new secondary category item.
secondaryCategory = {
id: secondaryId,
name: secondaryName,
tasks: [],
};
// store newly created secondary category reference in `lookup`.
lookup.set(secondaryKey, secondaryCategory);
// push newly created secondary category reference into the
// `secondary_categories` array of its related primary category.
primaryCategory
.secondary_categories
.push(secondaryCategory);
}
// push the currently processed task-item's rest-data as
// item into the related secondary category's `task` array.
secondaryCategory
.tasks
.push(taskRest);
return { lookup, result };
}
let tasks = [{
id: 1,
name: 'Cleanup desk',
primary_category: { id: 1, name: 'Indoor' },
secondary_category: { id: 2, name: 'Surfaces' },
}, {
id: 2,
name: 'Cleanup office floors',
primary_category: { id: 1, name: 'Indoor' },
secondary_category: { id: 3, name: 'Ground' },
}, {
id: 3,
name: 'Water plants',
primary_category: { id: 2, name: 'Outdoor' },
secondary_category: { id: 3, name: 'Irrigation' },
}];
const { result: tasks_categorized } = tasks
.reduce(createAndAggregatePrioritizedCategoryTask, { result: [] });
console.log({ tasks_categorized });
.as-console-wrapper { min-height: 100%!important; top: 0; }
You could take a dynamic approach with an array of arrays with functions and keys for the nested arrays.
const
tasks = [{ id: 1, name: 'Cleanup desk', primary_category: { id: 1, name: 'Indoor' }, secondary_category: { id: 2, name: 'Surfaces' } }, { id: 2, name: 'Cleanup office floors', primary_category: { id: 1, name: 'Indoor' }, secondary_category: { id: 3, name: 'Ground' } }, { id: 3, name: 'Water plants', primary_category: { id: 2, name: 'Outdoor' }, secondary_category: { id: 3, name: 'Irrigation' } }],
groups = [
[o => o, 'primary category'],
[o => o.primary_category, 'secondary category'],
[o => o.secondary_category, 'tasks']
],
result = tasks.reduce((r, o) => {
groups.reduce((parent, [fn, children]) => {
const { id, name } = fn(o);
let item = (parent[children] ??= []).find(q => q.id === id)
if (!item) parent[children].push(item = { id, name });
return item;
}, r);
return r;
}, {})[groups[0][1]];
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Can I sort an array of objects based on a array of subarrays?

I would ask a question regarding sorting.
Let's say I have an array of objects:
let arrayToBeSorted = [
{
name:"name1",
id:"id1",
},
{
name:"name2",
id:"id2",
},
{
name:"name3",
id:"id3",
},
{
name:"name4",
id:"id4",
},
{
name:"name5",
id:"id5",
},
{
name:"name6",
id:"id6",
}];
And Let's say I have an array of sub arrays which each one is containing IDs string like that:
let sortArray = [["id2", "id1"], ["id5"], ["id6","id3","id4"]]
What I want to do is to sort the arrayToBeSorted based on the sortArray preserving each subarrays (in order to maintain an hermetic order)
This is the wanted result:
arrayToBeSorted = [
{
name:"name2",
id:"id2",
},
{
name:"name1",
id:"id1",
},
{
name:"name5",
id:"id5",
},
{
name:"name6",
id:"id6",
},
{
name:"name3",
id:"id3",
},
{
name:"name4",
id:"id4",
}];
EDIT: I tried to:
arrayToBeSorted.sort((a,b)=> for(var i=0; i<sortArray.length;i++)
{
sortArr.indexOf(a.item.id) - sortArr.indexOf(b.item.id)
});
I also thought of sorting by each array and the concat the sorted result, but no success...
Thanks!
You seem to be overcomplicating the sort operation here. Use sort() on arrayToBeSorted and get the indexOf each array element in a flat()tened copy of sortArray:
let arrayToBeSorted = [{
name: "name1",
id: "id1",
}, {
name: "name2",
id: "id2",
}, {
name: "name3",
id: "id3",
}, {
name: "name4",
id: "id4",
}, {
name: "name5",
id: "id5",
}, {
name: "name6",
id: "id6",
}];
let sortArray = [
["id2", "id1"],
["id5"],
["id6", "id3", "id4"]
];
console.log(arrayToBeSorted.sort((a, b) => sortArray.flat().indexOf(a.id) - sortArray.flat().indexOf(b.id)));
You could flat the array and build an object with wanted order and sort the array.
const
data = [{ name: "name1", id: "id1" }, { name: "name2", id: "id2" }, { name: "name3", id: "id3" }, { name: "name4", id: "id4" }, { name: "name5", id: "id5" }, { name: "name6", id: "id6" }],
sortArray = [["id2", "id1"], ["id5"], ["id6", "id3", "id4"]],
order = Object.fromEntries(sortArray.flat().map((k, i) => [k, i + 1]));
data.sort((a, b) => order[a.id] - order[b.id]);
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use sort() based on a flattened sortArray using findIndex() or indexOf() as #esqew. You could also go a step further and pre-process sortArray and create an object ids as keys and indices of sortArray as values. Then the sort function would be based on the object as follows:
let arrayToBeSorted = [{
name: "name1",
id: "id1",
},
{
name: "name2",
id: "id2",
},
{
name: "name3",
id: "id3",
},
{
name: "name4",
id: "id4",
},
{
name: "name5",
id: "id5",
},
{
name: "name6",
id: "id6",
}
];
let sortArray = [["id2", "id1"], ["id5"], ["id6","id3","id4"]];
const flatO = Object.fromEntries( sortArray.flat().map((id,i) => [id,i]) );
const sortedArray = arrayToBeSorted.sort((a,b) => flatO[a.id] - flatO[b.id]);
console.log( sortedArray );
NOTE: This is equivalent to #NinaScholz's solution. Saw it just after I posted this. I have upvoted both #NinaScholz's and #esqew's solutions but I would take #NinaScholz's since the flat() method including the creation of the order object execute JUST ONCE.

Javascript compare two JSON arrays and return key of the unmatched value

I have two JSON arrays, would like to know the key which don't match. I don't need the value.
Example:
livetable: [
{ id: 1, name: "Sandra" },
{ id: 2, name: "John" },
],
backupTable: [
{ id: 1, name: "Sandra" },
{ id: 2, name: "Peter" },
],
I can get the key/value pair which is diffrent with this Lodash script:
difference = _.differenceWith(livetable,backupTable,_.isEqual)
But I would just need the key, in this example "name" for "id: 2" is not matching, so I would need to get the "name" key to new array/variable.
(Using VUE CLI)
EDIT: Added example of current code output.
var livetable = [{"id": 1, "name": "Sandra", "id": 2, "name": "John"}]
var backupTable = [{"id": 1, "name": "Sandra", "id": 2, "name": "Peter"}]
console.log(_.differenceWith(backupTable,livetable,_.isEqual))
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
This will output the key:value pair, but I would just need the key which is diffrent.
I think I understand what you're trying to do. There are some unknowns though, like what should happen if there is a missing record in the second data set?
This solution assumes each table of data has the same amount of records and the records have the same IDs.
// define data
const livetable = [
{ id: 1, name: "Sandra" },
{ id: 2, name: "John" }
]
const backupTable = [
{ id: 1, name: "Sandra" },
{ id: 2, name: "Peter" }
]
const getDifferentRecordsByID = (sourceRecords, compareRecords) => {
// simple utility function to return a record object matching by ID
const findComparisionRecord = id => compareRecords.find(compareRecord => compareRecord.id === id)
// using the utility function, we can filter out any mismatching records by comparing name
return sourceRecords
.filter(sourceRecord => sourceRecord.name !== findComparisionRecord(sourceRecord.id).name)
// then map over all the records and just pull out the ID
.map(record => record.id)
}
console.log(getDifferentRecordsByID(livetable, backupTable)) // [2]
Here is working VUE code for my problem.
Function returns [ "name" ], which is exactly what I need.
data() {
return {
livetable: [{ id: 1, name: "Sandra" },{ id: 2, name: "John" }],
backupTable: [{ id: 1, name: "Sandra" },{ id: 2, name: "Peter" }],
difColumns: null,
};
},
methods: {
test3() {
let resultArray = []
this.livetable.forEach((array1, index) => {
const array2 = this.backupTable[index];
resultArray.push(this._.reduce(array1, (result, value, key) => this._.isEqual(value, array2[key]) ? result : result.concat(key), []))
});
this.difColumns = resultArray[0]
}
},

Return array of objects (from array of objects)

I have the following data and I want to return an array (of objects) of years that are distinct.
I tried the following function but I'm getting an array within an array.
const data = [{
id: 1,
name: "test1",
years: [{
id: 1,
name: "year1"
}, {
id: 2,
name: "year2"
}]
},
{
id: 2,
name: "test2",
years: [{
id: 1,
name: "year1"
}]
},
]
let years = data.map((s) => {
return s.years
})
let distinctYears = Array.from(new Set(years.map(c => c.id))).map(id => {
return {
id: id,
name: years.find(c => c.id === id).name,
}
})
console.log(distinctYears);
desired outcome:
[
{id: 1, name: "year1"},
{id: 2, name: "year2"}
]
Since s.years() is an array, and data.map() returns an array of the results, years is necessarily an array of arrays.
Instead of using .map(), use .reduce() to concatenate them.
const data = [{
id: 1,
name: "test1",
years: [{
id: 1,
name: "year1"
}, {
id: 2,
name: "year2"
}]
},
{
id: 2,
name: "test2",
years: [{
id: 1,
name: "year1"
}]
},
];
const years = data.reduce((a, {
years
}) => a.concat(years), []);
let distinctYears = Array.from(new Set(years.map(c => c.id))).map(id => {
return {
id: id,
name: years.find(c => c.id === id).name,
}
});
console.log(distinctYears);
There's so many ways you can go about doing this. Here's one, it's not a one-liner but its broken down to parts to help us understand whats going on.
Your dataset:
let data =
[
{
id: 1,
name: "test1",
years: [{id: 1, name: "year1"}, {id: 2, name: "year2"} ]
},
{
id: 2,
name: "test2",
years: [{id: 1, name: "year1"} ]
},
]
Use .flatMap() to create a one-level array with all items:
let allItems = data.flatMap((item) => {
return item.years.map((year) => {
return year
})
})
Getting distinct items:
let distinct = []
allItems.forEach((item) => {
let matchingItem = distinct.find((match) => match.id == item.id && match.name == item.name)
if(!matchingItem){
distinct.push(item)
}
})
In Practice:
let data = [{
id: 1,
name: "test1",
years: [{
id: 1,
name: "year1"
}, {
id: 2,
name: "year2"
}]
},
{
id: 2,
name: "test2",
years: [{
id: 1,
name: "year1"
}]
},
]
let allItems = data.flatMap((item) => {
return item.years.map((year) => {
return year
})
})
let distinct = []
allItems.forEach((item) => {
let matchingItem = distinct.find((match) => match.id == item.id && match.name == item.name)
if (!matchingItem) {
distinct.push(item)
}
})
console.log(distinct)

Categories

Resources