Traversing through indented arrays in javascript - javascript

I have a javascript object as follows :
let hogwartsHeirarchy = {
Headmaster: [
{
name: "Professor Dumbledore",
key: 1,
Headmistress: [
{
name: "Minerva McGonagall",
key: 2,
StandByProfessor: [
{
name: "Rubeus Hagrid",
subject: "Potions Master",
key: 3,
Professor: [
{ name: "Horace Slughorn", key: 4 },
{ name: "Severus Snape", key: 4 },
],
},
{
name: "Alastor Moody",
subject: "Defense Against the Dark Arts",
key: 3,
Professor: [
{ name: "Remus Lupin", key: 4 },
{ name: "Gilderoy Lockhart", key: 4 },
],
},
],
},
],
},
],
};
I want to print/get each of the node value [headmaster, headmastress,..] and their corresponding child values. I tried various methods like looping through the array using a for loop, recurse etc, but unfortunately I am not able to get any value out of the nodes. Please help.
e.g : I used this :
printArray(hogwartsHeirarchy);
function printArray(arr){
for(var i = 0; i < arr.length; i++){
if(arr[i] instanceof Array){
console.log("true: ");
console.log("intermediate one : ",arr[i]);
printArray(arr[i]);
}else{
console.log("final one : ",arr[i]);
}
}
}
The values can be shown in this format:
Headmaster - name : Professor Dumbledore, key : 1
.
.
StandByProfessor - name : Robeus Hagrid, subject : Potion Master, key : 3
StandByProfessor - name : Alastor Moody, subject : Defence against the dark arts, key : 3
.
.
Professor - ...
Professor - ...
Professor - ...
Professor - ...

I would suggest restructuring so that the subordinates are always accessed with the same key, and thus can be visited very simply. I also made it so every node is a person, there's no object at the top that isn't a person. I left the variable name, but it now refers to Dumbledore directly.
let hogwartsHeirarchy =
{
name: "Professor Dumbledore",
role: "Headmaster",
key: 1,
subordinates: [
{
name: "Minerva McGonagall",
role: "Headmistress",
key: 2,
subordinates: [
{
name: "Rubeus Hagrid",
role: "StandByProfessor",
subject: "Potions Master",
key: 3,
subordinates: [
{ name: "Horace Slughorn", key: 4, role: "Professor" },
{ name: "Severus Snape", key: 4, role: "Professor" },
],
},
{
name: "Alastor Moody",
role: "StandByProfessor",
subject: "Defense Against the Dark Arts",
key: 3,
subordinates: [
{ name: "Remus Lupin", key: 4, role: "Professor" },
{ name: "Gilderoy Lockhart", key: 4, role: "Professor" },
],
},
],
},
],
};
function visitStaff(staffMember) {
if (staffMember.subordinates) {
for (const subordinate of staffMember.subordinates) {
visitStaff(subordinate);
}
}
console.log("Staff member:", staffMember);
}
visitStaff(hogwartsHeirarchy);
When setting up a data structure, it's important to think about how it will be accessed, and what are its defining parts. In this case, there are people, which are the nodes, and (subordinate) relationships, which are the edges of the graph.
In your original code, you had an object { Headmaster: [...] } — what does it represent? is it a person? no; is it a relationship? kind of, but no. It defines something about Dumbledoor, that he's the Headmaster, but not who or what he's the Headmaster of. So it's really just describing the role/job title of Dumbledoor, so it makes more sense as a property of the person. It's redundant as an object.
It helps to align your objects so they all represent something. You should be able to describe what each object and array is.

