I'm trying to transform a flat list of persons into a structured tree of ancestry.
The source array of persons looks like this:
const list = [
{
id: 1,
name: 'John',
akin: true,
motherId: undefined,
fatherId: undefined,
partnerIds: [2]
},
{
id: 2,
name: 'Maria',
akin: false,
motherId: undefined,
fatherId: undefined,
partnerIds: [1]
},
{
id: 3,
name: 'Steven',
akin: true,
fatherId: 1,
motherId: 2,
partnerIds: [4, 5]
},
{
id: 4,
name: 'Stella',
akin: false,
motherId: undefined,
fatherId: undefined,
partnerIds: [3]
},
{
id: 5,
name: 'Laura',
akin: false,
motherId: undefined,
fatherId: undefined,
partnerIds: [3]
},
{
id: 5,
name: 'Solomon',
akin: true,
motherId: 4,
fatherId: 3,
partnerIds: []
},
{
id: 6,
name: 'Henry',
akin: true,
fatherId: 3,
motherId: 5,
partnerIds: []
}
]
It can contain n generations of people whose direct ancestors are defined by their respective fatherId and motherId. Unknown parents (oldest known ancestor, or related only by partnership) are simply undefined.
Partnerships are indicated by an array of partnerIds.
The expected output should look like this:
const pedigree = [
{
id: 1,
name: 'John',
partnerships: [
{
partner: {
id: 2,
name: 'Maria',
},
children: [
{
id: 3,
name: 'Steven',
partnerships: [
{
partner: {
id: 4,
name: 'Stella',
},
children: [
{
id: 5,
name: 'Solomon'
}
]
},
{
partner: {
id: 5,
name: 'Laura',
},
children: [
{
id: 6,
name: 'Henry',
}
]
}
]
}
]
}
]
}
]
Visually the result would look like this:
Visual pedigree
The desired output format is not intended for storing, but for easier visualization and processing for later rendering.
I tried to loop over the flat list, create a hashTable for referencing the single persons and then find partners and common children.
My issue is though that my approach only works for two generations, or one level of nesting, although I need it to be suitable for n generations.
I think I need some recursive function or way of starting to loop up from the bottom of ancestry somehow, but I can't figure out a smart way.
I'd be glad for any suggestions or tips!
EDIT:
This is what I've tried:
const createPedigree = (dataset) => {
const hashTable = Object.create(null)
dataset.forEach(
(person) => (hashTable[person.id] = { ...person, partnerships: [] })
)
const dataTree = []
dataset.forEach((person) => {
if (person.akin) {
if (person.partnerIds.length) {
person.partnerIds.forEach((partnerId) => {
hashTable[person.id].partnerships.push({
partner: { ...dataset.find((p) => p.id === partnerId) },
children: []
})
})
}
}
dataTree.push(hashTable[person.id])
})
dataset.forEach((child) => {
// fill partnerships with children
if (child.fatherId && child.motherId) {
if (
hashTable[child.fatherId].akin &&
hashTable[child.fatherId].partnerships.length
) {
let mother = hashTable[child.fatherId].partnerships.find(
(partnership) => {
return partnership.partner.id === child.motherId
}
)
mother.children.push(child)
} else if (hashTable[child.motherId].akin) {
let father = hashTable[child.motherId].partnerships.find(
(partnership) => {
return partnership.partner.id === child.fatherId
}
)
father.children.push(child)
}
}
})
return dataTree
}
You are correct in the assumption that a general solution will involve some recursive calls (or a queue of candidates to expand until the queue is empty).
The output structure levels alternate between:
a person with partnerships
partnerships that contain a partner and children (each child is then again a 1.)
To make things simpler we can just model the 2 steps above with 2 separate functions. I chose the names expandPerson and expandPartnership.
const expandPerson = (personId, dataset) => {
// we get the person from the dataset by their id
const personData = dataset.find(p => p.id == personId)
// we clone only the data that we want in the output
const person = { id: personData.id, name: personData.name }
// all partnerIds of this person need to become their parnerships
// so we just map them to an "expanded partnership" (step 2.)
person.partnerships = personData.partnerIds
.map(partnerId => expandPartnership(partnerId, person.id, dataset))
// we return the "expanded" person
return person
}
const expandPartnership = (partner1Id, partner2Id, dataset) => {
// we get the partner from the dataset by their id
const partnerData = dataset.find(p => p.id == partner1Id)
// we clone only the data that we want in the output
const partner = { id: partnerData.id, name: partnerData.name }
// all people in the dataset, whose parents are partner1Id
// and pertner2Id are the children
const children = dataset
.filter(p => p.motherId == partner1Id && p.fatherId == partner2Id
|| p.motherId == partner2Id && p.fatherId == partner1Id)
// we map each child as an "expanded person" again (back to step 1.)
.map(p => expandPerson(p.id, dataset))
// we return the "expanded" partnership
return { partner, children }
}
In the code you then just call const pedigree = expandPerson(1, list)
If the root is not always id: 1 just find the root id first
const rootId = list.find(p => p.akin && !p.fatherId && !p.motherId).id
const pedigree = expandPerson(rootId, list)
Note: you have a duplicate id (id: 5) in the provided input. You have to fix that.
Related
What I want to do is basically join the information from two arrays via userId. Until then, this solution that I made works only when there is little data, if they are very large arrays, this huge amount of filter is very impractical. Can anyone think of a more efficient solution?
PS: I'm using > 0 ? because sometimes one of the properties is empty.
const data01 = [
{ userId: 0, motorcycles: 'motorcycle01', cars: 'car01' },
{ userId: 1, motorcycles: 'motorcycle02', cars: 'car02' },
{ userId: 2, cars: 'car03' },
{ userId: 3, motorcycles: 'motorcycle04' },
]
items.forEach(
a =>
(
a.motorcylces = data01.filter(b => b.userId === a.userId).length > 0 ? data01.filter(b => b.userId === a.userId)[0].motorcylces : null,
a.cars = data01.filter(b => b.userId === a.userId).length > 0 ? data01.filter(b => b.userId === a.userId)[0].cars : null
)
);
console.log(items)
Expected Output:
[
{
...
motorcycles: 'motorcycle01',
cars: 'cars01'
},
{
...
motorcycles: 'motorcycle01',
cars: 'cars01'
}
]
You can speed up the process by creating a Map of data01, keyed by userId.
And with Object.assign you can copy the properties from a match. This will not create the property if it doesn't exist in the source, so there will be no null assignments (unless the source has an explicit null):
let map = new Map(data01.map(o => [o.userId, o]));
items.forEach(a => Object.assign(a, map.get(a.userId)));
If you are only interested in a selection of properties, then create objects that only have those properties:
let map = new Map(data01.map(o =>
[o.userId, { cars: o.cars, motorcycles: o.motorcycles }]
));
items.forEach(a => Object.assign(a, map.get(a.userId)));
This second solution will always create the specific properties, also when they didn't exist yet. In that case their values will be undefined.
If your arrays are arrays of objects, and you need to consolidate 2+ arrays based on some property in the object, seems like the best thing to do would be to make an intermediate map that has its keys as the userIDs, and then just code something that will non-destructively update the map as you iterate through the arrays.
const data01 = [
{ userId: 0, motocycles: 'motocycle01', cars: 'car01' },
{ userId: 1, motocycles: 'motocycle02', cars: 'car02' },
{ userId: 2, cars: 'car03' },
{ userId: 3, motocycles: 'motocycle04' },
]
const data02 = [
{ userId: 0, dogs: 'doggy', cats: 'car01' },
{ userId: 1, dogs: 'doggo', cats: 'car02' },
{ userId: 2, dogs: 'sheperd' },
{ userId: 3, cats: 'kitty' },
]
function combineArrFromUserId(arr1,arr2){
const idMap= new Map()
data01.forEach(item=>checkAndAdd(item,idMap))
data02.forEach(item=>checkAndAdd(item,idMap))
return idMap.values()
}
function checkAndAdd(item,map){
const current = map.get(item.userId)
if(current){
map.set(item.userId,Object.assign(current,item))
} else {
map.set(item.userId, item)
}
}
console.log(combineArrFromUserId(data01,data02))
THE PROBLEM:
I have an array of Objects. And a currentObject, that I currently am viewing. I want to get the value from a Property that comes from the Array of Objects, if 2 other properties match.
Here is the Array, simplified:
ARRAY = [{
id: 1,
metadata: {author: "Company1"}
},
{
id: 2,
metadata: {author: "Company2"}
}
Here is the Object, simplified:
OBJECT = {
name: "Something
templateId: 2
}
So, basically, I want to return, the metdata.author information, if the ARRAY.id, matches the OBJECT.templateId..
Here is the code I wrote..
const getAuthorInfo = (connTemplates: ARRAY[], currentConn: ITEM_OBJECT) => {
connTemplates.find((connTemplate: ARRAY_ITEM_OBJECT) => connTemplate.id === currentConn.templateId);
};
console.log('Author Info:', connection); // This though returns the OBJECT, not the ARRAY_ITEM
Any ideas, on how to make this work? I tried to filter as well, with the same condition, but that returned undefined, when I called it in my ReactComponent.
is this what you need?
const arr = [{
id: 1,
metadata: { author: "Company1" }
},
{
id: 2,
metadata: { author: "Company2" }
}]
const obj = {
name: "Something",
templateId: 2
}
function getAuthorInfo(arr, obj) {
const arrItem = arr.find(item => item.id === obj.templateId)
return arrItem.metadata.author
}
console.log(getAuthorInfo(arr, obj))
You are on the right path:
const result = arr.find(f => f.id == obj.templateId).metadata.author;
const arr = [{
id: 1,
metadata: {author: "Company1"}
},
{
id: 2,
metadata: {author: "Company2"}
}]
const obj = {
name: "Something",
templateId: 2
}
const result = arr.find(f => f.id == obj.templateId);
console.log(result);
I am basically trying to convert a flat json file to tree view. Here the parent child relationship required for tree view is mentained by links key using source and target.
Here is the sample raw input:
{
"nodes" : [
{
name: "bz_db",
index: 0
},
{
name: "mysql",
index: 1
},
{
name: "postgres",
index: 2
},
{
name: "it-infra",
index: 3
},
{
name: "user-count",
index: 4
}
],
links: [
{
source: 0, target: 1
},
{
source: 0, target: 3
},
{
source: 1, target: 3
},
{
source: 3, target: 4
}
]
}
As you can see the link field maintains this relation ship, and finally I want my data in this format:
{
name: "bz_db",
children: [
{
name: "mysql",
children: [
{
name: "it-infra",
children: [
{
name: "user_count",
children: []
}
]
}
]
},
{
name: "it-infra",
children: [{
name: "user_count",
children: []
}
]
}
]
}
I tried to solve this, but it worked for 1 level (to show immediate child of a selected root element.
var findObjectByKeyValue = function(arrayOfObject, key, value){
return _.find(arrayOfObject, function(o){ return o[key] == value})
}
var rootObject = findObjectByKeyValue(sample_raw_input.nodes, 'name', 'bz_db');
var treeObject = {
name: rootObject.name,
index: rootObject.index,
root: true,
children: []
};
angular.forEach(dependencyData.links, function(eachLink){
if(treeObject.index == eachLink.source){
var rawChildObject = findObjectByKeyValue(dependencyData.nodes, 'index', eachLink.target);
var childObject = {};
childObject.index = rawChildObject.index;
childObject.name = rawChildObject.name;
childObject.children = [];
treeObject.children.push(childObject);
}
});
But the above code returns me only first level of depndencies, but i want hierarchical relationship.
I know i can use recursion here. But I am not so comfortable with it.
Josh's answer uses a sequence of map->filter->map->find calls, each of which iterate thru a collection of data. This loop of loop of loop of loops results in a stunning amount of computational complexity as the number of nodes in your collection increases.
You can dramatically simplify the creation of the tree by using a single reduce pass on each nodes and links. Map can also perform look-ups in logarithmic time, compared to Array's find which requires linear time (slower). When you consider this operation is called for each element of the input, it's clear to see a significant difference in time.
const makeTree = (nodes = [], links = []) =>
links.reduce
( (t, l) =>
t.set ( l.source
, MutableNode.push ( t.get (l.source)
, t.get (l.target)
)
)
, nodes.reduce
( (t, n) => t.set (n.index, MutableNode (n.name))
, new Map
)
)
.get (0)
Lastly, we provide the MutableNode interface we relied upon
const MutableNode = (name, children = []) =>
({ name, children })
MutableNode.push = (node, child) =>
(node.children.push (child), node)
Below is a full program demonstration. JSON.stringify is used only to display the result
const MutableNode = (name, children = []) =>
({ name, children })
MutableNode.push = (node, child) =>
(node.children.push (child), node)
const makeTree = (nodes = [], links = []) =>
links.reduce
( (t, l) =>
t.set ( l.source
, MutableNode.push ( t.get (l.source)
, t.get (l.target)
)
)
, nodes.reduce
( (t, n) => t.set (n.index, MutableNode (n.name))
, new Map
)
)
.get (0)
const data =
{ nodes:
[ { name: "bz_db", index: 0 }
, { name: "mysql", index: 1 }
, { name: "postgres", index: 2 }
, { name: "it-infra", index: 3 }
, { name: "user-count", index: 4 }
]
, links:
[ { source: 0, target: 1 }
, { source: 0, target: 3 }
, { source: 1, target: 3 }
, { source: 3, target: 4 }
]
}
const tree =
makeTree (data.nodes, data.links)
console.log (JSON.stringify (tree, null, 2))
You can rely on tracking an object reference and do this without any recursion. Using Object.assign, map the list of nodes to its children:
// Assuming that input is in `input`
const nodes = input.nodes.reduce((a, node) => {
a[node.index] = { ...node, index: undefined };
return a;
}, []);
// organize the links by their source
const links = input.links.reduce((a, link) => {
return a.set((a.get(link.source) || []).concat(nodes[link.target]);
}, new Map());
// Apply side effect of updating node children
nodes.forEach(node => Object.assign(node, {
children: links.get(node.index),
}));
So I'm taking the list of nodes, and assigning to each (to mutate the node itself -- keep in mind this is a side-effect) a new array. Those children are all the links that link this node, and we Array#map them to convert their target ID into the actual node we want.
just share sample, a little different from yours.
But it give you a hint with recursive function.
jsFiddle flat array json transform to recursive tree json
function getNestedChildren(arr, parent) {
var out = []
for(var i in arr) {
if(arr[i].parent == parent) {
var children = getNestedChildren(arr, arr[i].id)
if(children.length) {
arr[i].children = children
}
out.push(arr[i])
}
}
return out
}
var flat = [
{id: 1, title: 'hello', parent: 0},
{id: 2, title: 'hello', parent: 0},
{id: 3, title: 'hello', parent: 1},
{id: 4, title: 'hello', parent: 3},
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4},
{id: 7, title: 'hello', parent: 3},
{id: 8, title: 'hello', parent: 2}
]
var nested = getNestedChildren(flat, 0)
console.log(nested)
const List = Immutable.List;
const items = [
{ id: 1, subList: [] },
{ id: 2, subList: [] },
{ id: 3, subList: [] }
];
const newItem = { name: "sublist item" };
let collection = List(items);
collection = collection.updateIn([0, 'subList'], function (items) {
return items.concat(newItem)
});
https://jsbin.com/midimupire/edit?html,js,console
Results in:
Error: invalid keyPath
I think that perhaps I need to set subList as a List(); I get the same error when trying this.
If I understand the question correctly, you want to return collection with the first element as:
{
id : 1,
subList: [
{name: "sublist item"}
]
}
To do this we'll need to make a few changes.
Use Immutable.fromJS to deeply convert the plain JS array of objects to an Immutable List of Maps
Use List.update() to return a new List with the updated value
Use Map.updateIn() to return a new LMapist with the updated value
Here's the whole thing:
const List = Immutable.List;
const items = [{
id: 1,
subList: []
},
{
id: 2,
subList: []
},
{
id: 3,
subList: []
}
];
const newItem = {
name: "sublist item"
};
let collection = Immutable.fromJS(items);
collection = collection.update(0, item => {
return item.updateIn(['subList'], subList => {
return subList.concat(newItem);
});
});
console.log(collection)
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.js"></script>
And the result:
[
{
"id": 1,
"subList": [
{
"name": "sublist item"
}
]
},
{
"id": 2,
"subList": []
},
{
"id": 3,
"subList": []
}
]
Update: List.updateIn() can use an index as the keypath, so you can simplify this to the following:
collection = collection.updateIn([0, 'subList'], subList => {
return subList.concat(newItem);
});
Like this:
const List = Immutable.List;
const items = [{
id: 1,
subList: []
},
{
id: 2,
subList: []
},
{
id: 3,
subList: []
}
];
const newItem = {
name: "sublist item"
};
let collection = Immutable.fromJS(items);
collection = collection.updateIn([0, 'subList'], subList => {
return subList.concat(newItem);
});
console.log(collection)
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.js"></script>
Use the object you got, update the subArray and return the whole object.
const List = Immutable.List;
const items = [
{ id: 1, subList: [] },
{ id: 2, subList: [] },
{id: 3, subList: [] }
];
const newItem = { name: "sublist item" };
let collection = List(items);
collection = collection.update([0], function (obj) {
obj.subList = obj.subList.concat(newItem)
return obj;
});
This doesn’t work because the elements of your Immutable.List are plain-old JavaScript objects (POJO), not Immutable.Maps, so updateIn doesn’t know how to work with them. You can either:
Make the objects Immutable.Maps by using Immutable.fromJS instead of Immutable.List as the constructor to convert the entire object graph to Immutable objects. (See JS Bin)
Use update([0]) instead of updateIn to just get the POJO and mutate it (as in #Navjot’s answer).
dontMutateMeArray=[1,2,3,3,3,4,5];
toBeRemoved=3;
newArray=dontMutateMeArray.something(toBeRemoved); // [1,2,3,3,4,5]
iDontWantArray=dontMutateMeArray.filter(value=>value===toBeRemoved); // [1,2,4,5]
I indeed need it for array of objects too. And I specifically need to remove the last added object (ie. the one with higher index in the array). Something like:
dontMutateMeArray=[{id:1},{id:2},{id:3,sth:1},{id:3,sth:42},{id:3,sth:5},{id:4},{id:5}];
toBeRemoved=3;
newArray=dontMutateMeArray.something(toBeRemoved); // [{id:1},{id:2},{id:3,sth:1},{id:3,sth:42},{id:4},{id:5}]
iDontWantArray=dontMutateMeArray.filter(obj=>obj.id===toBeRemoved); // [{id:1},{id:2},{id:4},{id:5}]
iDontWantArray2=dontMutateMeArray.blahBlah(toBeRemoved); // [{id:1},{id:2},{id:3,sth:1},{id:3,sth:5},{id:4},{id:5}]
You could iterate from right and check with a closure.
var dontMutateMeArray = [{ id: 1 }, { id: 2 }, { id: 3, sth: 1 }, { id: 3, sth: 42 }, { id: 3, sth: 5 }, { id: 4 }, { id: 5 }],
toBeRemoved = 3,
newArray = dontMutateMeArray.reduceRight((found => (r, a) => (!found && a.id === toBeRemoved ? found = true : r.unshift(a), r))(false), []);
console.log(newArray);