Compare JSON Keys to get missing elements - javascript

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.

Related

Transform dot notation to a tree data form

I have object oriented data in the form:
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
...
]
which I would like to transform into a tree structure, to be able to serve them with a tree component (https://github.com/vinz3872/vuejs-tree in particular). The require form is the following:
var ok = [
{
text: "foo",
state: { expanded: false },
nodes: [
{
id: 1,
path: "foo.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
id: 2,
path: "foo.lol2",
text: "lol2",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
text: "barbar",
state: { expanded: false },
nodes: [
{
id: 4,
path: "bar.barbar.kk",
text: "kk",
checkable: true,
state: { checked: false },
},
]
},
]
}
]
I am aware that I should use recursion and I have tried all relevan posts in stackoverflow, i.e. How to build a JSON tree structure using object dot notation.
My main problem is that I have to somehow preserve the information of the full path to the leaves of the tree. As a newbie in js I lost myself in counters and callback for days without any luck.
I would appreciate your help.
Thank you in advance
Basically you could use forEach then split each string into array and then use reduce on that. Then you build nested object where the keys are current paths and also ad to result array.
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
]
const result = []
const levels = {
result
}
let prev = ''
let id = 1
alist.forEach(str => {
str.split('.').reduce((r, text, i, arr) => {
const path = prev += (prev.length ? '.' : '') + text
if (!r[path]) {
r[path] = {result: []}
const obj = {
id: id++,
text,
}
if (i === 0) {
obj.state = {expanded: false}
} else {
obj.state = {checked: false}
obj.checkable = true
obj.path = path
}
obj.nodes = r[path].result
r.result.push(obj)
}
if (i === arr.length - 1) {
prev = ''
}
return r[path]
}, levels)
})
console.log(result)
I found that it was easiest to do this transformation in two steps. The first converts your input into this format:
{
foo: {
lol1: {},
lol2: {}
},
bar: {
barbar: {
kk: {}
},
lol1: {}
},
}
The second uses just this format to create your desired structure. This has two advantages. First, I have tools lying around that make it easy to create this structure from your input. Second, this structure embeds enough information to create your output, with only one branching construct: whether the value at a path is an empty object or has properties. This makes the generation code relatively simple:
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 reformat = (o, path = [], nextId = ((id) => () => String (++ id)) (0)) =>
Object .entries (o) .map (([k, v]) => Object .entries (v) .length > 0
? {text: k, state: {exapanded: false}, nodes: reformat (v, [...path, k], nextId)}
: {id: nextId (), path: [...path, k] .join('.'), text: k, checkable: false, state: {checked: false}}
)
const transform = (pathTokens) =>
reformat (pathTokens
.map (s => s .split ('.'))
.reduce ((a, path) => setPath (path) ({}) (a), {})
)
const alist = ['foo', 'foo.lol1', 'foo.lol2', 'bar.lol1', 'bar.barbar.kk']
console .log (transform (alist))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with setPath, which takes a path, in a format such as ['bar', 'barbar', 'kk'], the value to set at that path, and an object to shallow clone with this new property along that path. Thus setPath (['foo', 'bar', 'baz']) (42) ({foo: {qux: 7}, corge: 6}) yields {foo: {qux: 7, bar: {baz: 42}}, corge: 6}. (There's a little more in this reusable function to also handle array indices instead of string object paths, but we can't reach that from this input format.)
Then we have reformat, which does the format conversion. It simply builds a different input object based upon whether the input value is an empty object.
Finally, transform maps a splitting function over your input array to get the path structure needed for setPath, folds the results into an initially empty object by setting every path value to an empty object, yielding our intermediate format, which we then pas to reformat.
There is one thing I really don't like here, and that is the nextId function, which is a stateful function. We could just have easily used a generator function, but whatever we do here, we're using state to build this output and that bothers me. If someone has a cleaner suggestion for this, I'd love to hear it.

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

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

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

How to convert an object with keys representing a number list ("1.1.1") into a multidimensional array