Given the data structure:
a) I assume that there will be only one array in each type of "title", and
b) that array will contain a list of objects of a similar structure as it's parent
It's possible ...
to use for..of in order to
iterate through each key on an object, and add them to a string. Because there are arrays that contains objects,
I can loop through them, and
do a recursive loop, by having the method calling itself.
const hogwartsHierarchy = { Headmaster: [{ name: "Professor Dumbledore", key: 1, Headmistress: [{ name: "Minerva McGonagall", key: 2, StandByProfessor: [{ name: "Rubeus Hagrid", subject: "Potions Master", key: 3, Professor: [{ name: "Horace Slughorn", key: 4 }, { name: "Severus Snape", key: 4 }] }, { name: "Alastor Moody", subject: "Defense Against the Dark Arts", key: 3, Professor: [{ name: "Remus Lupin", key: 4 }, { name: "Gilderoy Lockhart", key: 4 }] }] }] }] };
function printAllWithChilds(obj, prevProp) {
let listItem = (prevProp) ? ' -- ' + prevProp : '';
for (const property in obj) { // 1
if (obj[property] instanceof Array) {
obj[property].forEach((child_obj) => { // 3
listItem += printAllWithChilds(child_obj, property); // 4
});
} else {
listItem += `, ${property}: ${obj[property]}`; // 2
}
}
return listItem;
}
let listStr = printAllWithChilds(hogwartsHierarchy);
console.log(listStr);
I would honestly split up hogwartsHierarchy into smaller bits, following a kind of database structure, where primary_key is unique for each individual. These arrays doesn't make much sense, until you look at the variable professors and how their respectively belongs_to key corresponds to the standbyprofessors, where you can see that "Horace Slughorn" belongs to "Rubeus Hagrid".
const headermaster = {
name: "Professor Dumbledore",
primary_key: 1
};
const headmistress = {
name: "Minerva McGonagall",
primary_key: 2,
belongs_to: 1
};
const standbyprofessors = [{
name: "Rubeus Hagrid",
subject: "Potions Master",
primary_key: 3,
belongs_to: 2
},
{
name: "Alastor Moody",
subject: "Defense Against the Dark Arts",
primary_key: 4,
belongs_to: 2
}
];
const professors = [{
name: "Horace Slughorn",
primary_key: 5,
belongs_to: 3
},
{
name: "Severus Snape",
primary_key: 6,
belongs_to: 3
},
{
name: "Remus Lupin",
primary_key: 7,
belongs_to: 4
},
{
name: "Gilderoy Lockhart",
primary_key: 8,
belongs_to: 4
},
];

You could remove known keys from the object and get the type hierarchy then iterate the property and return the tupel of type, name, subject and key only if type exists.
const
getValues = (object, type) => [
...(type ? [`${type} - name : ${object.name}, ${object.subject ? `subject : ${object.subject}, ` : ''}key : ${object.key}`] : []),
...Object
.keys(object)
.filter(k => !['name', 'subject', 'key'].includes(k))
.flatMap(k => object[k].flatMap(o => getValues(o, k)))
],
hogwartsHierarchy = { Headmaster: [{ name: "Professor Dumbledore", key: 1, Headmistress: [{ name: "Minerva McGonagall", key: 2, StandByProfessor: [{ name: "Rubeus Hagrid", subject: "Potions Master", key: 3, Professor: [{ name: "Horace Slughorn", key: 4 }, { name: "Severus Snape", key: 4 }] }, { name: "Alastor Moody", subject: "Defense Against the Dark Arts", key: 3, Professor: [{ name: "Remus Lupin", key: 4 }, { name: "Gilderoy Lockhart", key: 4 }] }] }] }] };
console.log(getValues(hogwartsHierarchy));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Based on a 1j01 object model:
const hogwartsHeirarchy =
{ name: "Professor Dumbledore", role: "Headmaster", key: 1,
subordinates: [{ name: "Minerva McGonagall", role: "Headmistress", key: 2,
subordinates: [
{ name: "Rubeus Hagrid", role: "StandByProfessor", subject: "Potions Master", key: 3,
subordinates: [
{ name: "Horace Slughorn", key: 4, role: "Professor" },
{ name: "Severus Snape", key: 4, role: "Professor" }]},
{ name: "Alastor Moody", role: "StandByProfessor", subject: "Defense Against the Dark Arts", key: 3,
subordinates: [
{ name: "Remus Lupin", key: 4, role: "Professor" },
{ name: "Gilderoy Lockhart", key: 4, role: "Professor" }]}]}]};
const visitStaff = (staffMember) => {
const iter = (obj) => {
const { name, role, key, subordinates } = obj;
const subject = obj.subject ? `, subject : ${obj.subject}` : '';
const line = `${role} - name : ${name}${subject}, key : ${key}`;
const sublines = (Array.isArray(subordinates)) ? subordinates.flatMap(iter) : [];
return [line, ...sublines];
}
return iter(staffMember).join('\n');
}
console.log(visitStaff(hogwartsHeirarchy));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

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; }

