I'm trying to achieve some specific use case but come to a dead end.
I need to, given an flat array of objects like this (Copying it from another similar post, as I found several similar posts but none matching my use case, or at least I haven't been smart enough to realise how to tweak possible solutions to fit my use-case):
const myArr = [
{
id: '1',
parentId: '0',
},
{
id: '2',
parentId: '1',
},
{
id: '3',
parentId: '2',
},
{
id: '4',
parentId: '2',
},
{
id: '5',
parentId: '2',
},
{
id: '6',
parentId: '2',
},
{
id: '7',
parentId: '6',
},
{
id: '8',
parentId: '7',
}
]
And then I have another array of IDs like so:
const idArr = [2, 4, 8]
So I need to filter from the first array, elements with matching ID, i.e the element with ID 2, then ID 4 and ID 8
And then, for each element present in the filtered array, I need to find it's ancestry until reach the root level, then build a tree
The problem here is that I already achieved it, but in real life this array will be huge, with thousands of elements in it, and the code will run most likely a lot of times
So I am looking for the technically most performant possible solution:
I'd say building a tree recursively is pretty much done in a performant way, but somehow I am in a dead end with step 2, getting all the ancestry of certain elements.
Could anyone bring some light here?
Thanks a lot in advance
const myArr = [
{
id: '1',
parentId: '0',
},
{
id: '2',
parentId: '1',
},
{
id: '3',
parentId: '2',
},
{
id: '4',
parentId: '2',
},
{
id: '5',
parentId: '2',
},
{
id: '6',
parentId: '2',
},
{
id: '7',
parentId: '6',
},
{
id: '8',
parentId: '7',
}
]
const idArr = [2, 4, 8]
elObj = {}
for (const el of myArr) {
elObj[el.id] = {"parentId": el.parentId}
}
const getAncestory = (id, res) => {
if (elObj[id].parentId === '0') {
res.push('0')
return res
}
res.push(elObj[id].parentId)
return getAncestory(elObj[id].parentId, res)
}
const res = []
idArr.forEach(el => {
res.push(getAncestory(el.toString(), [el.toString()]))
})
console.log(elObj)
console.log(res)
Here's how the above code works and performs in terms of time complexity:
Creating an object of objects where elements can be accessed in constant time based on their ids is a linear operation, both time and space-wise. We could have skipped this step if there was a guarantee that say element with id i is at index i of the initial array.
Creating each ancestry list takes O(m) time where m is the distance of the initial element to the root element of the tree. Note that we have assumed that all elements are eventually connected to the root element (our base case of parentId === '0'). If this assumption is not correct, we need to amend the code for that.
Assuming that there are n elements that you need to build the ancestry lists for (length of idArr), this whole process takes O(n * m), since the operations are all constant.
This algorithm can deteriorate into a quadratic one in terms of the number of nodes in the tree in case the tree has the shape of a flat linked list and you want the ancestry of all of its elements. That's because we would need to list 1 + 2 + ... n-1 + n elements where the closest element to the root takes 1 step and the farthest away takes n steps. This leads to n * (n+1)/2 steps which is O(n^2) in Big O terms.
One way to amend it is to change the representation of the tree with parent to child pointers. Then we can start from the root and backtrack, traversing all the possible paths and saving those of interest. This approach could be beneficial or worse the proposed one depending on data and the exact requirements for the output.
Note: If you have a few thousands of objects and are looking for the ancestry of a few hundreds of them, the above approach is fine (I'm making a lot of assumptions about the data). To make an educated guess one needs more details about the data and requirements.
It's not entirely clear what you're trying to generate. This answer makes the guess that you want a tree that includes only the nodes whose ids are specified, as well as their ancestors. It returns a structure like this:
[
{id: "1", children: [
{id: "2", children: [
{id: "4", children: []},
{id: "6", children: [
{id: "7", children: [
{id: "8", children: []}
]}
]}
]}
]}
]
Note that this is not a tree but a forest. I see nothing to demonstrate that every lineage ends up at a single root node, so the output might have multiple roots. But if your data does enforce the single root, you can just take the first element.
This code will do that:
const unfold = (fn, init, res = []) =>
fn (init, (x, s) => unfold (fn, s, res .concat (x)), () => res)
const filterByIds = (xs, map = Object .fromEntries(xs .map (({id, parentId}) => [id, parentId]))) => (
ids,
selected = [...new Set (ids .map (String) .flatMap (
id => unfold ((i, next, stop) => i in map ? next (i, map [i]) : stop(), id)
))]
) => xs .filter (({id}) => selected .includes (id))
const makeForest = (xs, root = 0) =>
xs .filter (({parentId}) => parentId == root)
.map (({id, parentId, ...rest}) => ({
id,
...rest,
children: makeForest (xs, id)
})) // [0] /* if the data really forms a tree */
const extractForest = (xs, ids) =>
makeForest (filterByIds (xs) (ids))
const myArr = [{id: "1", parentId: "0"}, {id: "2", parentId: "1"}, {id: "3", parentId: "2"}, {id: "4", parentId: "2"}, {id: "5", parentId: "2"}, {id: "6", parentId: "2"}, {id: "7", parentId: "6"}, {id: "8", parentId: "7"}]
console .log (
extractForest (myArr, [2, 4, 8])
)
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with the somewhat interesting unfold helper function. This lets you start with a seed value and turn it into an array of values by repeatedly calling the function you supply with the current seed and two function, one to pass along a new value and the next seed, the other to stop processing and return the list of values returned so far. We use this to track the lineage of each id. This is by no means the only way we could have done so. A while loop is probably more familiar, but it's a useful tool, and it doesn't involve any reassignment or mutable variables, which I really appreciate.
(An example of how unfold works might be a simple Fibonacci number generator:
const fibsTo = (n) =>
unfold (([a, b], next, stop) => b <= n ? next (b, [b, a + b]) : stop (), [0, 1])
fibsTo (100) //=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
This calls next with the next Fibonnaci number and a new seed including the current value and the one which would come next, starting with the seed [0, 1] When the total passes our target number, we instead call stop.)
Next we have the function filterByIds that takes your input array, and returns a function that accepts a list of ids and filters the array to include just those elements which are in the ancestry of one of those ids. We do this in three steps. First, we create an Object (map) mapping the ids of our input values to those actual values. Second, we flatmap the ids with a function to retrieve the list of their ancestors; this uses our unfold above, but could be rewritten with a while loop. And we use [... new Set (/* above */)] to collect the unique values from this list. Third, we filter the original list to include only the elements whose ids are in this new list (selected.)
The function makeForest -- like unfold is fairly generic, taking a flat list of {id, parentId, ...more} nodes and nesting them recursively in an {id, ...more, children: []} structure. Uncomment the [0] in the last line if your data is singly rooted.
And finally we have our main function extractForest which calls makeForest pm the result of filterByIds.
I would like to stress that unfold and makeForest are both quite generic. So the custom code here is mostly filterByIds, and the simple extractForest wrapper.
Related
I have two array:
for example:
arraySelectedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
arraySavedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
now I need to check if there is some item in arraySavedItems that is not present in arraySelectedItems, and in this case I'll go to populate another array called arrayDeletedItems.
If the two arrays have the same items I don't need to populate the arrayDeletedItems.
So I have tried with this code:
arraySavedItems.filter((itemSaved) => !arraySelectedItems.find((itemSel) => {
if (itemSaved.id !== itemSel.id) {
arrayDeletedItems.push(itemSaved)
}
}
))
So with this data:
arraySelectedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
arraySavedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
I'll expect that arrayDeletedItems will be:
arrayDeletedItems = []
Instead whit this data for example:
arraySelectedItems = [{id: 1, name: "item1"}]
arraySavedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
I'll expect that arrayDeletedItems will be:
arrayDeletedItems = [{id: 2, name: "item2"}]
With my code I receive and arrayDeletedItems that has the all values:
arrayDeletedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}]
Consider this generic function:
function difference(a, b, keyFn) {
let keys = new Set(a.map(keyFn))
return b.filter(obj => !keys.has(keyFn(obj)))
}
//
selectedItems = [{id: 1, name: "item1"}, {id:4}]
savedItems = [{id: 1, name: "item1"}, {id: 2, name: "item2"}, {id:3}, {id:4}]
result = difference(selectedItems, savedItems, obj => obj.id)
console.log(result)
You can use the .includes() method on an array to check whether a value is contained in it (see the documentation for more information).
Now we can just filter the array of saved items to find only ones that aren't contained by the selected items array.
arrayDeletedItems = arraySavedItems.filter((itemSaved) =>
!arraySelectedItems.includes(itemSaved)
)
As #owenizedd points out in the comments, this only works for primitive data types where a shallow equality check is sufficient. A more robust approach can be used with the .reduce() method and a custom equality check. For example, lodash's isEqual() does a deep comparison for equality. You would have to import the module for this. Unfortunately there is no native deep equality check in JavaScript currently (workarounds like JSON.stringify() to then compare the string representations have various downsides).
arrayDeletedItems = arraySavedItems.filter((itemSaved) =>
!arraySelectedItems.reduce((previous, current) =>
previous || _.isEqual(current, itemSaved)
)
)
Note that passing previous as the first argument to the 'or' operator (||) means we can benefit from lazy evaluation - once a hit has been found, the second half of the statement does not need to be evaluated any more.
To solve this problem, since we have id we can utilize it.
You need a key that is unique. so id commonly known will have unique value.
So my approach, find items that is not exist in B array but in A array, and find items that exist in B but not in A array.
This approach not be the fastest, but the findDiff is reusable.
const a = [....];
const b = [....];
const findDiff = (source, target) => {
return source.filter((sourceItem, index) => {
const isInTarget = target.findIndex(targetItem => targetItem.id === sourceItem.id)
return isInTarget === -1
})
}
const difference = findDiff(a,b).concat(findDiff(b,a)); //result
I have two arrays that need merging in Javascript. They are arranged as follows:
arrayA = [town1A, town2A, town3A];
arrayB = [town3B, town5B];
Each town is an object with a townName: 'town1' (matching the object variable name). Each town also has an array of occupants: [{}, {}] which each have their own personName, and a status: 'dead' or 'alive'.
My goal, is that after merging, the new array will contain every unique town according to townName (town3B and town3A both have townName : 'town3').
arrayC = [town1, town2, town3, town5]
Any new towns in arrayB (i.e., town5) should be added directly to the list. Any towns with the same name (i.e., town3) should combine their lists of occupants, but remove any "dead" people. ArrayB has priority over ArrayA when determining status, as it is "overwriting" the old data. For example:
arrayA.town3.occupants = [{name: 'Bob', status: 'alive'}, {name: 'Joe', status: 'alive'}];
arrayB.town3.occupants = [{name: 'Bob', status: 'dead'}, {name: 'Alice', status: 'alive'}];
arrayC.town3.occupants = [{name: 'Joe', status: 'alive'}, {name: 'Alice', status: 'alive'}];
I'm just struggling with the logic sequence process here and need a nudge to figure out what tools to use. Currently I'm trying to work with Lodash's _.merge and _.union in some combination. It seems I can use _.mergeWith or _.unionBy to "nest" the merging steps without resorting to manually looping over the arrays, but their usage is going over my head. If a solution exists that uses one of those, I would like to see an example to learn better how they work.
Edit: I was asked for the entire contents of an example arrayA and arrayB:
arrayA = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
{name: 'Jim', status: 'dead'}
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'alive'},
{name: 'Joe', status: 'alive'}
]
}
];
arrayB = [
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'dead'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Sam', status: 'dead'},
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
The output I expect is:
arrayC = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Joe', status: 'alive'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
I managed to find a consistent way to do this (thanks to #Enlico for some hints). Since _.mergeWith() is recursive, you can watch for a specific nested object property and handle each property differently if needed.
// Turn each array into an Object, using "townName" as the key
var objA = _.keyBy(arrayA, 'townName');
var objB = _.keyBy(arrayB, 'townName');
// Custom handler for _.merge()
function customizer(valueA, valueB, key) {
if(key == "occupants"){
//merge occupants by 'name'. _.union prioritizes first instance (so swap A and B)
return _.unionBy(valueB, valueA, 'name');
//Else, perform normal _.merge
}
}
// Merge arrays, then turn back into array
var merged = _.values(_.mergeWith(objA, objB, customizer));
// Remove dead bodies
var filtered = _.map(merged, town => {
town.occupants = _.filter(town.occupants, person => {return person.status == "alive";});
return town;
});
The complexity with this problem is that you want to merge on 2 different layers:
you want to merge two arrays of towns, so you need to decide what to do with towns common to the two arrays;
when handling two towns with common name, you want to merge their occupants.
Now, both _.merge and _.mergeWith are good candidates to accomplish the task, except that they are for operating on objects (or associative maps, if you like), whereas you have vectors of pairs (well, not really pairs, but objects with two elements with fixed keys; name/status and townName/occupants are fundamentally key/value) at both layers mentioned above.
One function that can be useful in this case is one that turns an array of pairs into an object. Here's such a utility:
arrOfPairs2Obj = (k, v) => (arr) => _.zipObject(..._.unzip(_.map(arr, _.over([k, v]))));
Try executing the following
townArr2townMap = arrOfPairs2Obj('townName', 'occupants');
mapA = townArr2townMap(arrayA);
mapB = townArr2townMap(arrayB);
to see what it does.
Now you can merge mapA and mapB more easily…
_.mergeWith(mapA, mapB, (a, b) => {
// … well, not that easily
})
Again, a and b are arrays of "pairs" name/status, so we can reuse the abstraction I showed above, defining
personArr2personMap = arrOfPairs2Obj('name', 'status');
and using it on a and b.
But still, there are some problems. I thought that the (a, b) => { … } I wrote above would be called by _.mergeWith only for elements which have the same key across mapA and mapB, but that doesn't seem to be the case, as you can verify by running this line
_.mergeWith({a: 1, b: 3}, {b:2, c:4, d: 6}, (x, y) => [x, y])
which results in
{
a: 1
b: [3, 2]
c: [undefined, 4]
d: [undefined, 6]
}
revealing that the working lambda is called for the "clashing" keys (in the case above just b), and also for the keys which are absent in the first object (in the case above c and d), but not for those absent in the second object (in the case above a).
This is a bit unfortunate, because, while you could filter dead people out of towns which are only in arrayB, and you could also filter out those people which are dead in arrayB while alive in arrayA, you'd still have no place to filter dead people out of towns which are only in arrayA.
But let's see how far we can get. _.merge doc reads
Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
So we can at least handle the merging of towns common across the array in a more straightforward way. Using _.merge means that if a person is common in the two arrays, we'll always pick the one from arrayB, whether that's (still) alive or (just) dead.
Indeed, a strategy like this doesn't give you the precise solution you want, but not even one too far from it,
notSoGoodResult = _.mergeWith(mapA, mapB, (a, b) => {
return _.merge(personArr2personMap(a), personArr2personMap(b));
})
its result being the following
{
town1: [
{name: "Charlie", status: "alive"},
{name: "Jim", status: "dead"}
],
town2: [
{name: "Rachel", status: "alive"}
],
town3:
Alice: "alive",
Bob: "dead",
Joe: "alive"
},
town5: {
Bob: "alive",
Ray: "alive",
Sam: "dead"
}
}
As you can see
Bob in town3 is correctly dead,
we've not forgotten Alice in town3,
nor have we forogtten about Joe in town3.
What is left to do is
"reshaping" town3 and town5 to look like town1 and town2 (or alternatively doing the opposite),
filtering away all dead people (there's no more people appearing with both the dead and alive status, so you don't risk zombies).
Now I don't have time to finish up this, but I guess the above should help you in the right direction.
The bottom line, however, in my opinion, is that JavaScript, even with the power of Lodash, is not exactly the best tool for functional programming. _.mergeWith disappointed me, for the reason explained above.
Also, I want to mention that there a module named lodash/fp that
promotes a more functional programming (FP) friendly style by exporting an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods.
This shuould slightly help you be less verbose. With reference to your self answer, and assuming you wanted to write the lambda
person => {return person.status == "alive";}
in a more functional style, with "normal" Lodash you'd write
_.flowRight([_.curry(_.isEqual)('alive'), _.iteratee('status')])
whereas with lodash/fp you'd write
_.compose(_.isEqual('alive'), _.get('status'))
You can define a function for merging arrays with a mapper like this:
const union = (a1, a2, id, merge) => {
const dict = _.fromPairs(a1.map((v, p) => [id(v), p]))
return a2.reduce((a1, v) => {
const i = dict[id(v)]
if (i === undefined) return [...a1, v]
return Object.assign([...a1], { [i]: merge(a1[i], v) })
}, a1)
}
and use it like this:
union(
arrayA,
arrayB,
town => town.townName,
(town1, town2) => ({
...town1,
occupants: union(
town1.occupants,
town2.occupants,
occupant => occupant.name,
(occupant1, occupant2) => occupant1.status === 'alive' ? occupant1 : occupant2
).filter(occupant => occupant.status === 'alive')
})
)
I have two arrays like so
data = [{id: 1, name: apple},
{id: 2, name: mango},
{id: 3, name: grapes},
{id: 4, name: banana}]
data2 =[{id: 1, name: apple},
{id: 3, name grapes}]
My Expected result would be:
[{ id: 2, name: mango},
{id:4, name: banana}]
My code is
let finalData =[];
data.forEach(result => {
data2.find(datum => {
if(datum['id'] === result['id]{
finalData.push(result);
}
})
})
I am getting wrong result. What is the simplest code or library that I can use?
Your sample data doesn't make sense, but assuming you mean that all data items that have matching IDs also have matching names and also assuming you want a set of all items where the IDs are the same in the two sets of data, you could use a Set to keep track of which IDs are present in one array then filter the second array by those that have their IDs in the set:
const idsInFirst = new Set(data.map(d => d.id));
const intersection = data2.filter(d => idsInFirst.has(d.id));
The reason why an intermediate Set structure is used is because it allows O(1) lookups after a one-time scan, which is more efficient than repeatedly scanning the first array over and over.
If you meant to say you wanted a difference between data sets (items excluded from data that are in data2), you'd want to negate/inverse things a bit:
const idsToExclude = new Set(data2.map(d => d.id));
const difference = data.filter(d => !idsToExclude.has(d.id));
Edit
After your clarifying edit, it's that second block of code that you'll want.
I would say a good way to do that is filtering your longest array using a function that will validate if the object id is present in both arrays. Check this example:
const data = [
{id: 1, name: 'apple'},
{id: 2, name: 'mango'},
{id: 3, name: 'grapes'},
{id: 4, name: 'banana'}
]
const data2 =[
{id: 1, name: 'apple' },
{id: 3, name: 'grapes' }
]
const longest = data.length > data2.length ? data : data2;
const shortest = data.length <= data2.length ? data : data2;
const finalData = longest.filter( obj => !shortest.find( o => o.id === obj.id ) )
console.log(finalData)
Good luck!
I have an flat array of Folders like this one :
const foldersArray = [{id: "1", parentId: null, name: "folder1"}, {id: "2", parentId: null, name: "folder2"}, {id: "1.1", parentId: 1, name: "folder1.1"}, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"},{id: "2.1", parentId: 2, name: "folder2.1"}]
I want to output an array of all parents of a given folder to generate a Breadcrumb-like component of Folder path.
I have presently a code that does what I need but I'd like to write it better in a more "functional" way, using reduce recursively.
If I do :
getFolderParents(folder){
return this.foldersArray.reduce((all, item) => {
if (item.id === folder.parentId) {
all.push (item.name)
this.getFolderParents(item)
}
return all
}, [])
}
and I log the output, I can see it successfully finds the first Parent, then reexecute the code, and outputs the parent's parent... as my initial array is logically reset to [] at each step... Can't find a way around though...
You could do this with a Map so you avoid iterating over the array each time you need to retrieve the next parent. This way you get an O(n) instead of an O(n²) time complexity:
const foldersArray = [{id: "1", parentId: null, name: "folder1"}, {id: "2", parentId: null, name: "folder2"}, {id: "1.1", parentId: "1", name: "folder1.1"}, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"},{id: "2.1", parentId: "2", name: "folder2.1"}];
const folderMap = new Map(foldersArray.map( o => [o.id, o] ));
const getFolderParents = folder =>
(folder.parentId ? getFolderParents(folderMap.get(folder.parentId)) : [])
.concat(folder.name);
// Example call:
console.log(getFolderParents(foldersArray[4]));
Just a minor remark: your parentId data type is not consistent: it better be always a string, just like the data type of the id property. If not, you need to cast it in your code, but it is really better to have the data type right from the start. You'll notice that I have defined parentId as a string consistently: this is needed for the above code to work. Alternatively, cast it to string in the code with String(folder.parentId).
Secondly, the above code will pre-pend the parent folder name (like is done in file folder notations). If you need to append the parent name after the child, then swap the concat subject and argument:
[folder.name].concat(folder.parentId ? getFolderParents(folderMap.get(folder.parentId)) : []);
You can do what you're looking for with a rather ugly looking while loop. Gets the job done though. Each loop iteration filters, looking for an instance of a parent. If that doesn't exist, it stops and exits. If it does exist, it pushes that parent into the tree array, sets folder to its parent to move up a level, then moves on to the next iteration.
const foldersArray = [{
id: "1",
parentId: null,
name: "folder1"
}, {
id: "2",
parentId: null,
name: "folder2"
}, {
id: "1.1",
parentId: 1,
name: "folder1.1"
}, {
id: "1.1.1",
parentId: "1.1",
name: "folder1.1.1"
}, {
id: "2.1",
parentId: 2,
name: "folder2.1"
}]
function getParents(folder){
const tree = [], storeFolder = folder
let parentFolder
while((parentFolder = foldersArray.filter(t => t.id == folder.parentId)[0]) !== undefined){
tree.push(parentFolder)
folder = parentFolder
}
console.log({ originalFolder: storeFolder, parentTree: tree})
}
getParents(foldersArray[3])
You're thinking about it in a backwards way. You have a single folder as input and you wish to expand it to a breadcrumb list of many folders. This is actually the opposite of reduce which takes as input many values, and returns a single value.
Reduce is also known as fold, and the reverse of a fold is unfold. unfold accepts a looping function f and an init state. Our function is given loop controllers next which add value to the output and specifies the next state, and done which signals the end of the loop.
const unfold = (f, init) =>
f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
, () => []
, init
)
const range = (m, n) =>
unfold
( (next, done, state) =>
state > n
? done ()
: next ( state // value to add to output
, state + 1 // next state
)
, m // initial state
)
console.log (range (3, 10))
// [ 3, 4, 5, 6, 7, 8, 9, 10 ]
Above, we start with an initial state of a number, m in this case. Just like the accumulator variable in reduce, you can specify any initial state to unfold. Below, we express your program using unfold. We add parent to make it easy to select a folder's parent
const parent = ({ parentId }) =>
data .find (f => f.id === String (parentId))
const breadcrumb = folder =>
unfold
( (next, done, f) =>
f == null
? done ()
: next ( f // add folder to output
, parent (f) // loop with parent folder
)
, folder // init state
)
breadcrumb (data[3])
// [ { id: '1.1.1', parentId: '1.1', name: 'folder1.1.1' }
// , { id: '1.1', parentId: 1, name: 'folder1.1' }
// , { id: '1', parentId: null, name: 'folder1' } ]
breadcrumb (data[4])
// [ { id: '2.1', parentId: 2, name: 'folder2.1' }
// , { id: '2', parentId: null, name: 'folder2' } ]
breadcrumb (data[0])
// [ { id: '1', parentId: null, name: 'folder1' } ]
You can verify the results of the program below
const data =
[ {id: "1", parentId: null, name: "folder1"}
, {id: "2", parentId: null, name: "folder2"}
, {id: "1.1", parentId: 1, name: "folder1.1"}
, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"}
, {id: "2.1", parentId: 2, name: "folder2.1"}
]
const unfold = (f, init) =>
f ( (value, state) => [ value, ...unfold (f, state) ]
, () => []
, init
)
const parent = ({ parentId }) =>
data .find (f => f.id === String (parentId))
const breadcrumb = folder =>
unfold
( (next, done, f) =>
f == null
? done ()
: next ( f // add folder to output
, parent (f) // loop with parent folder
)
, folder // init state
)
console.log (breadcrumb (data[3]))
// [ { id: '1.1.1', parentId: '1.1', name: 'folder1.1.1' }
// , { id: '1.1', parentId: 1, name: 'folder1.1' }
// , { id: '1', parentId: null, name: 'folder1' } ]
console.log (breadcrumb (data[4]))
// [ { id: '2.1', parentId: 2, name: 'folder2.1' }
// , { id: '2', parentId: null, name: 'folder2' } ]
console.log (breadcrumb (data[0]))
// [ { id: '1', parentId: null, name: 'folder1' } ]
If you trace the computation above, you see that find is called once per folder f added to the outupt in the unfolding process. This is an expensive operation, and if your data set is significantly large, could be a problem for you.
A better solution would be to create an additional representation of your data that has a structure better suited for this type of query. If all you do is create a Map of f.id -> f, you can decrease lookup time from linear to logarithmic.
unfold is really powerful and suited for a wide variety of problems. I have many other answers relying on it in various ways. There's even some dealing with asynchrony in there, too.
If you get stuck, don't hesitate to ask follow-up questions :D
I'm currently working on a small application where I have to loop through an enormous array of objects. What would be the most efficient method to perform this?
var array = [
{
id: "1",
name: "Alpha"
},
{
id: "2",
name: "Beta"
},
...
];
I'd like to get each object where name equals "Alpha". I'm currently using a simple if statement to filter the objects with a different name value out, but I wonder if there's a more efficient way to do this, performance-wise.
It's worth to mention that I'll push the matching results into a new array.
No, there is no more efficient way.
The alternative is to build and maintain some kind of internal data structure which allows you to find the desired elements faster. As usual, the trade off is between the work involved in maintaining such a structure vs the time it saves you.
I don't have any way about which I would know it's more effective.
But if you had your objects ordered by name you could stop your search imideatly upon reaching an object whose name is not equal to "Alpha".
To find the first object you're looking for you can use binary search and from this Object you go up and down until at both ends you reach an object which isn't named "Alpha" or the end of array.
This is only a way of optimizing and will require time to sort the array and also will take more time when adding an element.
There's a JavaScript function exactly for this kind of task. Filter
From the Docs
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
Here is a small example by code for getting all element from array which has a certain 'name' field:
const arr = [
{name: 'Abc'},
{name: 'Xyz'},
{name: 'Lmn'},
{name: 'Xyz'},
{name: 'Xyz'}
];
let response = findByName('Xyz');
console.log(response);
function findByName (name) {
return arr.filter((element) => {
return element.name = name;
});
}
If you need more than one time a collection with a given name, you could use an object with the names as hashes and have instantly access to the items.
var array = [{ id: "1", name: "Alpha" }, { id: "2", name: "Beta" }, { id: "3", name: "Beta" }, { id: "4", name: "Gamma" }, { id: "5", name: "Beta" }, { id: "2", name: "Alpha" }],
hash = Object.create(null);
array.forEach(function (a) {
if (!hash[a.name]) {
hash[a.name] = [];
}
hash[a.name].push(a);
});
console.log(hash);
.as-console-wrapper { max-height: 100% !important; top: 0; }