Find path to ALL matching key values in nested object - javascript

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

Related

Iterative depth-first traversal with remembering value's paths

I need help with specific implementation of iterative depth first traversal algorithm.
I have an object like this (it's just an example, object might have more properties and be deeper nested):
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
What I need is a function that would traverse the whole object and return an array like this:
[
{"a": 1},
{"b.c.d.e": 2},
{"b.c.d.f": 3},
{"b.g.0.h": 4},
{"b.g.0.i": 5},
{"b.g.1.j": 6},
{"b.g.1.k": 7},
]
I managed to create an algorithm that sort of solves my problem, but needs one additional step in the end. Result of the algorithm is an array of strings like that:
[
'a^1',
'b.c.d.e^2',
'b.c.d.f^3',
'b.g.0.h^4',
'b.g.0.i^5',
'b.g.1.j^6',
'b.g.1.k^7'
]
so in order to achieve what I want I have to do one full iteration over the result of my algorithm, split strings by ^ symbol and then create objects based on that.
This is the part that I need help with - how can I improve/change my solution so I don't need to do that last step?
function dft(root) {
let stack = [];
let result = [];
const isObject = value => typeof value === "object";
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
if (isObject(node)) {
Object.entries(node).forEach(([childNodeKey, childNodeValue]) => {
if (isObject(childNodeValue)) {
const newObject = Object.fromEntries(
Object.entries(childNodeValue).map(([cnk, cnv]) => {
return [`${childNodeKey}.${cnk}`, cnv];
})
);
stack.push(newObject);
} else {
stack.push(`${childNodeKey}^${childNodeValue}`);
}
})
} else {
result.push(node);
}
}
return result.reverse();
}
I'd keep pairs <keys,value> in the stack and only create a string key when storing a newly created object:
function dft(obj) {
let stack = []
let res = []
stack.push([[], obj])
while (stack.length) {
let [keys, val] = stack.pop()
if (!val || typeof val !== 'object') {
res.push({
[keys.join('.')]: val
})
} else {
Object.entries(val).forEach(p => stack.push([
keys.concat(p[0]),
p[1],
]))
}
}
return res.reverse()
}
//
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
console.log(dft(root))
You can push the childNodeKey childNodeValue pair directly as an object to your result array.
Change
stack.push(`${childNodeKey}^${childNodeValue}`);
to
const newEntry = {}
newEntry[childNodeKey] = childNodeValue
result.push(newEntry);
or with ES2015 syntax (you would need a transpiler for browser compatibility)
result.push({[childNodeKey]: childNodeValue});
Complete function:
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
function dft(root) {
let stack = [];
let result = [];
const isObject = value => typeof value === "object";
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
if (isObject(node)) {
Object.entries(node).forEach(([childNodeKey, childNodeValue]) => {
if (isObject(childNodeValue)) {
const newObject = Object.fromEntries(
Object.entries(childNodeValue).map(([cnk, cnv]) => {
return [`${childNodeKey}.${cnk}`, cnv];
})
);
stack.unshift(newObject);
} else {
const newEntry = {}
newEntry[childNodeKey] = childNodeValue
result.push({[childNodeKey]: childNodeValue});
}
})
} else {
result.push(node);
}
}
return result;
}
console.log(dft(root))
As you mentioned, you almost got it complete. Just make the array entry an object just before pushing it into result. By splitting Array.prototype.split('^') you can get 'b.g.0.h^4' >>> ['b.g.0.h', '4']. So, rest is a cake:
if (isObject(node)) {
...
} else {
const keyAndValue = node.split('^')
// approach 1)
// const key = keyAndValue[0]
// const value = keyAndValue[1]
// dynamic key setting
// result.push({[key]: value});
// approach 2)
// or in short,
// dynamic key setting
result.push({[keyAndValue[0]]: keyAndValue[1]});
}
You could use a stack where each item has an iterator over the children, and the path up to that point:
function collect(root) {
const Node = (root, path) =>
({ iter: Object.entries(root)[Symbol.iterator](), path });
const result = [];
const stack = [Node(root, "")];
while (stack.length) {
const node = stack.pop();
const {value} = node.iter.next();
if (!value) continue;
stack.push(node);
const [key, child] = value;
const path = node.path ? node.path + "." + key : key;
if (Object(child) !== child) result.push({ [path]: child });
else stack.push(Node(child, path));
}
return result;
}
const root = {a:1,b:{c:{d:{e:2,f:3}},g:[{h:4,i:5},{j:6,k:7}]}};
console.log(collect(root));
I would suggest that the quickest fix to your code is simply to replace
return result.reverse();
with
return result.reverse()
.map ((s, _, __, [k, v] = s .split ('^')) => ({[k]: v}));
But I also think that we can write code to do this more simply. A function I use often will convert your input into something like this:
[
[["a"], 1],
[["b", "c", "d", "e"], 2],
[["b", "c", "d", "f"], 3],
[["b", "g", 0, "h"], 4],
[["b", "g", 0, "i"], 5],
[["b", "g", 1, "j"], 6],
[["b", "g", 1, "k"], 7]
]
and a fairly trivial wrapper can then convert this to your output. It could look like this:
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 transform = (o) =>
pathEntries (o)
.map (([k, v]) => ({[k .join ('.')] : v}))
const root = {a: 1, b: {c: {d: {e: 2, f: 3, }}, g: [{h: 4, i: 5, }, {j: 6, k: 7}]}}
console .log (transform (root))
.as-console-wrapper {max-height: 100% !important; top: 0}
I don't know your usecase, but I would find this output generally more helpful:
{
"a": 1,
"b.c.d.e": 2,
"b.c.d.f": 3,
"b.g.0.h": 4,
"b.g.0.i": 5,
"b.g.1.j": 6,
"b.g.1.k": 7
}
(that is, one object with a number of properties, rather than an array of single-property objects.)
And we could do this nearly as easily, with a small change to transform:
const transform = (o) =>
pathEntries (o)
.reduce ((a, [k, v]) => ((a[k .join ('.')] = v), a), {})

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.

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.