How do I check that the correct objects are being returned via a function (expect function returns [Function Anonymous])?

I have a function:
const sort =
(pets,attribute) =>
_(pets)
.filter(pets=> _.get(pets, attribute) !== null)
.groupBy(attribute)
.value()
Some data:
const pets= [{
id: 1,
name: 'snowy',
},
{
id: 2,
name: 'quacky',
},
{
id: 3,
name: 'snowy',
age: 5,
},
{
id: null,
name: null,
age: null
}
]
const attribute = 'name'
I am currently trying to write some Jest unit tests for this, that tests if the function returns the correct resultant object after being sorted based off an attribute.
The result of:
sort(pets,attribute) is something like this:
{
snowy: [ { id: 1, name: 'snowy' }, { id: 3, name: 'snowy', age: 5} ],
quacky: [ { id: 2, name: 'quacky' } ]
}
Is there a way I can do a expect to match the two objects snowy and quacky here?
The thing I want to test for is that the objects are being correctly grouped by the key.
I've tried using something like
const res = sort(users,key)
expect(res).toEqual(
expect.arrayContaining([
expect.objectContaining({'snowy' : [ { id: 1, name: 'snowy' }, { id: 3, name: 'snowy', age: 5 } ]},
expect.objectContaining({'quacky' : [ { id: 2, name: 'quacky' } ]}))
])
)
which doesn't seem to work, the received output seems to output:
Expected: ArrayContaining [ObjectContaining {"snowy": [{"id": 1, "name": "snowy"}, {"age": 5, "id": 3, "name": "snowy"}]}]
Received: [Function anonymous]
I am unsure what the best method to test this kind of function is either so advice on that would be appreciated.
If this is what your arrangeBy() returns:
{
snowy: [ { id: 1, name: 'snowy' }, { id: 3, name: 'snowy', age: 5} ],
quacky: [ { id: 2, name: 'quacky' } ]
}
Then you can just do:
const expected = {
snowy: [ { id: 1, name: 'snowy' }, { id: 3, name: 'snowy', age: 5} ],
quacky: [ { id: 2, name: 'quacky' } ]
}
const res = arrangeBy(users,key)
expect(res).toEqual(expected)
But looking at your Error message I guess you have something else mixed up. In the beginning you listed the implementation of a sort function which seems to not be used in the test. Where is arrangeBy coming from now.
Please provide more code examples.

How to create an array with some properties from an inner JSON array in javascript