Let's consider I have the following plain object:
var numberedList = {
"1": "Text of 1",
"1.1": ["array of 1.1"],
"1.2": {
key: "object of 1.2"
},
"1.1.1": 999,
"1.2.1": true,
"1.2.2": function () {
return "function of 1.2.2";
},
"1.3": null,
"2": 2
}
I want to accomplish the following multidimensional array:
To easy explain myself:
[
["1",
[
[
[
"1.1",
[
"1.1.1"
]
]
],
[
[
"1.2",
[
"1.2.1",
"1.2.2"
]
],
],
"1.3"
]
],
"2"
]
Final array:
[
[
"Text of 1",
[
[
[
["array of 1.1"],
[
999
]
]
],
[
[
{
key: "object of 1.2"
},
[
true,
function()
{
return "function of 1.2.2";
}
]
],
],
null
]
],
2
]
Note that the deepest elements are not wrapped into an array.
How can I do a generic recursion function to accomplish this in pure vanilla js?
The main objective is to perform a tree of console.group() calls such as the console.groupEnd()would be also called on the end of each tree branch. It would be a bonus to help me to accomplish this also – given the object on the top execute these console calls.
Here is my solution,
var numberedList = { "1": "Text of 1", "1.1": ["array of 1.1"], "1.2": { key: "object of 1.2" }, "1.1.1": 999, "1.2.1": true, "1.2.2": function () { return "function of 1.2.2"; }, "1.3": null, "2": 2 }
function gen(numberedList) {
let result = Object.keys(numberedList).reduce((res, key) => {
let indexs = key.split(".");
let lastindex = indexs.pop()
indexs.reduce((res, i) => res[i], res)[lastindex] = [key]
return res
}, []);
result.shift(); // Removing first item, Becouse index count from 0;
return result
}
console.log(gen(numberedList))
You need to create a trimer function, to trim this result, for removing brackets ([])
First of all, in the future, please include your attempted code, or at least one relevant version of it, in the question. It's not a problem to refer to JSFiddle or the like for additional context, but the question should be complete enough without it.
Here I use a generic helpers, setPath, which takes a path such as ['foo', 0, 'bar', 'baz'], a new result, say, 'new result', and creates an updated copy of a nested object/array with the value at that path set to the new value:
setPath
(['foo', 1, 'bar', 'baz'])
("new result")
({foo: [{bar: {qux: 3}}, {bar: {qux: 5}}], corge: 8})
//=> {foo: [{bar: {qux: 3}}, {bar: {qux: 5, baz: "new result"}}], corge: 8}
// ^^^^^^^^^^^^^^^^^
(Here integers represent array indices and strings represent object keys.)
Then it's a simple matter of converting your input structure into an array of path-value objects, and reduce-ing that by calling setPath with each of them on a target array.
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 nest = (o) =>
Object .entries (o) .map (
([k, v]) => [k .split ('.') .map (Number), v]
) .reduce ((a, [p, v]) => setPath (p) ([[v]]) (a), []) .slice (1)
const numberedList = {"1": "Text of 1", "1.1": ["array of 1.1"], "1.2": {key: "object of 1.2"}, "1.1.1": 999, "1.2.1": true, "1.2.2": function () {return "function of 1.2.2";}, "1.3": null, "2": 2}
console .log (nest (numberedList))
.as-console-wrapper {max-height: 100% !important; top: 0}
This isn't precisely the output you requested. But I don't make much sense out of that output. You might have to tweak a bit to get it exactly how you want.

Replace a property in arbitrary data structure

