Javascript - How to delete element from deeply nested array by value - javascript

I have JSON something like:
[
{
"property": "Foo",
"address": "Foo Address",
"debitNote": [
{
"title": "Debit A Foo",
"desc": "Desc Debit Foo",
"listDebit": [
{
"id": "IP-1A1",
"amount": "273000"
},
{
"id": "IP-1A2",
"amount": "389000"
},
{
"id": "IP-1A3",
"amount": "382000"
},
{
"id": "IP-1A4",
"amount": "893000"
},
{
"id": "IP-1A5",
"amount": "1923000"
}
]
},
{
"title": "Debit B Foo",
"desc": "Desc Debit B Foo",
"listDebit": [
{
"id": "IP-1B1",
"amount": "120000"
},
{
"id": "IP-1B2",
"amount": "192000"
}
]
}
]
},
{
"property": "Bar",
"address": "Address Bar",
"debitNote": [
{
"title": "Debit A Bar",
"desc": "Desc Bar",
"listDebit": [
{
"id": "IP-1C1",
"amount": "893000"
},
{
"id": "IP-1C2",
"amount": "1923000"
}
]
},
{
"title": "Debit B Bar",
"desc": "Desc Debit B Bar",
"listDebit": [
{
"id": "IP-1D1",
"amount": "192000"
}
]
}
]
}
]
I need to fix 2 problem from those json:
check if value is match with id inside JSON
remove element if id of JSON is found by value and the id index is the latest index
I can handle the 1st task with current code:
let json = [{"property":"Foo","address":"Foo Address","debitNote":[{"title":"Debit A Foo","desc":"Desc Debit Foo","listDebit":[{"id":"IP-1A1","amount":"273000"},{"id":"IP-1A2","amount":"389000"},{"id":"IP-1A3","amount":"382000"},{"id":"IP-1A4","amount":"893000"},{"id":"IP-1A5","amount":"1923000"}]},{"title":"Debit B Foo","desc":"Desc Debit B Foo","listDebit":[{"id":"IP-1B1","amount":"120000"},{"id":"IP-1B2","amount":"192000"}]}]},{"property":"Bar","address":"Address Bar","debitNote":[{"title":"Debit A Bar","desc":"Desc Bar","listDebit":[{"id":"IP-1C1","amount":"893000"},{"id":"IP-1C2","amount":"1923000"}]},{"title":"Debit B Bar","desc":"Desc Debit B Bar","listDebit":[{"id":"IP-1D1","amount":"192000"}]}]}]
function findID(array, value){
return array.some((item)=>{
if(item.id == value) {
return true
}
return Object.keys(item).some(x=>{
if(Array.isArray(item[x])) return findID(item[x], value)
})
})
}
console.log(findID(json, 'IP-1D1'))
console.log(findID(json, 'IP-1A1'))
console.log(findID(json, 'IP-1B1'))
console.log(findID(json, 'IP-1Z1'))
But I'm stuck with the 2nd task with the expected result as follow:
console.log(removeID(json, 'IP-1A5')) // {"id": "IP-1A5", "amount": "1923000"} removed
console.log(removeID(json, 'IP-1A3')) // couldn't remove, because there's {"id": "IP-1A4", "amount": "893000"} after id 'IP-1A3'
anyone suggestion to fix my 2nd problem?