So I have :
list =
{
id: 1,
arr: [
{index : 1 , description: "lol" , author: "Arthur"},
{index : 2 , description: "sdadsa" , author: "Bob"},
{index : 3 , description: "loasd" , author: "Mackenzie"}
]
}
and I want to create an array only with description and author property from the arr array.
I tried var a = {l : list.arr.map(x => {x.description,x.author})}. But all items from the array are undefined .
Another approach would be to use the rest parameter. This way you can remove the index and keep everything else intact.
var list = {
id: 1,
arr: [
{ index: 1, description: "lol", author: "Arthur" },
{ index: 2, description: "sdadsa", author: "Bob" },
{ index: 3, description: "loasd", author: "Mackenzie" },
],
};
var a = list.arr.map(({index, ...rest}) => rest);
console.log(a);
You almost finish, you should define the key in your return object in the map funciton.
var list = {
id: 1,
arr: [
{ index: 1, description: "lol", author: "Arthur" },
{ index: 2, description: "sdadsa", author: "Bob" },
{ index: 3, description: "loasd", author: "Mackenzie" },
],
};
var a = list.arr.map(x => ({
description: x.description,
author: x.author,
}));
console.log(a);
list = {
id: 1,
arr: [{
index: 1,
description: "lol",
author: "Arthur"
},
{
index: 2,
description: "sdadsa",
author: "Bob"
},
{
index: 3,
description: "loasd",
author: "Mackenzie"
}
]
}
var a = {
l: list.arr.map(x => ({
"description": x.description,
"author": x.author
}))
}
console.log(a);
You just forget to define keys in return object.
var list = {
id: 1,
arr: [
{ index: 1, description: "lol", author: "Arthur" },
{ index: 2, description: "sdadsa", author: "Bob" },
{ index: 3, description: "loasd", author: "Mackenzie" },
],
};
var a = l.arr.map(x => ({
description: x.description,
author: x.author,
}));

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

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; }

Sort array by showing first the items common to another array

I am loading users with their skills:
let users: Observable<UserModel[]> this.userService.getOpen().pipe(
map((payload: Payload<UserResponse[]>) =>
payload.result.map((response: UserResponse) => {
return {
userId: response.userId,
skillsNames: response.skills.map((skill: SkillModel) => skill.name)
};
})));
I need to sort the skillsNames showing first the ones that are common to a project.
To get the skills of a project I am doing the following:
let skills: Observable<SkillModel[]> = this.skillService.getByProjectId(id).pipe(
map((payload: Payload<SkillResponse[]>) =>
payload.result.map((response: SkillResponse) => {
return {
skillId: response.skillId,
name: response.name
};
})));
I can also do the matching between user's SkillModel and project's SkillModel by skillId but getting the names in the end.
How to do this?
Update
I am adding an example to better explain what I am looking for:
let users = [
{
userId: 1,
skills: [
{ Id: 1, name: 'Javascript' },
{ Id: 2, name: 'CSS' }
]
},
{
userId: 2,
skills: [
{ Id: 1, name: 'Javascript' },
{ Id: 2, name: 'CSS' }
{ Id: 3, name: 'C#' }
]
},
];
let project = {
projectId: 1,
skills: [
{ Id: 2, name: 'CSS' },
{ Id: 3, name: 'C#' }
]
},
So I would like to reorder each user skills so that the skills that also appear on project skills would appear first. So the users would become:
let users = [
{
userId: 1,
skills: [
{ Id: 2, name: 'CSS' },
{ Id: 1, name: 'Javascript' }
]
},
{
userId: 2,
skills: [
{ Id: 2, name: 'CSS' }
{ Id: 3, name: 'C#' }
{ Id: 1, name: 'Javascript' }
]
},
];
See how in both cases the skills that appear on project are moved up in each User.
I not really sure if I could understand, but I did this sample that could fit exactly what you need:
This code just sort all users based on how many skills they match with the project skills (array);
const users = [
{
userId: 1,
skillsNames: ['JAVA', 'PHP', 'RUBY', 'R']
},
{
userId: 2,
skillsNames: ['LISP', 'TYPESCRIPT', 'C', 'DOTNET']
},
{
userId: 3,
skillsNames: ['C#', 'C++', 'VRAPTOR', 'R']
},
{
userId: 4,
skillsNames: ['LISP', 'RUBY', 'R', 'DOTNET']
},
];
const skills = ['JAVA', 'PHP', 'RUBY', 'R'];
const userWithSortedSkills = users.map(u => {
const sortedSkillsName = u.skillsNames.sort(name => {
return skills.lastIndexOf(name) ? 1 : -1;
});
u.skillsNames = sortedSkillsName;
return u;
});
console.log(userWithSortedSkills);

Categories

Resources