Let's say I have an array of arbitrary objects I don't know structure of. I would like to process it in a way that when a property matching some criteria in terms of property name is found at any nesting level, there is some action (mutation) done on it.
As an example, "find all properties with exact name" or "find all properties with a hyphen" and "replace the value with {redacted: true}".
I tried with R.set and R.lensProp but it seems to act on properties at the root level only. Here's my playground. Let's say I'd like to replace baz value with {redacted: true} or run R.map on it when it's an array.
const arr = [
{
id: 5,
name: "foo",
baz: [
{
a: 1,
b: 2
},
{
a: 10,
b: 5
}
],
other: {
very: {
nested: {
baz: [
{
a: 1,
b: 2
}
]
}
}
}
},
{
id: 6,
name: "bar",
baz: []
}
];
const result = R.set(
R.lensProp("baz"),
{
replaced: true,
},
arr[0]
);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>
I feel I'm missing something very basic.
You'll need a recursive function that handles:
Arrays - mapping and calling itself on each item
Other objects - converting the object to array with R.toPairs, mapping the array of pairs, calling the predicate on each [key, value] pair, and calling itself on each value which is also an array
Primitives returning as is
const { curry, cond, is, pipe, toPairs, map, when, last, evolve, identity, fromPairs, T } = R;
const transform = curry((pred, arr) => cond([
[is(Array), map(a => transform(pred, a))], // handle array - map each item
[is(Object), pipe( // handle objects which are not arrays
toPairs, // convert to pairs
map(pipe( // map each pair
pred, // call predicate on the pair
when(pipe(last, is(Object)), evolve([ // handle array values
identity, // return key as is
a => transform(pred, a) // transform the array
])),
)),
fromPairs // convert back to an array
)],
[T, identity], // handle primitives
])(arr))
const redactBaz = transform(
R.when(R.pipe(R.head, R.equals('baz')), R.always(['redacted', true]))
);
const arr = [{"id":5,"name":"foo","baz":[{"a":1,"b":2},{"a":10,"b":5}],"other":{"very":{"nested":{"baz":[{"a":1,"b":2}]}}}},{"id":6,"name":"bar","baz":[]}];
const result = redactBaz(arr);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>
Similar to Ori Drori's answer, we can create generic top-down or bottom-up traversals over all possible JS types, allowing transformation at each step by passing in the current value and, where appropriate, the associated object key or array index to the provided function.
Given you want to replace the entire subtree when finding a matching node, you can process this top-down.
const mapIndexed = R.addIndex(R.map)
function bottomUp(f, val) {
return R.is(Array, val) ? mapIndexed((v, i) => f(bottomUp(f, v), i), val)
: R.is(Object, val) ? R.mapObjIndexed((v, i) => f(bottomUp(f, v), i), val)
: f(val, null)
}
function topDown(f, val) {
function go(val, i) {
const res = f(val, i)
return R.is(Array, res) ? mapIndexed(go, res)
: R.is(Object, res) ? R.mapObjIndexed(go, res)
: res
}
return go(val, null)
}
/////
const arr = [{"id":5,"name":"foo","baz":[{"a":1,"b":2},{"a":10,"b":5}],"other":{"very":{"nested":{"baz":[{"a":1,"b":2}]}}}},{"id":6,"name":"bar","baz":[]}];
const result = topDown(
(v, k) => k == 'baz' ? { redacted: false } : v,
arr
)
console.log(result)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"></script>
One approach you could take is recursively finding all the paths in your object that match the provided key. An example of this can be seen in the findPaths function below. This makes use of R.chain to collect a matching path in a singleton array, or ignore unwanted keys with an empty array. If an object or array is found, it will recursively call the go function over them.
Once you have a list of all the paths you'd like to update, you can make use of R.assocPath to update the values at each path. For example, I have used reduce to iterate through each of the found paths in the redact function below.
const indexedChain = R.addIndex(R.chain)
const findPaths = (prop, obj) => {
function go(prefix, el) {
if (R.is(Array, el)) return indexedChain((v, i) => go(R.append(i, prefix), v), el)
if (R.is(Object, el)) return R.chain(k => {
if (k == prop) return [R.append(k, prefix)]
return go(R.append(k, prefix), el[k])
}, R.keys(el))
return []
}
return go([], obj)
}
const redact = (prop, obj) =>
R.reduce(
(obj_, path) => R.assocPath(path, { redacted: true }, obj_),
obj,
findPaths(prop, obj)
)
//////
const arr = [
{
id: 5,
name: "foo",
baz: [{a: 1, b: 2}, {a: 10, b: 5}],
other: {
very: {
nested: {
baz: [{a: 1, b: 2}]
}
}
}
},
{
id: 6,
name: "bar",
baz: []
}
]
//////
console.log(redact('baz', arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"></script>
Generic Powerhouse
We could write a fairly generic nested object transformation function, which takes two callbacks, one a predicate which given a key and a value decides whether we're going to handle this node, and a second which takes a key and a value and returns a collection of key-value pairs. This lets us not only filter nodes or transform individual nodes but also create new nodes or split a single node in pieces. This has the great power / great responsibility feature, though. Writing the action callback can be uglier than we might want for simple transforms. But we then can write simpler functions built atop this to handle the simpler transforms.
Here's one implementation of such a broad function:
const transform = (pred, action) => (obj) =>
Array .isArray (obj)
? obj .map (transform (pred, action))
: Object (obj) === obj
? Object .fromEntries (
Object .entries (obj) .flatMap (
([k, v]) => pred (k, v)
? action (k, v).map (transform (pred, action))
: [[k, transform (pred, action) (v)]]
)
)
: obj
We could use it like this:
const splitHyphensAndIncrement = transform (
(k) => k .includes ('-'),
(k, v) => k .split ('-') .map ((_k, i) => [_k, v + i])
)
splitHyphensAndIncrement ({a: 1, b: [{c: 2, 'x-y': 10}, {c: 3, 'x-y': 20}], 'x-y-z': 42})
//=> {a: 1, b: [{c: 2, x: 10, y: 11}, {c: 3, x: 20, y: 21}], x: 42, y: 43, z: 44}
Note what it does. It replaces single nodes including hyphens with multiple node, for instance, it replaces the x-y-x: 42 node with three nodes: x: 42, y: 43, z: 44.
This is quite powerful. But often, we don't want to deal with having to return an array of key-value pairs, when all we want to do is to convert our node into another node.
Changing Single Values
We can write a simpler transformer on top of this:
const transformSimple = (pred, action) =>
transform (pred, (k, v) => [[k, action(v)]])
The predicate here still can use the key and the value, but the action does nothing but transform a value into another one. Here's a simple example:
const squareNumbers = transformSimple (
(k, v) => typeof v == 'number',
(n) => n * n
)
squareNumbers({foo: 'a', x: 3, y: 5, z: {bar: 42, x: 7}})
//=> {foo: 'a', x: 9, y: 25, z: {bar: 1764, x: 49}}
Perhaps we want a version that only squares with certain keys. We can change only the predicate:
const squareXyzNumbers = transformSimple (
(k, v) => ['x', 'y', 'z'] .includes (k) && typeof v == 'number',
(n) => n * n
)
squareXyzNumbers ({foo: 'a', x: 3, y: 5, z: {bar: 42, x: 7}})
//=> {foo: "a", x: 9, y: 25, z: {bar: 42, x: 49}}
We can also use this to add sequential id numbers to each object encountered:
const addSequentialIds = transformSimple (
(k, v) => Object (v) === v,
((n) => (obj) => ({_id: ++n, ...obj}))(0)
)
addSequentialIds ({foo: {bar: 'a', baz: {qux: 'b', corge: {grault: 'c'}}}})
//=> {foo: {_id :1, bar: 'a', baz: {_id: 2, qux: 'b', corge: {_id: 3, grault: 'c'}}}}
Changing Keys
We can write a simple wrapper which transforms all keys like this:
const transformKey = (fn) =>
transform (() => true, (k, v) => [[fn(k), v]])
and use it like this:
const shoutKeys = transformKey (
s => s.toUpperCase()
)
shoutKeys ({foo: 1, bar: {baz: 2, qux: {corge: 3, grault: 4}}})
//=> {FOO: 1, BAR: {BAZ: 2, QUX: {CORGE: 3, GRAULT: 4}}}
Or of course we could choose to write a version that uses a predicate to decide whether to transform the key or not.
The point is that this powerful function easily offers us ways to write the simpler generic transformation functions we want, and those can lead to fairly nice code.
More Power?
Could we make this more powerful? Certainly. I'm not going to try it now, but I keep imagining changing
? action (k, v).map (transform (pred, action))
to
? action (k, v, transform (pred, action))
so that your action function receives the key, the value, and the recursive transformer, which you can choose to apply or not to the nodes you generate.
This is undoubtedly more powerful, but also gives you more chances to mess up. That might be worth the trade-off, especially if you mostly use it to create things like transformSimple or transformKey.
Original Question
Here's how we might use this to handle the original question, replacing baz: <something> with baz: {redacted: true}. We don't need the full power of transform, but can use transformSimple:
const redactBaz = transformSimple (
(k, v) => k === 'baz',
(v) => ({redacted: true})
)
const arr = [{id: 5, name: "foo", baz: [{a: 1, b: 2}, {a: 10, b: 5}], other: {very: {nested: {baz: [{a: 1, b: 2}]}}}}, {id: 6,name: "bar", baz: []}]
redactBaz (arr) //=>
// [
// {id: 5, name: 'foo', baz: {redacted: true}, other: {very: {nested: {baz: {redacted :true}}}}},
// {id: 6, name: 'bar', baz: {redacted: true}}
// ]
In Action
We can see all this in action in this snippet:
const transform = (pred, action) => (obj) =>
Array .isArray (obj)
? obj .map (transform (pred, action))
: Object (obj) === obj
? Object .fromEntries (
Object .entries (obj) .flatMap (
([k, v]) => pred (k, v)
? action (k, v) .map (transform (pred, action))
: [[k, transform (pred, action) (v)]]
)
)
: obj
const res1 = transform (
(k) => k .includes ('-'),
(k, v) => k .split ('-') .map ((_k, i) => [_k, v + i])
) ({a: 1, b: [{c: 2, 'x-y': 10}, {c: 3, 'x-y': 20}], 'x-y-z': 42})
const transformSimple = (pred, action) =>
transform (pred, (k, v) => [[k, action(v)]])
const res2 = transformSimple (
(k, v) => typeof v == 'number',
(n) => n * n
)({foo: 'a', x: 3, y: 5, z: {bar: 42, x: 7}})
const res3 = transformSimple (
(k, v) => ['x', 'y', 'z'] .includes (k) && typeof v == 'number',
(n) => n * n
) ({foo: 'a', x: 3, y: 5, z: {bar: 42, x: 7}})
const res4 = transformSimple (
(k, v) => Object (v) === v,
((n) => (obj) => ({_id: ++n, ...obj}))(0)
)({foo: {bar: 'a', baz: {qux: 'b', corge: {grault: 'c'}}}})
const transformKey = (fn) =>
transform (() => true, (k, v) => [[fn(k), v]])
const res5 = transformKey (
s => s.toUpperCase()
) ({foo: 1, bar: {baz: 2, qux: {corge: 3, grault: 4}}})
const arr = [{id: 5, name: "foo", baz: [{a: 1, b: 2}, {a: 10, b: 5}], other: {very: {nested: {baz: [{a: 1, b: 2}]}}}}, {id: 6,name: "bar", baz: []}]
const res6 = transformSimple (
(k, v) => k === 'baz',
(v) => ({redacted: true})
) (arr)
console .log (res1)
console .log (res2)
console .log (res3)
console .log (res4)
console .log (res5)
console .log (res6)
.as-console-wrapper {max-height: 100% !important; top: 0}
Ramda
I'm one of the founders of Ramda and a big fan, but I rarely use it in recursive situations. It isn't particularly designed to help with them.
But if I was writing this in an environment that was already using Ramda, it can certainly help around the edges. is, map, pipe, toPairs, fromPairs, chain are all slightly nicer than their native counterparts, and there's a reasonable ifElse lurking in the Object branch. So we could write this like:
const transform = (pred, action) => (obj) =>
is (Array) (obj)
? map (transform (pred, action)) (obj)
: is (Object) (obj)
? pipe (
toPairs,
chain (apply (ifElse (
pred,
pipe (action, map (transform(pred, action))),
(k, v) => [[k, transform (pred, action) (v)]]
))),
fromPairs
) (obj)
: obj
There also looks to be a cond lurking at the root of this function (note that all the conditions and consequents end by applying our parameter, except the final clause, which could easily be replaced by identity), but I'm not quite sure how to make that work with the recursion.
It's not that Ramda offers nothing here. I slightly prefer this version to the original. But there's not enough difference that I would introduce Ramda to a codebase that wasn't using it just for those minor advantages. And if I were to get that cond working, it might well be a clearly better version.
Update
I did figure out how to get that cond working, and it's nicer code, but still not quite what I'd like. That version looks like this:
const transform = (pred, action) => (o) => cond ([
[is (Array), map (transform (pred, action))],
[is (Object), pipe (
toPairs,
chain (apply (ifElse (pred, pipe (action, map (transform (pred, action))), (k, v) => [[k, transform (pred, action) (v)]]))),
fromPairs
)],
[T, identity]
]) (o)
If I didn't have to have the (o) => cond ([...]) (o) and could just use cond ([...]) I would be very happy with this. But the recursive calls inside the cond won't permit that.

Categories

Resources