How to use Array.protoype.map() on array of objects to filter out some specific keys based on it's values?

I have the following array of objects, in which some specific keys have a value which is either a string or an array. I want to get the keys with string values as it is and remove "NA" from keys with array values.
I am trying to achieve the same by doing .map method on the array, check the type of the value of each key in the data object, if an array then use .filter to remove the "NA" values.
var dataArr = [
{
a: 'foo',
b: [1, 2, "NA", "NA", 3],
c: ["NA", 6]
},
{
a: 'bar',
b: ["NA", "NA", "NA"],
c: []
}
];
dataArr.map(dataObj => {
for (let key in dataObj) {
if (key !== 'a')
dataObj[key] = dataObj[key].filter(val => { if(val != "NA") return val})
}
return dataObj;
});
The above block of code works as expected but I want a better and future-proof solution. Moreover, this looks bad too.
If you just want to update the original array, you could loop through the array using forEach. Then loop through each object's keys using for...in and check if it is an array using Array.isArray(). Update the property after filtering out the NA value
const dataArr = [{a:'foo',b:[1,2,"NA","NA",3],c:["NA",6]},{a:'bar',b:["NA","NA","NA"],c:[]}];
dataArr.forEach(o => {
for (const key in o) {
if (Array.isArray(o[key]))
o[key] = o[key].filter(s => s !== "NA")
}
})
console.log(dataArr)
If you want to get a new array without mutating the original objects, you can use map like this:
const dataArr = [{a:'foo',b:[1,2,"NA","NA",3],c:["NA",6]},{a:'bar',b:["NA","NA","NA"],c:[]}];
const newArray = dataArr.map(o => {
const newObj = {};
for (const key in o) {
if (Array.isArray(o[key]))
newObj[key] = o[key].filter(s => s !== "NA")
else
newObj[key] = o[key]
}
return newObj;
})
console.log(newArray)
You can do that using nested map() and filter()
First of all use map() on the main array.
Then get the entries of each object using Object.entries()
Then use map() on the entries of each object.
Return the value as it is if the Array.isArray is false otherwise return the filtered value of array.
Finally use Object.fromEntries() to make an object.
var dataArr = [
{
a: 'foo',
b: [1, 2, "NA", "NA", 3],
c: ["NA", 6]
},
{
a: 'bar',
b: ["NA", "NA", "NA"],
c: []
}
];
const res = dataArr.map(x =>
Object.fromEntries(
Object.entries(x)
.map(([k,v]) =>
[k,Array.isArray(v) ? v.filter(b => b !== "NA") : v])
)
)
console.log(res)
Use for...of on the object's entries. For each entry check if it's an array. If it's an array filter, and then assign to the new object. If not, assign without filtering. This will not mutate the original array.
const dataArr = [{"a":"foo","b":[1,2,"NA","NA",3],"c":["NA",6]},{"a":"bar","b":["NA","NA","NA"],"c":[]}]
const result = dataArr.map(dataObj => {
const filterObject = {};
for (const [key, val] of Object.entries(dataObj)) {
filterObject[key] = Array.isArray(val) ?
dataObj[key].filter(val => val != 'NA')
:
val;
}
return filterObject;
});
console.log(result);
const dataArr = [{
a: 'foo',
b: [1, 2, "NA", "NA", 3],
c: ["NA", 6]
},
{
a: 'bar',
b: ["NA", "NA", "NA"],
c: []
}
];
const res = dataArr.map((obj) => {
for(let i in obj){
if(Array.isArray(obj[i])){
obj[i] = obj[i].filter(el=>el!=="NA")
}
}
return obj;
});
console.log('#>res', res)
I guess this will be a better code refactor,pls provide with the desired output also:
dataArr.forEach(object=>{
for (let value in object){
let isArr = Object.prototype.toString.call(data) == '[object Array]';
isArr? object[value] = object[value].filter(val => {return val != "NA"}):null}
})