It's still not clear what you mean by the latest id. But we might be able to break the problem down so that decision is isolated from the rest of the logic.
We can start to write a generic traversal of a Javascript structure with arbitrary nesting of arrays and objects, one that builds a new object according to whether a given node matches a test we supply, and then build on that one which removes elements matching a given id. It might look something like this:
const deepFilter = (pred) => (o) =>
Array .isArray (o)
? o .filter (x => pred (x)) .map (deepFilter (pred))
: Object (o) === o
? Object .fromEntries (
Object .entries (o) .flatMap (([k , v]) => pred (v) ? [[k, deepFilter (pred) (v)]] : [])
)
: o
const deepReject = (pred) =>
deepFilter ((x) => ! pred (x))
const removeById = (target) => deepReject (({id}) => target == id)
deepFilter accepts a predicate function and returns a function which accepts such a nested object/array, and returns a copy of it including the nodes which match that predicate.
deepReject is built atop deepFilter, simply reversing the sense of the predicate.
And removeById is built atop deepReject, accepting a target id and returning a function that takes an object, returning a copy of it without those nodes whose ids match that target.
Now, if we had a function (lastMatch) to tell us whether an id was the last matching one in our input, we could write our main function like this:
const removeIdIfLast = (id) => (input) =>
lastMatch (id) (input)
? removeById (id) (input)
: input
I'm going to take a guess at what "the latest index" might mean. But be warned, the rest of this answer involves a guess that might well be wrong.
I will assume that for an id such as 'IP-1A4', the sense of "last" has to do with that final 4, that, say, 'IP-1A1' and 'IP-1A3' are before it, and 'IP-1A5' and 'IP-1A101' are after it., but that we only compare to ids with prefix IP-1A.
So, again wishing we had another function at our disposal (matchingIds), we can write a lastMatch function that finds the maximum id by sorting the ids from the document that match our target, then sorting those descending by extracting the numeric parts and subtracting, then taking the first element of the sorted list. (Sorting is not the most efficient way to find a maximum. If those lists are likely to be long, you might want to write a more efficient version of this function; it's not at all hard, but it would take us afield.)
const lastMatch = (id) => (input) =>
id == matchingIds (id) (input) .sort (
(a, b, x = a .match (/.+(\d+)$/) [1], y = b .match (/.+(\d+)$/) [1]) => y - x
) [0]
We could write matchingIds based on this idea, if we only had a function to gather all the ids in our data. It might look like this:
const matchingIds = (id, prefix = id .match (/(.+)\d+$/) [1]) => (input) =>
findIds (input) .filter (id => id .match (/(.+)\d+$/) [1] == prefix)
This is making the assumption (quite possibly unwarranted) that all ids in the input have this something-ending-in-a-string-of-digits structure. There might well need to be a more sophisticated version of this if that assumption is not correct.
So how would we write findIds? Well, a straightforward approach would mimic the design of deepFilter to look like this:
const findIds = (o) =>
Array.isArray (o)
? o .flatMap (findIds)
: Object (o) === o
? Object .entries (o) .flatMap (([k, v]) => k == 'id' ? [v, ...findIds (v)] : findIds (v))
: []
But I see a useful general-purpose abstraction lurking in that function, so I might build it on that, in this manner:
const fetchAll = (prop) => (o) =>
Array.isArray (o)
? o .flatMap (findIds)
: Object (o) === o
? Object .entries (o) .flatMap (
([k, v]) => k == prop ? [v, ... fetchAll (prop) (v)] : fetchAll (prop) (v)
)
: []
const findIds = fetchAll ('id')
Putting all this together, here is a solution, based on the assumptions above:
const deepFilter = (pred) => (o) =>
Array .isArray (o)
? o .filter (x => pred (x)) .map (deepFilter (pred))
: Object (o) === o
? Object .fromEntries (
Object .entries (o) .flatMap (([k , v]) => pred (v) ? [[k, deepFilter (pred) (v)]] : [])
)
: o
const deepReject = (pred) =>
deepFilter ((...args) => ! pred (...args))
const removeById = (target) => deepReject (({id}) => target == id)
const fetchAll = (prop) => (o) =>
Array.isArray (o)
? o .flatMap (findIds)
: Object (o) === o
? Object .entries (o) .flatMap (([k, v]) => k == prop ? [v, ... fetchAll (prop) (v)] : fetchAll (prop) (v))
: []
const findIds = fetchAll ('id')
const matchingIds = (id, prefix = id .match (/(.+)\d+$/) [1]) => (input) =>
findIds (input) .filter (id => id .match (/(.+)\d+$/) [1] == prefix)
const lastMatch = (id) => (input) =>
id == matchingIds (id) (input) .sort (
(a, b, x = a .match (/.+(\d+)$/) [1], y = b .match (/.+(\d+)$/) [1]) => y - x
) [0]
const removeIdIfLast = (id) => (input) =>
lastMatch (id) (input)
? removeById (id) (input)
: input
const input = [{property: "Foo", address: "Foo Address", debitNote: [{title: "Debit A Foo", desc: "Desc Debit Foo", listDebit: [{id: "IP-1A1", amount: "273000"}, {id: "IP-1A2", amount: "389000"}, {id: "IP-1A3", amount: "382000"}, {id: "IP-1A4", amount: "893000"}, {id: "IP-1A5", amount: "1923000"}]}, {title: "Debit B Foo", desc: "Desc Debit B Foo", listDebit: [{id: "IP-1B1", amount: "120000"}, {id: "IP-1B2", amount: "192000"}]}]}, {property: "Bar", address: "Address Bar", debitNote: [{title: "Debit A Bar", desc: "Desc Bar", listDebit: [{id: "IP-1C1", amount: "893000"}, {id: "IP-1C2", amount: "1923000"}]}, {title: "Debit B Bar", desc: "Desc Debit B Bar", listDebit: [{id: "IP-1D1", amount: "192000"}]}]}]
console .log (
'Removing IP-1A4 does nothing:',
removeIdIfLast ('IP-1A4') (input)
)
console .log (
'Removing IP-1A5 succeeds:',
removeIdIfLast ('IP-1A5') (input)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

// Get the id's path
function findPath(array, val) {
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (item.id !== val) {
const x = Object.keys(item).find(x => Array.isArray(item[x]))
if (x) {
const path = findPath(item[x], val);
if (path) {
path.unshift(x);
path.unshift(i);
return path
}
}
} else {
return [i];
}
}
}
// Use the path to remove the item from json
function removeLatestIndexId(path, json) {
const result = path.slice(0, -1).reduce((pre, next) => {
return pre[next]
}, json);
const idIndex = path[path.length - 1]
if (idIndex === result.length - 1) {
result.splice(idIndex, 1)
}
}
https://codepen.io/shuizy/pen/bGqLXoQ
You can use findPath to get the id's path inside json, and use that path to help you solve problem #2. (actually if you have a path you can also solve problem #1)

function removeID(array, value) {
for (let dn of json.map((node) => node.debitNote)) {
for (let lb of dn.map((n) => n.listDebit)) {
for (let i = 0; i < lb.length; i++) {
if (lb[i].id === value) {
return lb.splice(i, 1);
}
}
}
}
return {};
}
https://codepen.io/chriswong979/pen/bGqLZOL?editors=1111

Related

Compare JSON Keys to get missing elements

I want to compare to Nested Objects and create a new Object with all the missing fields.
I have a main JSON respone in a Javascript Object:
var MainFile = {
"id": 0,
"name": 'test',
"info": {
"data_11":0,
"data_12":0,
"data_13":{
"data_131":0,
"data_132":0,
"data_133":0,
},
},
"info2": {
"data_21":0,
"data_22":0,
"data_23":0,
}
}
And now I have x amount of objects that I have to compare against the main and check that object has all the same keys.
var obj2 = {
"id": 0,
"info": {
"data_11":0,
"data_13":{
"data_131":0,
"data_133":0,
},
},
"info2": {
"data_22":0,
"data_23":0,
}
}
}
So seeing both objects we can see the differences. I've tried recursive functions to check with Object.hasProperty, but I never get the result I'm looking for. that would be an object that would look like the following:
result = {
"id": true,
"name": false,
"info": {
"data_11":true,
"data_12":false,
"data_13":{
"data_131":false,
"data_132":true,
"data_133":false,
},
},
"info2": {
"data_21":true,
"data_22":false,
"data_23":false,
}
}
Has anyone tried anything like this? I've looked everywhere, but everyone compares the value of the key, not if the actual key is missing in the nested array.
Any help would be appreciated
One advantage of keeping around a collection of useful utility functions is that we can combine them quickly to create new functionality. For me, this function is as simple as this:
const diffs = (o1, o2) =>
hydrate (getPaths (o1) .map (p => [p, hasPath (p) (o2)]))
We first call getPaths (o1), which yields
[
["id"],
["name"],
["info", "data_11"],
["info", "data_12"],
["info", "data_13", "data_131"],
["info", "data_13", "data_132"],
["info", "data_13", "data_133"],
["info2", "data_21"],
["info2", "data_22"],
["info2", "data_23"]
]
Then we map these into path entry arrays, using hasPath against our test object, which yields:
[
[["id"], true],
[["name"], false],
[["info", "data_11"], true],
[["info", "data_12"], false],
[["info", "data_13", "data_131"], true],
[["info", "data_13", "data_132"], false],
[["info", "data_13", "data_133"], true],
[["info2", "data_21"], false],
[["info2", "data_22"], true],
[["info2", "data_23"], true]
]
This is very similar to the format used by Object .fromEntries, except that the keys are arrays of values rather than single strings. (Those values could be strings for object keys or integers for array indices.) To turn this back into an object, we use hydrate, which in turn depends upon setPath. Again hydrate tis a generalization of Object .fromEntries, and setPath is a recursive version of setting a value.
Put together it looks like this:
// utility functions
const getPaths = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [Array .isArray (obj) ? Number(k) : k, ... 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 hydrate = (xs) =>
xs .reduce ((a, [p, v]) => setPath (p) (v) (a), {})
const hasPath = ([p, ...ps]) => (obj) =>
p == undefined ? true : p in obj ? (ps .length > 0 ? hasPath (ps) (obj [p]) : true) : false
// main function
const diffs = (o1, o2) =>
hydrate (getPaths (o1) .map (p => [p, hasPath (p) (o2)]))
// sample data
const MainFile = {id: 0, name: "test", info: {data_11: 0, data_12: 0, data_13: {data_131: 0, data_132: 0, data_133: 0}}, info2: {data_21: 0, data_22: 0, data_23: 0}}
const obj2 = {id: 0, info: {data_11: 0, data_13: {data_131: 0, data_133: 0}}, info2: {data_22: 0, data_23: 0}};
// demo
console .log (diffs (MainFile, obj2))
.as-console-wrapper {max-height: 100% !important; top: 0}
The functions that this is built upon are all generally useful for many problems, and I simply keep them in my back pocket. You may note that this also handles arrays; that just comes along with those functions
diffs ({foo: ['a', 'b', 'c']}, {foo: ['a', 'b']})
//=> {"foo": [true, true, false]}
I also want to note that if you're looking for a more complete diff function, an old answer by user Mulan is a great read.

Find path to ALL matching key values in nested object

I have a function that gets me the path to the first finding of a nested object where the key and the value matches.
function getPath(obj, givenKey, givenValue) {
for(var key in obj) {
if(obj[key] && typeof obj[key] === "object") {
var result = getPath(obj[key], givenValue, givenKey);
if(result) {
result.unshift(key)
return result;
}
} else if(obj[key] === givenValue && key === givenKey ) {
return [key];
}
}
}
sample data
var myObj = [
{
"name": "needle",
"children": [
{
"name": "group2",
"children": [
{
"name": "item0"
}]
}]
},
{
"name": "item1"
},
{
"name": "needleGroup",
"children": [
{
"name": "needleNestedGroup",
"children": [
{
"name": "item3"
},
{
"name": "needleNestedDeeperGroup",
"children": [
{
"name": "needle"
}]
}]
}]
}];
expected output
getPath(myObj, "name", "needle"):
[0, "name"]
["2","children","0","children","1","children","0","name"]
However, I have now an object that contains these key-values multiple times, so I have multiple matches.
How can I get all of them in an array?
My current function is just stopping after it finds the first match. The fact, that it's recursive makes things very complicated for me
I would write this atop a more generic findAllPaths function that accepts a predicate and finds the paths of all nodes in the object that match that predicate. With that, then findPathsByName is as simple as (target) => findAllPaths (({name}) => name == target).
In turn, I build findAllPaths on pathEntries, variants of which I use all the time. This function turns an object into an array of path/value pairs. Some versions only generate the leaf nodes. This one generate it for all nodes, including the root (with an empty path.) The basic idea of this function is to turn something like:
{a: 'foo', b: {c: ['bar', 'baz'], f: 'qux'}}
into this:
[
[[], {a: 'foo', b: {c: ['bar', 'baz'], f: 'qux'}}],
[['a'], 'foo'],
[['b'], {c: ['bar', 'baz'], f: 'qux'}],
[['b', 'c'], ['bar', 'baz']],
[['b', 'c', 0], 'bar'],
[['b', 'c', 1], 'baz'],
[['b', 'f'], 'qux']
]
where the first item in every subarray is a path and the second a reference to the value at that path.
Here is what it might look like:
const pathEntries = (obj) => [
[[], obj],
...Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (
([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v]
)
)
: []
]
const findAllPaths = (predicate) => (o) =>
[...pathEntries (o)] .filter (([p, v]) => predicate (v, p)) .map (([p]) => p)
const findPathsByName = (target) => findAllPaths (({name}) => name == target)
const myObj = [{name: "needle", children: [{name: "group2", children: [{name: "item0"}]}]}, {name: "item1"}, {name: "needleGroup", children: [{name: "needleNestedGroup", children: [{name: "item3"}, {name: "needleNestedDeeperGroup", children: [{name: "needle"}]}]}]}]
console .log (findPathsByName ('needle') (myObj))
.as-console-wrapper {max-height: 100% !important; top: 0}
The question asked for string values for the array indices. I prefer the integer values myself as done here, but you simplify the function a bit:
- ([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v]
+ ([p, v]) => [[k, ... p], v]
Instead of returning the value, you could push it to an array and keep iterating.
At the end of the function, you return the array.
function getPath(obj, givenKey, givenValue) {
let matches = [];
for (var key in obj) {
if (obj[key] && typeof obj[key] === "object") {
var result = getPath(obj[key], givenValue, givenKey);
if (result) {
result.unshift(key)
matches.push(...result);
}
} else if (obj[key] === givenValue && key === givenKey) {
matches.push(key);
}
}
return matches;
}

How to build map for large json with unknown structure

I have large json data with unknown depth and I need to build a map in the following format of result.
const json = {
1: {
11: {
111: [{ "111-0": "b" }, { "111-1": [{ "111-1-0": "vs" }] }],
112: "asasd",
...
},
12: [{ "12-0": "sd" }],
...
},
2: [{ "2-0": "sd" }],
....
};
const result = {
"1::11::111::A0::111-0": "b",
"1::11::111::A1::111-1::A0::111-1-0": "vs",
"1::11::112": "asasd",
"1::12::A0::12-0": "sd",
"2::A0::2-0": "sd",
};
I think recursion is a good way to solve this but I am not able to implement recursion properly.
This is my current progress. Which gives incorrect output.
const buildRecursion = (json, r, idx = 0, prev = "") => {
Object.keys(json).forEach((key) => {
prev += key + "::";
if (Array.isArray(json[key])) {
for (let [i, v] of json[key].entries()) {
buildRecursion(v, r, i, prev);
}
} else if (typeof json[key] === "object") {
buildRecursion(json[key], r, "", prev);
} else {
if (idx === "") {
r[prev + "::" + key + "::"] = json[key];
} else {
r[prev + "::" + key + "::" + "::A" + idx] = json[key];
}
}
});
};
I'm glad to say, you're on the right track. All I did was clean up your variables
(especialy your handling of prev) and it works fine.
Other notes,
use '' instead of "" for strings
consider using template strings (backticks) for concatenating strings instead of + when doing so is cleaner (more often than not).
I renamed the vars json -> input, r -> output, prev -> key for clarity.
let input = {
1: {
11: {
111: [{"111-0": "b"}, {"111-1": [{"111-1-0": "vs"}]}],
112: "asasd",
},
12: [{"12-0": "sd"}],
},
2: [{"2-0": "sd"}],
};
let buildRecursion = (input, output = {}, key = []) => {
if (Array.isArray(input))
input.forEach((v, i) =>
buildRecursion(v, output, [...key, `A${i}`]));
else if (typeof input === 'object')
Object.entries(input).forEach(([k, v]) =>
buildRecursion(v, output, [...key, k]));
else
output[key.join('::')] = input;
return output;
};
let result = buildRecursion(input);
console.log(result);
// {
// "1::11::111::A0::111-0": "b",
// "1::11::111::A1::111-1::A0::111-1-0": "vs",
// "1::11::112": "asasd",
// "1::12::A0::12-0": "sd",
// "2::A0::2-0": "sd",
// }
You could use reduce method and forEach if the value is array. Then you can use Object.assign to assign recursive call result to the accumulator of reduce which is the result object.
const json = {"1":{"11":{"111":[{"111-0":"b"},{"111-1":[{"111-1-0":"vs"}]}],"112":"asasd"},"12":[{"12-0":"sd"}]},"2":[{"2-0":"sd"}]}
function f(data, prev = '') {
return Object.entries(data).reduce((r, [k, v]) => {
const key = prev + (prev ? '::' : '') + k
if (typeof v === 'object') {
if (Array.isArray(v)) {
v.forEach((o, i) => Object.assign(r, f(o, key + `::A${i}`)))
} else {
Object.assign(r, f(v, key))
}
} else {
r[key] = v
}
return r
}, {})
}
const result = f(json);
console.log(result)
(Updated to handle a missing requirement for the 'A' in the front of array indices.)
You can build this atop a function which associates deeper paths with the value for all leaf nodes. I use variants of this pathEntries function in other answers, but the idea is simply to traverse the object, collecting leaf nodes and the paths that lead to it.
const pathEntries = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (([p, v]) => [[Array.isArray(obj) ? Number(k) : k, ... p], v])
)
: [[[], obj]]
const compress = (obj) =>
Object .fromEntries (
pathEntries (obj)
.map (([p, v]) => [p.map(n => Number(n) === n ? 'A' + n : n) .join ('::') , v])
)
const json = {1: {11: {111: [{ "111-0": "b" }, { "111-1": [{ "111-1-0": "vs" }] }], 112: "asasd", }, 12: [{ "12-0": "sd" }], }, 2: [{ "2-0": "sd" }]}
console .log (compress (json))
The result of pathEntries on your data would look like this:
[
[["1", "11", "111", 0, "111-0"], "b"],
[["1", "11", "111", 1, "111-1", 0, "111-1-0"], "vs"],
[["1", "11", "112"], "asasd"],
[["1", "12", 0, "12-0"], "sd"],
[["2", 0, "2-0"], "sd"]
]
Then compress maps those path arrays into strings, adding the 'A' to the front of the numeric paths used for arrays and calls Object .fromEntries on those results. If you're working in an environment without Object .fromEntries it's easy enough to shim.
While compress is specific to this requirement, pathEntries is fairly generic.

How to write recursive javascript function to achieve desired output

I need some help in writing recursive javascript function to achieve something but I am really struggling to achieve that. Can someone please help me or guide me on this.
I have nested tree something like this, Group1 has child Group2 and Group2 has child Group3. Group3 has no child.
[
{
name: "Group1",
children: [{
name: "Group2",
children: [{
name: "Group3"
children: []
}],
}]
},
{
name: "Group5",
children: [],
},
{
name: "Group6",
children: [],
},
{
name: "Group7",
children: [{
name: "Group 10",
children: [
{
name: 'Group2'
children: [{
name: "Group3"
children: []
}],
}
]
}],
}
]
If I search Group2, function should return all parents and child elements separately in an array. How can I achieve this.
Example:
Input: "Group2"
Output:
[
{
name: "Group1",
children:[
{
name:"Group2",
children :[
{
name:"Group3",
children:[]
}
]
}
]
},
{
name:"Group2",
children: [
{
name:"Group3",
children:[]
}
]
},
{
name:"Group3",
children:[]
},
{
name:"Group7",
children :[
{
name:"Group10",
children:[
{
name:"Group2",
children: [
{
name:"Group3",
children :[]
}
]
}
]
}
],
},
{
name:"Group10",
children:[
{
name:"Group2",
children:[
{
name:"Group3",
children":[]
}
]
}
]
}
]
If I search 'Group2', it should give me complete node separately in a list in which this term exist. For example: 'Group 2' is child of 'Group 1' so complete node tree should be in list [Group1, Group2, Group3] and then 'Group2' is also child of 'Group10', which is also child of 'Group7', so combining complete family for this [Group7, Group10, Group2, Group3]. So complete output should be like all distinct nodes [Group1, Group2, Group3, Group7, Group10 ]. Is this making sense??
Any help will be appreciated.
I was writing code something like this, this works but don't know how to do it better.
const groups = this.props.groups;
const filter = (group: Group) => group.name.toLowerCase().includes(searchTerm);
if (this.state.searchTerm !== '') {
groups = groups.filter(filter);
groups = this.groupHierarchy(groups);
}
private groupHierarchy = (filteredGroups: Group[]) => {
const groups: Group[] = filteredGroups;
groups.map((filteredGroup: Group) =>
this.addAllChildren(filteredGroup, groups));
groups.map((filteredGroup: Group) =>
this.addAllParents(filteredGroup, groups));
return groups;
}
private addAllChildren = (filteredGroup: Group, childGroups: Group[]) => {
const groups = childGroups;
if (filteredGroup.children) {
filteredGroup.children.map((child: Group) => {
if (!groups.find(a => a.id === child.id)) {
groups.push(child);
}
this.addAllChildren(child, groups);
});
}
return groups;
}
private addAllParents = (filteredGroup: Group, parentGroups: Group[]) => {
const groups = parentGroups;
if (filteredGroup.parent && filteredGroup.parent.id) {
const parentGroup = this.props.groups.find(group => group.id === filteredGroup.parent!.id)!;
if (!groups.find(a => a.id === parentGroup.id)) {
groups.push(parentGroup);
}
this.addAllParents(parentGroup, groups);
}
return groups;
}
This version makes generic functions for checking whether a node or one of its descendants matches a generic predicate and, using that, for collecting all the nodes matching a predicate along with all their ancestors and children.
Using the latter, we write a simple function which accepts a target name, and returns a function which will find all the nodes with a name property that matches the target, along with all their ancestors and children:
const hasMatch = (pred) => ({children = [], ...rest}) =>
pred (rest) || children .some (hasMatch (pred))
const collectFamily = (pred) => (xs) =>
xs .flatMap (
x => pred(x)
? [x, ...(x.children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x.children || [])]
: []
)
const collectFamilyByName = (target) =>
collectFamily((({name}) => name == target))
const log = (fn) => (inputs) => // JSON.stringify avoids StackOverflow console artifacts
inputs .forEach (o => console .log (JSON .stringify (fn (o), null, 2)))
const inputs = [
[{name: "Group 1", children: [{name: "Group 2", children: [{name: "Group 3", children: []}]}]}, {name: "Group 5", children: []}],
[{name: "Group 1", children: [{name: "Group 2", children: [{name: "Group 3", children: []}]}]}, {name: "Group 5", children: [{name: 'Group 2', children: []}]}],
]
log (collectFamilyByName ('Group 2')) (inputs)
.as-console-wrapper {max-height: 100% !important; top: 0}
I have two example cases. The first is the one from the question. The second, following a comment on another answer, adds to the 'Group 5' object a child of 'Group 2', and it is collected as I believe you wish.
The code is fairly simple, but I see two potential disadvantages. First, it recurs separately over the tree for testing and collecting. I'm sure there is a relatively clean way to combine them, but I don't see it right away. Second, this will fail to collect descendants of children of nodes that also have the right name. It stops at their children. Again, I imagine there's a quick fix for that, but I'm not seeing it at the moment.
Update - code explanation
A comment implied that this code could use explanation. Here's an attempt
We look first at the simplest function, the main one, collectFamilyByName:
const collectFamilyByName = (target) =>
collectFamily((({name}) => name == target))
This function takes a target string and calls collectFamily passing a function that uses that target. We don't know yet what collectFamily will do; we just know that it takes a predicate function (one returning true or false) and returns to us a new function.
We could have written this in a few different ways. Here's one alternative:
const testMatch = function (target) {
return function (element) {
return element.name == target
}
}
const collectFamilyByName = function (target) {
return collectFamily (testMatch (target))
}
which by simple substitution would be equivalent to
const collectFamilyByName = function (target) {
return collectFamily (function (element) {
return element.name == target
})
}
By using more modern arrow functions for the inner function, this would become
const collectFamilyByName = function (target) {
return collectFamily (element => element.name == target)
}
and then for the outer one:
const collectFamilyByName = (target) =>
collectFamily (element => element.name == target)
And finally, we can use parameter destructuring to remove the element parameter, like this:
const collectFamilyByName = (target) =>
collectFamily (({name}) => name == target)
Now, in order to understand collectFamily, we have to understand hasMatch. Let's expand this again to ES5 style:
const hasMatch = function (pred) {
return function (element) {
return pred (element) ||
element .children .some (function (child) {
return hasMatch (pred) (child)
})
}
}
This is standard ES5 code, almost, but not quite, equivalent to the version above. Here we accept a predicate and return a function which accepts an element and returns a boolean. It will be true if the predicate returns true for the element, or, recurring on the element's children, if hasMatch passed the same predicate and then passed each child, returns true on any of them, using Array.prototype.some.
This is simplified into the above again using arrow functions and parameter destructuring. The one difference is that the ES5 function applies the predicate to the entire object and the ES6 one applies it to a copy of the object that doesn't include children. If this is not desired, we could skip the parameter destructuring here, but I think it usually makes sense to do it this way.
Finally, the main function is collectFamily, which looks like this:
const collectFamily = (pred) => (xs) =>
xs .flatMap (
x => pred(x)
? [x, ...(x.children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x.children || [])]
: []
)
I won't go through the ES6 -> ES5 exercise here. It works just the same as in the other two. Instead, let's look at flatMap and the spread syntax.
flatMap is an operation on arrays that works much like map, except that it expects the function to return an array of objects on every call, which it then combines into a single array.
[10, 20, 30] .flatMap ((n) => [n, n + 1, n + 2])
//=> [10, 11, 12, 20, 21, 22, 30, 31, 32]
the spread syntax uses the token ... to spread the contents of an array
(or an object, not relevant here) into another array. So if xs held [1, 2, 3] then [5, 10, ...xs, 20] would yield [5, 10, 1, 2, 3, 20].
Knowing these, we can understand collectFamily. It accepts a predicate (which we already know we saw will be one that matches on objects whose name property has the same value as our target value) and returns a function which takes an array of objects. It calls flatMap on this array, passing a function, so we know that that function must return an array of values, given one element in the original array.
If I were to rewrite this, I might lay it out a little differently to make the body of that function slightly more clear, perhaps like this:
const collectFamily = (pred) => (xs) =>
xs .flatMap (
(x) =>
pred (x)
? [x, ...(x .children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x .children || [])]
: []
)
And the function, which is passed an array value with parameter name x, would have this body:
pred (x)
? [x, ...(x.children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x.children || [])]
: []
We return one of three different values.
In the first case, if the predicate matches our current value (again, remember in this case, that's if the name property of x matches the target, such as 'Group 2'), then we return an array including x and all of its children. We use the || [] so that if x .children were not defined, we would still have an iterable object to spread into our array. It doesn't matter for the sample data as supplied, but it's useful in many cases.
In the second case, if we have a match somewhere nested more deeply, as reported by hasMatch, we return an array including this node, and all the results found by recursively calling collectFamily with the same predicate against the list of children (again, defaulted to an empty array if they don't exist.)
And in the third case, if neither of those is true, we simply return an empty array.
So that's how this works. There's no magic here, but if you're new to the language, some of the more modern features may seem a bit obscure. I promise, though, that with a little practice, they'll become second-nature. They make for much simpler code, and, once you're used to the syntax, they seem to me much easier to read as well.
You could take a recursive approach with a short circuit on find.
To get all parents, the function contains another parameter for visited parents.
const
getFromTree = (tree, name, parents = []) => {
let result;
tree.some(node => result = node.name === name
? [...parents, node, ...node.children]
: getFromTree(node.children, name, [...parents, node])
);
return result;
},
tree = [{ name: "Group 1", children: [{ name: "Group 2", children: [{ name: "Group 3", children: [] }] }] }, { name: "Group 5", children: [] }];
console.log(getFromTree(tree, "Group 1"));
console.log(getFromTree(tree, "Group 2"));
console.log(getFromTree(tree, "Group 3"));
console.log(getFromTree(tree, "Group 5"));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Deep Find, Remove, Update in a hierarchical array

Let's assume we have a tree like:
const items = [
{
"id": 1
},
{
"id": 2,
"items": [
{
"id": 3
},
{
"id": 4,
"items": []
}
]
}
];
I'm looking for an efficient way to remove/update an item by its unique id; the main issue is we do not know the exact path of item, and worse: the tree may have n levels, and what we have is just an id.
delete a node and its descendants
Here's an effective technique using flatMap and mutual recursion -
del accepts an array of nodes, t, a query, q, and calls del1 on each node with the query
del1 accepts a single node, t, a query, q, and calls del on a node's items
const del = (t = [], q = null) =>
t.flatMap(v => del1(v, q)) // <-- 1
const del1 = (t = {}, q = null) =>
q == t.id
? []
: { ...t, items: del(t.items, q) } // <-- 2
const data =
[{id:1},{id:2,items:[{id:3},{id:4,items:[]}]}]
const result =
del(data, 3)
console.log(result)
In the output, we see node.id 3 is removed -
[
{
"id": 1,
"items": []
},
{
"id": 2,
"items": [
{
"id": 4,
"items": []
}
]
}
]
What if we only want to delete the parent but somehow keep the parent's items in the tree? Is there a way to write it without mutual recursion? Learn more about this technique in this related Q&A.
everything is reduce
If you know filter but haven't used flatmap yet, you might be surprised to know that -
filter is a specialised form of flatmap
flatmap is a specialised form of reduce
const identity = x =>
x
const filter = (t = [], f = identity) =>
flatmap // 1
( t
, v => f(v) ? [v] : []
)
const flatmap = (t = [], f = identity) =>
t.reduce // 2
( (r, v) => r.concat(f(v))
, []
)
const input =
[ "sam", "alex", "mary", "rat", "jo", "wren" ]
const result =
filter(input, name => name.length > 3)
console.log(result)
// [ "alex", "mary", "wren" ]

Categories

Resources