Related
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.
Given an array of objects named allItems which is pre-sorted, but cannot be sorted again from the information it contains - what is an alternative implementation to the reduce function below that will retain the sorted order of allItems?
The logic below will output:
[{ id: 'd' }, { id: 'a' }, { id: 'b' }]
The desired output is:
[{ id: 'a' }, { id: 'b' }, { id: 'd' }]
// NOTE: allItems is pre-sorted, but lacks the information to re-sort it
const allItems = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const includedIds = ['d', 'a', 'b'];
// QUESTION: How to create the same output, but in the order they appear in allItems
const unsortedIncludedItems = includedIds.reduce((accumulator, id) => {
const found = allItems.find(n => n.id === id);
if (found) accumulator.push(found);
return accumulator;
}, [])
As mentioned in response to #Ben, simply reversing the logic is a deal breaker for performance reasons.
Instead of iterating over the includedIds (in the wrong order) and seeing whether you can find them in allItems, just iterate over allItems (which is the right order) and see whether you can find their ids in includedIds:
const allItems = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const includedIds = ['d', 'a', 'b'];
const includedItems = allItems.filter(item => includedIds.includes(item.id));
The issue you have here is that your code reverses the list. You can simply push to the front of the list instead, and the original order will be maintained.
Unfortunately, pushing to the front of a list is slower, it's O(n) rather than O(1). It looks like Array.prototype.unshift is supposed to be faster, but it's still O(n) according to this blog. Assuming that the number of found elements is small you won't notice any performance issues. In that case, replace push with unshift like so:
// NOTE: allItems is pre-sorted, but lacks the information to re-sort it
const allItems = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const includedIds = ['d', 'a', 'b'];
// QUESTION: How to create the same output, but in the order they appear in allItems
const unsortedIncludedItems = includedIds.reduce((accumulator, id) => {
const found = allItems.find(n => n.id === id);
if (found) accumulator.unshift(found);
return accumulator;
}, [])
Otherwise, these are your options:
Create a wrapper around this object that reverses the indexes rather than the array. This can be done with a function like this:
const getFromEnd = (arr, i) => arr[arr.length - 1 - i]
Note that this can be replaced with arr.at(-i) in new browser versions (last few months). This could be encapsulated within a class if you're feeling OOP inclined.
Remember to manually invert the indices wherever you use this array (this will be bug-prone, as you may forget to invert them)
Reverse the array. As shown in this fiddle, even with 10,000 elements, the performance is not bad. Assuming this isn't a hotpath or user-interactive code, I think that even 100,000 is probably fine.
Update
Example B will use the index of the input array to sort the filtered array.
Try .filter() and .include() to get the desired objects and then .sort() by each object's string value. See Example A.
Another way is to use .flatMap() and .include() to get an array of arrays.
// each index is from the original array
[ [15, {id: 'x'}], [0, {id: 'z'}], [8, {id: 'y'}] ]
Then use .sort() on each sub-array index.
[ [0, {id: 'z'}], [8, {id: 'y'}], [15, {id: 'x'}] ]
Finally, use .flatMap() once more to extract the objects and flatten the array of arrays into an array of objects.
[ {id: 'z'}, {id: 'y'}, {id: 'x'} ]
See Example B
Example A (sort by value)
const all = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const values = ['d', 'a', 'b'];
const sortByStringValue = (array, vArray, key) => array.filter(obj => vArray.includes(obj[key])).sort((a, b) => a[key].localeCompare(b[key]));
console.log(JSON.stringify(sortByStringValue(all, values, 'id')));
Example B (sort by index)
const all = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const values = ['d', 'a', 'b'];
const alt = [{name:'Matt'}, {name:'Joe'}, {name:'Jane'}, {name:'Lynda'}, {name:'Shelly'}, {name:'Alice'}];
const filter = ['Shelly', 'Matt', 'Lynda'];
const sortByIndex = (array, vArray, key) => array.flatMap((obj, idx) => vArray.includes(obj[key]) ? [[idx, obj]] : []).sort((a, b) => a[0] - b[0]).flatMap(sub => [sub[1]]);
console.log(JSON.stringify(sortByIndex(all, values, 'id')));
console.log(JSON.stringify(sortByIndex(alt, filter, 'name')));
Why not just reverse the logic, Filter out the ids which not suppose to be includes.
// NOTE: allItems is pre-sorted, but lacks the information to re-sort it
const allItems = [
{ id: "a" },
{ id: "b" },
{ id: "c" },
{ id: "d" },
{ id: "e" },
{ id: "f" },
];
const includedIds = ["d", "a", "b"];
const findElms = (items, includedIds) => items.filter((n) => includedIds.includes(n.id))
console.log(findElms(allItems, includedIds));
I have multiple json5 files that I need to join in one object.
Example the main object that other files will join to:
/equations/mass-energy-equivalence.json5
{
name: 'Mass-energy equivalence',
expression: 'E=mc^{2}',
expressionIntern: '\\mag{E}=\\mag{m}\\const{c}^{2}',
description: '...',
categories: ['physics'],
units: [
'joule'
],
constants: [
'speed-of-light'
],
magnitudes: [
'energy', 'mass'
],
values: [
{ value: 1000, units: ['joule'] }
]
}
/magnitudes/energy.json5
{ name: 'Energy', symbol: 'E', slug: 'energy', units: ['joule'], description: '', ... }
So Magnitudes have units, i have to join units/joule with magnitudes/energy and finally to the first object.
/units/joule.json5
{ name: 'Joule', symbol: 'J', slug: 'joule', description: '', ... }
And so on.
I need to join: categories, units, constants and mangitudes. Just like MySQL join. Also magnitudes has units so they have to be joined too.
So I'm trying to do a function that gets an array of nested properties like this:
This is the input to the function:
const nestedProperties = [
'categories.slug',
'constants.slug.units.slug',
'magnitudes.slug',
'units.slug',
'variables.slug.units.slug',
'values.units.slug'
];
Needs to do this for all the nestedProperties. The final object will be the output:
https://i.stack.imgur.com/yat2V.jpg
I have a function that gets the data await getData() so I need this recursive function that will set data.categories = await getData('categories', slug: 'physics')
My idea is something like this... yet not finished.
getAllData(object, nestedProperties) {
nestedProperties.forEach(async (item) => {
const parts = item.split('.');
const size = parts.length;
// We need at least two parts to get the data.
if(size === 0) console.error('Invalid Path');
// Size is even so is a multiple of 2
// ex. categories.slug
if(size % 2 === 0) {
for(let i = 0; i < size - 2; i += 2) {
// path.property -> categories.slug
if(i == 0) {
let path = parts[i];
let property = parts[i + 1];
// If the path is in the data and is an array with items
// ex. data[categories]
if(Array.isArray(data[path]) && data[path].length > 0) {
// Iterate
for(let i = 0; i < data[path].length; i++) {
// path => 'categories',
// data[path] => 'physics'
data[path] = getData(path, data[path]);
// Recursive data[path] = getAllData(path, data[path])
}
} else {
data[path] = getAllData(path, data[path])
}
}
}
} else {
// Is odd so we need to do it a bit different
// ex. 'values.units.slug'
}
}
}
Been trying a lot but not success to get to the 2 or 3 nested property :[
Thanks a lot.
I'm afraid I don't have time right now to write up a more complete explanation of this, so I'll be brief. If I find time tomorrow, I'll add more explanation. It's not complete, and doesn't handle your 'constants.slug.units.slug', so it might really be way off. (I actually simply remove the slug nodes, as I don't make sense of them.)
getData is just a dummy, meant to allow us to run something like your getData above. (Is this the equivalent of $content from your code?).
last is a trivial helper getting the last element of an array.
getPath takes a path such as ['foo', 1' 'bar'] and an object such as {foo: [{bar: 1, baz: 2}, {bar: 3, baz: 4}], qux: 5} and returns 3, the value of the bar property of the element at index 1 of the foo property of our object.
setPath simply reverses this:
setPath (['foo', 1, 'bar']) (42) ({foo: [{bar: 1, baz: 2}, {bar: 3, baz: 4}], qux: 5})
//=> {foo: [{bar: 1, baz: 2}, {bar: 42, baz: 4}], qux: 5}
fullPaths is more complex. It deals with the fact that you have fields that might be arrays or might be scalar values. It takes a path such as ['magnitudes'] and your initial data and finds the paths in the format required by getPath and setPath. Thus
fullPaths (['magnitudes']) (rawData) //=> [["magnitudes", 0], ["magnitudes", 1]]
which in turn point to 'energy' and 'mass', respectively.
With these helpers in place, we can write getAllData.
That uses fullPaths after taking your nestedProperties and turning them to arrays, removing the 'slug' substrings. With these results, we can dig into, say values.units.slug to get ['values', 0, 'units', 0], which maps to 'joules', and using 'units' and 'joules' we call getData.
After the Promises return resolve, we can fold over the results, calling such things as setPath (['values', 0, 'units', 0], promiseResult, accumulator). We return the result of that fold.
I don't know if I'll have much time to come back to this, but in case I do, I'd love to hear how close this is to your requirements. It's not clear to me for instance if you need to run those same getAllData over each result returned from getData, and if you do, whether the same nestedProperties are to be used for them.
I also don't know how to deal with the constants.slug.units.slug, as our constants are string values and don't have units.
const last = (xs) =>
xs [xs .length - 1]
const getPath = ([p, ...ps]) => (o) =>
p == undefined ? o : getPath (ps) (o && o[p])
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const fullPaths = ([p, ...ps]) => (o) =>
p == undefined
? [[]]
: Array .isArray (o)
? o .flatMap ((x, i) => fullPaths (ps) (x [p]) .map (ns => [p, i, ...ns]))
: Object (o) === o
? p in o
? Array .isArray (o [p])
? o [p] .map ((x, i) => fullPaths (ps) (x) .flatMap ((x) => [p, i, ...x]))
: fullPaths (ps) (o [p]) .map (x => [p, ...x])
: []
: [[p]]
const getAllData = (
rawData,
nestedProperties,
paths = nestedProperties .map (s => s.split ('.'))
.map (a => a.filter (s => s !== 'slug'))
.flatMap (p => fullPaths (p) (rawData))
) =>
Promise .all (
paths .map (
p => getData (
last (p .filter (s => String (s) === s)),
getPath (p) (rawData)
)
)
) .then (res => res .reduce (
(a, r, i) => setPath (paths[i]) (r) (a),
rawData
))
const rawData = {name: 'Mass-energy equivalence', expression: 'E=mc^{2}', expressionIntern: '\\mag{E}=\\mag{m}\\const{c}^{2}', description: '...', categories: ['physics'], units: ['joule'], constants: ['speed-of-light'], magnitudes: ['energy', 'mass'], values: [{value: 1000, units: ['joule']}]}
const nestedProperties = ['categories.slug', /*'constants.slug.units.slug',*/ 'magnitudes.slug', 'units.slug', 'variables.slug.units.slug', 'values.units.slug']
getAllData (rawData, nestedProperties )
.then ((r) => console .log(JSON.stringify(r, null, 4)))
.catch (console .warn)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script> <!-- Dummy version of getData -->
const getData = ((data) => async (group, value) => group in data && value in data [group] ? Promise .resolve (data [group] [value]) : Promise .reject (`Cannot find ${group}/${value}`))({categories: {physics: {id: 2, name: "Physics", description: "Physics (from Ancient Greek: φυσική (ἐπιστήμη), romanized: physikḗ (epistḗmē), lit. 'knowledge of nature', from φύσις phýsis 'nature') is the natural science that studies matter, its motion and behavior through space and time, and the related entities of energy and force. Physics is one of the most fundamental scientific disciplines, and its main goal is to understand how the universe behaves."}, chemistry: {id: 3, name: "Chemistry", description: "Chemistry is the scientific discipline involved with elements and compounds composed of atoms, molecules and ions: their composition, structure, properties, behavior and the changes they undergo during a reaction with other substances."}}, units: {joule: {name: "Joule", symbol: {text: "J", html: "J", tex: "J"}, type: "si", categories: ["physics"], units: ["joule-per-kelvin", "joule-second"], description: "The joule (/dʒaʊl, dʒuːl/ jowl, jool) is a derived unit of energy in the International System of Units. It is equal to the energy transferred to (or work done on) an object when a force of one newton acts on that object in the direction of the force's motion through a distance of one metre (1 newton metre or $N⋅m$). It is also the energy dissipated as heat when an electric current of one ampere passes through a resistance of one ohm for one second. It is named after the English physicist James Prescott Joule (1818–1889)."}}, magnitudes: {energy: {name: 'Energy', symbol: {text: 'E', html: 'E', tex: 'E',}, categories: ['physics'], description: 'In physics, energy is the quantitative property that must be transferred to an object in order to perform work on, or to heat, the object. Energy is a conserved quantity; the law of conservation of energy states that energy can be converted in form, but not created or destroyed. The SI unit of energy is the joule, which is the energy transferred to an object by the work of moving it a distance of 1 metre against a force of 1 newton.', baseUnit: 'joule', units: ['joule']}, mass: {name: "Mass", symbol: {text: "m", html: "m", tex: "m"}, categories: ["physics"], description: "Property of matter to resist changes of the state of motion and to attract other bodies", baseUnit: "kilogram", units: ["tonne", "kilogram", "gram", "milligram", "microgram", "long-ton", "short-ton", "stone", "pound", "ounce"]}}, constants: {'speed-of-light': {name: "Speed of light in vacuum", symbol: {text: "c", html: "c", tex: "c"}, description: "The speed of light in vacuum, commonly denoted $c$, is a universal physical constant important in many areas of physics. Its exact value is defined as $299, 792, 458$ $m/s$ (approximately $300, 000$ $km/s$, or $18, 6000$ $mi/s$). It is exact because, by international agreement, a metre is defined as the length of the path travelled by light in vacuum during a time interval of $\\frac{1}{299, 792, 458}$ second. According to special relativity, $c$ is the upper limit for the speed at which conventional matter, energy or any information can travel through coordinate space.", categories: ["universal", "physics"], units: ["metre-per-second"], values: [{value: 299792458, units: "metre-per-second", exact: false, base: false}, {value: 3e8, units: "metre-per-second", exact: false}]}}})
</script>
I have an array of objects that I would like to know how to parse, find the keys that are in my other array and check if they are null in the initial array of objects.
How can I achieve this?
So I have something like:
[{"nonmandatoryfield":"","mandatoryfield1":"1","mandatoryfield2":"2",
"mandatoryfield3":"3"}]
now I would like to check if keys: [mandatoryfield1, mandatoryfield2, mandatoryfield3] values are null
In JavaScript
Maybe you want something like this:
const mandatoryFieldNames = [
'mandatoryfield1',
'mandatoryfield2',
'mandatoryfield3',
];
const data = [
{
nonmandatoryfield: '',
mandatoryfield1: '1',
mandatoryfield2: '2',
mandatoryfield3: '3',
},
{
nonmandatoryfield: '',
mandatoryfield1: null,
mandatoryfield2: '2',
mandatoryfield3: '3',
},
];
const dataWithMandatoryFields = data.filter(item =>
mandatoryFieldNames.every(field => item[field]),
);
I wanted to filter the values in an array of objects by a certain value. When I run the function, I get TypeError: obj[key].includes is not a function. I am using reactjs. What I'm I missing in the function?
var arr = [{
name: 'xyz',
grade: 'x'
}, {
name: 'yaya',
grade: 'x'
}, {
name: 'x',
frade: 'd'
}, {
name: 'a',
grade: 'b'
}];
filterIt(arr, searchKey) {
return arr.filter(obj => Object.keys(obj)
.map(key => obj[key].includes(searchKey)));
}
I got this example from https://stackoverflow.com/a/40890687/5256509 and tried it out
You can't filter array of object like this, because this combination
Object.keys(obj).map(key => obj[key].includes(searchKey))
always provides an array (with true and false values), and any array is truthy. Hence filter doesn't filter anything. You can try something like this:
arr.filter(obj =>
Object.keys(obj)
.some(key => obj[key].includes(searchKey))
);