best way to convert object with arrays to array with objects and viceversa

what is the best way to convert an object of arrays to an array of objects and vice-versa
{
category : ['a','b','c'],
title : ['e','f','g'],
code : ['z','x','v']
}
To
[
{
category : 'a',
title : 'e',
code : 'z'
},
{
category : 'b',
title : 'f',
code : 'x'
},
{
category : 'c',
title : 'g',
code : 'v'
},
]
You could use two function for generating an array or an object. They works with
Object.keys for getting all own property names,
Array#reduce for iterating an array and collecting items for return,
Array#forEach just fo itarating an array.
function getArray(object) {
return Object.keys(object).reduce(function (r, k) {
object[k].forEach(function (a, i) {
r[i] = r[i] || {};
r[i][k] = a;
});
return r;
}, []);
}
function getObject(array) {
return array.reduce(function (r, o, i) {
Object.keys(o).forEach(function (k) {
r[k] = r[k] || [];
r[k][i] = o[k];
});
return r;
}, {});
}
var data = { category: ['a', 'b', 'c'], title: ['e', 'f', 'g'], code: ['z', 'x', 'v'] };
console.log(getArray(data));
console.log(getObject(getArray(data)));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use map() and forEach()
var obj = {
category : ['a','b','c'],
title : ['e','f','g'],
code : ['z','x','v']
}
var result = Object.keys(obj).map(function(e, i) {
var o = {}
Object.keys(obj).forEach((a, j) => o[a] = obj[a][i])
return o
})
console.log(result)
Here is solution using reduce method and arrow function.
var obj={
category : ['a','b','c'],
title : ['e','f','g'],
code : ['z','x','v']
}
var result=obj[Object.keys(obj)[0]].reduce((a,b,i)=>{
var newObj={};
Object.keys(obj).forEach(function(item){
newObj[item]=obj[item][i];
});
return a.concat(newObj);
},[]);
console.log(result);
Not an answer to the original question, but thought I'd share this as I came here looking for a solution to a related problem: I needed the Cartesian product of values of uneven length. What I'm using for that:
/**
* Get the Cartesian product of a list of array args.
*/
const product = (...args) => args.length === 1
? args[0].map(x => [x])
: args.reduce(
(a, b) => Array.from(a).flatMap(
v1 => Array.from(b).map(v2 => [v1, v2].flat())
)
);
/**
* Map[String,Array[Any]] -> Array[Map[String,Any]]
*/
const arrayValuesToObjectArray = (obj) =>
product(...Object.values(obj)).map(values => Object.fromEntries(
Object.keys(obj).map((k, idx) => [k, values[idx]])
));
console.log(arrayValuesToObjectArray({
hello: ["this", "is", "a", "test"]
}));
console.log(arrayValuesToObjectArray({
one: ["foo"],
two: ["bar", "baz"],
three: ["this", "is", "three"],
}));
console.log(arrayValuesToObjectArray({
one: "foo",
two: "bar baz",
three: "this is three",
}));

Categories

Resources