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.
Related
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;
}
I need to filter nested Objects by property values. I know similar questions have been asked before, but I couldn't find a solution for a case where the values were stored in an array.
In the provided code sample, I will need to filter the object based on tags. I would like to get objects which include "a" and "b" in the tags array.
const input1 = {
"0":{
"id":"01",
"name":"item_01",
"tags":["a","b"],
},
"1":{
"id":"02",
"name":"item_02",
"tags":["a","c","d"],
},
"2":{
"id":"03",
"name":"item_03",
"tags":["a","b","f"],
}
}
function search(input, key) {
return Object.values(input).filter(({ tags }) => tags === key);
}
console.log(search(input1, "a"));
As an output, I would like to receive the fallowing:
{
"0":{
"id":"01",
"name":"item_01",
"tags":["a","b"],
},
"2":{
"id":"03",
"name":"item_03",
"tags":["a","b","f"],
}
}
Thanks a lot in advance!
Since you want to keep the object structure, you should use Object.entries instead of Object.values and to revert back to object type use Object.fromEntries:
Object.fromEntries(Object.entries(input).filter(...))
To make it work for multiple keys, use every in combination with includes as predicate:
keys.every(key => tags.includes(key))
const input1 = {
"0":{
"id":"01",
"name":"item_01",
"tags":["a","b"],
},
"1":{
"id":"02",
"name":"item_02",
"tags":["a","c","d"],
},
"2":{
"id":"03",
"name":"item_03",
"tags":["a","b","f"],
}
}
function search(input, keys) {
return Object.fromEntries(
Object.entries(input).filter(([, { tags }]) => keys.every(key => tags.includes(key)))
)
}
console.log(search(input1, ["a", "b"]));
function search(input, key) {
Object.values(input).filter(x=>x.tags.includes(key))
}
You can use Object.entries to get the [key, value] pair as an array of an array then you can use filter to filter out the elements that don't contain the element in the key array. Finally, you can use reduce to produce a single value i.e object as a final result
const input1 = {
"0": {
id: "01",
name: "item_01",
tags: ["a", "b"],
},
"1": {
id: "02",
name: "item_02",
tags: ["a", "c", "d"],
},
"2": {
id: "03",
name: "item_03",
tags: ["a", "b", "f"],
},
};
function search(input, key) {
return Object.entries(input)
.filter(([, v]) => key.every((ks) => v.tags.includes(ks)))
.reduce((acc, [k, v]) => {
acc[k] = v;
return acc;
}, {});
}
console.log(search(input1, ["a", "b"]));
function search(input, tagsToFind) {
return Object.values(input).filter(inputItem => {
tagsToFind = Array.isArray(tagsToFind) ? tagsToFind : [tagsToFind];
let tagFound = false;
for (const key in tagsToFind) {
if (Object.prototype.hasOwnProperty.call(tagsToFind, key)) {
const element = tagsToFind[key];
if (inputItem.tags.indexOf(element) === -1) {
tagFound = false;
break;
} else {
tagFound = true;
}
}
}
return tagFound;
})
// ({ tags }) => tags === key);
}
}
I'm trying to write a recursive function to go through an object and return items based off an ID. I can get the first part of this to work, but I'm having a hard time trying to get this function in a recursive manner and could use a fresh set of eyes. The code is below. When you run the snippet, you get an array of 6 items which for the first iteration is what I want, but how can I call my function with the proper parameters to get the nested items? My end goal is to have the all the objects starting with 'Cstm', nested ones too, to be added to the tablesAndValues array. I was trying to model my code after this: Get all key values from multi level nested array JavaScript, but this deals with an array of objects and not an object of objects. Any hint or tips I can get are very appreciated.
JSFiddle: https://jsfiddle.net/xov49jLs/
const response = {
"data": {
"Cstm_PF_ADG_URT_Disposition": {
"child_welfare_placement_value": ""
},
"Cstm_PF_ADG_URT_Demographics": {
"school_grade": "family setting",
"school_grade_code": ""
},
"Cstm_Precert_Medical_Current_Meds": [
{
"med_name": "med1",
"dosage": "10mg",
"frequency": "daily"
},
{
"med_name": "med2",
"dosage": "20mg",
"frequency": "daily"
}
],
"Cstm_PF_ADG_URT_Substance_Use": {
"dimension1_comment": "dimension 1 - tab1",
"Textbox1": "text - tab1"
},
"Cstm_PF_ADG_Discharge_Note": {
"prior_auth_no_comm": "auth no - tab2"
},
"Cstm_PF_ADG_URT_Clinical_Plan": {
"cca_cs_dhs_details": "details - tab2"
},
"container": {
"Cstm_PF_Name": {
"first_name": "same text for textbox - footer",
"last_name": "second textbox - footer"
},
"Cstm_PF_ADG_URT_Demographics": {
"new_field": "mapped demo - footer"
},
"grid2": [
{
"Cstm_PF_ADG_COMP_Diagnosis": {
"diagnosis_label": "knee",
"diagnosis_group_code": "leg"
}
},
{
"Cstm_PF_ADG_COMP_Diagnosis": {
"diagnosis_label": "ankle",
"diagnosis_group_code": "leg"
}
}
]
},
"submit": true
}
};
function getNamesAndValues(data, id) {
const tablesAndValues = [],
res = data;
Object.entries(res).map(([key, value]) => {
const newKey = key.split('_')[0].toLowerCase();
// console.log(newKey) // -> 'cstm'
if (newKey === id) {
tablesAndValues.push({
table: key,
values: value
});
} else {
// I can log value and key and see what I want to push
// to the tablesAndValues array, but I can't seem to get
// how to push the nested items.
// console.log(value);
// console.log(key);
// getNamesAndValues(value, key)
}
});
return tablesAndValues;
}
console.log(getNamesAndValues(response.data, 'cstm'));
To achieve the result with a single push, one can pass the result table to the function when called recursively, but default it to an empty table on the first call. I've also changed .map to .forEach since the return value is not used:
const response = {
"data": {
"Cstm_PF_ADG_URT_Disposition": {
"child_welfare_placement_value": ""
},
"Cstm_PF_ADG_URT_Demographics": {
"school_grade": "family setting",
"school_grade_code": ""
},
"Cstm_Precert_Medical_Current_Meds": [
{
"med_name": "med1",
"dosage": "10mg",
"frequency": "daily"
},
{
"med_name": "med2",
"dosage": "20mg",
"frequency": "daily"
}
],
"Cstm_PF_ADG_URT_Substance_Use": {
"dimension1_comment": "dimension 1 - tab1",
"Textbox1": "text - tab1"
},
"Cstm_PF_ADG_Discharge_Note": {
"prior_auth_no_comm": "auth no - tab2"
},
"Cstm_PF_ADG_URT_Clinical_Plan": {
"cca_cs_dhs_details": "details - tab2"
},
"container": {
"Cstm_PF_Name": {
"first_name": "same text for textbox - footer",
"last_name": "second textbox - footer"
},
"Cstm_PF_ADG_URT_Demographics": {
"new_field": "mapped demo - footer"
},
"grid2": [
{
"Cstm_PF_ADG_COMP_Diagnosis": {
"diagnosis_label": "knee",
"diagnosis_group_code": "leg"
}
},
{
"Cstm_PF_ADG_COMP_Diagnosis": {
"diagnosis_label": "ankle",
"diagnosis_group_code": "leg"
}
}
]
},
"submit": true
}
};
function getNamesAndValues(data, id, tablesAndValues = []) {
const res = data;
Object.entries(res).forEach(([key, value]) => {
const newKey = key.split('_')[0].toLowerCase();
if (newKey === id) {
tablesAndValues.push({
table: key,
values: value
});
} else {
getNamesAndValues( value, id, tablesAndValues); }
});
return tablesAndValues;
}
console.log(getNamesAndValues(response.data, 'cstm'));
You just need to call push to tablesAndValues inside the else statement with the rest operator and pass the value and id as parameters
const response = {
"data": {
"Cstm_PF_ADG_URT_Disposition": {
"child_welfare_placement_value": ""
},
"Cstm_PF_ADG_URT_Demographics": {
"school_grade": "family setting",
"school_grade_code": ""
},
"Cstm_Precert_Medical_Current_Meds": [
{
"med_name": "med1",
"dosage": "10mg",
"frequency": "daily"
},
{
"med_name": "med2",
"dosage": "20mg",
"frequency": "daily"
}
],
"Cstm_PF_ADG_URT_Substance_Use": {
"dimension1_comment": "dimension 1 - tab1",
"Textbox1": "text - tab1"
},
"Cstm_PF_ADG_Discharge_Note": {
"prior_auth_no_comm": "auth no - tab2"
},
"Cstm_PF_ADG_URT_Clinical_Plan": {
"cca_cs_dhs_details": "details - tab2"
},
"container": {
"Cstm_PF_Name": {
"first_name": "same text for textbox - footer",
"last_name": "second textbox - footer"
},
"Cstm_PF_ADG_URT_Demographics": {
"new_field": "mapped demo - footer"
},
"grid2": [
{
"Cstm_PF_ADG_COMP_Diagnosis": {
"diagnosis_label": "knee",
"diagnosis_group_code": "leg"
}
},
{
"Cstm_PF_ADG_COMP_Diagnosis": {
"diagnosis_label": "ankle",
"diagnosis_group_code": "leg"
}
}
]
},
"submit": true
}
};
function getNamesAndValues(data, id) {
const tablesAndValues = [],
res = data;
Object.entries(res).map(([key, value]) => {
const newKey = key.split('_')[0].toLowerCase();
// console.log(newKey) // -> 'cstm'
if (newKey === id) {
tablesAndValues.push({
table: key,
values: value
});
} else {
// I can log value and key and see what I want to push
// to the tablesAndValues array, but I can't seem to get
// how to push the nested items.
// console.log(value);
// console.log(key);
tablesAndValues.push(...getNamesAndValues(value, id))
}
});
return tablesAndValues;
}
console.log(getNamesAndValues(response.data, 'cstm'));
Or in shorter way
function getNamesAndValues2(data, id) {
return Object.entries(data).reduce((arr, [key, value]) => {
arr.push(
...(key.split('_')[0].toLowerCase() === id ? [{ table: key, values: value }] : getNamesAndValues(value, id))
);
return arr
}, []);
}
Here's a working version. I call the main function recursively if the value is either an array or an object. Also passing in the current state of the tallying array each time.
function getNamesAndValues(data, id, tablesAndValues = []) {
const res = data;
Object.entries(res).map(([key, value]) => {
const newKey = key.split('_')[0].toLowerCase();
const item = res[key];
if (newKey === id) {
tablesAndValues.push({
table: key,
values: value
});
}
if(Array.isArray(item)) {
return item.map(el => getNamesAndValues(el, id, tablesAndValues));
}
if(typeof item === 'object') {
return getNamesAndValues(item, id, tablesAndValues);
}
})
return tablesAndValues;
}
console.log(getNamesAndValues(response.data, 'cstm'));
Here's another approach using generators -
const keySearch = (t = [], q = "") =>
filter(t, ([ k, _ ]) => String(k).startsWith(q))
const r =
Array.from
( keySearch(response, "Cstm")
, ([ table, values ]) =>
({ table, values })
)
console.log(r)
[
{
table: 'Cstm_PF_ADG_URT_Disposition',
values: { child_welfare_placement_value: '' }
},
{
table: 'Cstm_PF_ADG_URT_Demographics',
values: { school_grade: 'family setting', school_grade_code: '' }
},
{
table: 'Cstm_Precert_Medical_Current_Meds',
values: [ [Object], [Object] ]
},
{
table: 'Cstm_PF_ADG_URT_Substance_Use',
values: {
dimension1_comment: 'dimension 1 - tab1',
Textbox1: 'text - tab1'
}
},
{
table: 'Cstm_PF_ADG_Discharge_Note',
values: { prior_auth_no_comm: 'auth no - tab2' }
},
{
table: 'Cstm_PF_ADG_URT_Clinical_Plan',
values: { cca_cs_dhs_details: 'details - tab2' }
},
{
table: 'Cstm_PF_Name',
values: {
first_name: 'same text for textbox - footer',
last_name: 'second textbox - footer'
}
},
{
table: 'Cstm_PF_ADG_URT_Demographics',
values: { new_field: 'mapped demo - footer' }
},
{
table: 'Cstm_PF_ADG_COMP_Diagnosis',
values: { diagnosis_label: 'knee', diagnosis_group_code: 'leg' }
},
{
table: 'Cstm_PF_ADG_COMP_Diagnosis',
values: { diagnosis_label: 'ankle', diagnosis_group_code: 'leg' }
}
]
Above, keySearch is simply a specialisation of filter -
function* filter (t = [], test = v => v)
{ for (const v of traverse(t)){
if (test(v))
yield v
}
}
Which is a specialisation of traverse -
function* traverse (t = {})
{ if (Object(t) === t)
for (const [ k, v ] of Object.entries(t))
( yield [ k, v ]
, yield* traverse(v)
)
}
Expand the snippet below to verify the result in your browser -
function* traverse (t = {})
{ if (Object(t) === t)
for (const [ k, v ] of Object.entries(t))
( yield [ k, v ]
, yield* traverse(v)
)
}
function* filter (t = [], test = v => v)
{ for (const v of traverse(t)){
if (test(v))
yield v
}
}
const keySearch = (t = [], q = "") =>
filter(t, ([ k, _ ]) => String(k).startsWith(q))
const response =
{"data":{"Cstm_PF_ADG_URT_Disposition":{"child_welfare_placement_value":""},"Cstm_PF_ADG_URT_Demographics":{"school_grade":"family setting","school_grade_code":""},"Cstm_Precert_Medical_Current_Meds":[{"med_name":"med1","dosage":"10mg","frequency":"daily"},{"med_name":"med2","dosage":"20mg","frequency":"daily"}],"Cstm_PF_ADG_URT_Substance_Use":{"dimension1_comment":"dimension 1 - tab1","Textbox1":"text - tab1"},"Cstm_PF_ADG_Discharge_Note":{"prior_auth_no_comm":"auth no - tab2"},"Cstm_PF_ADG_URT_Clinical_Plan":{"cca_cs_dhs_details":"details - tab2"},"container":{"Cstm_PF_Name":{"first_name":"same text for textbox - footer","last_name":"second textbox - footer"},"Cstm_PF_ADG_URT_Demographics":{"new_field":"mapped demo - footer"},"grid2":[{"Cstm_PF_ADG_COMP_Diagnosis":{"diagnosis_label":"knee","diagnosis_group_code":"leg"}},{"Cstm_PF_ADG_COMP_Diagnosis":{"diagnosis_label":"ankle","diagnosis_group_code":"leg"}}]},"submit":true}}
const result =
Array.from
( keySearch(response, "Cstm")
, ([ table, values ]) =>
({ table, values })
)
console.log(result)
A reasonably elegant recursive answer might look like this:
const getNamesAndValues = (obj) =>
Object (obj) === obj
? Object .entries (obj)
.flatMap (([k, v]) => [
... (k .toLowerCase () .startsWith ('cstm') ? [{table: k, value: v}] : []),
... getNamesAndValues (v)
])
: []
const response = {data: {Cstm_PF_ADG_URT_Disposition: {child_welfare_placement_value: ""}, Cstm_PF_ADG_URT_Demographics: {school_grade: "family setting", school_grade_code: ""}, Cstm_Precert_Medical_Current_Meds: [{med_name: "med1", dosage: "10mg", frequency: "daily"}, {med_name: "med2", dosage: "20mg", frequency: "daily"}], Cstm_PF_ADG_URT_Substance_Use: {dimension1_comment: "dimension 1 - tab1", Textbox1: "text - tab1"}, Cstm_PF_ADG_Discharge_Note: {prior_auth_no_comm: "auth no - tab2"}, Cstm_PF_ADG_URT_Clinical_Plan: {cca_cs_dhs_details: "details - tab2"}, container: {Cstm_PF_Name: {first_name: "same text for textbox - footer", last_name: "second textbox - footer"}, Cstm_PF_ADG_URT_Demographics: {new_field: "mapped demo - footer"}, grid2: [{Cstm_PF_ADG_COMP_Diagnosis: {diagnosis_label: "knee", diagnosis_group_code: "leg"}}, {Cstm_PF_ADG_COMP_Diagnosis: {diagnosis_label: "ankle", diagnosis_group_code: "leg"}}]}, submit: true}}
console .log (getNamesAndValues (response))
.as-console-wrapper {max-height: 100% !important; top: 0}
But this is not as simple as I would like. This code mixes the searching for matches and the test used in that searching together with the formatting of the output. It means that it is a custom function that is both more complex to understand and less reusable than I would like.
I would prefer to use some reusable functions, separating out three feature of this functionality. So, while the following involves more lines of code, I think it makes more sense:
const findAllDeep = (pred) => (obj) =>
Object (obj) === obj
? Object .entries (obj)
.flatMap (([k, v]) => [
... (pred (k, v) ? [[k, v]] : []),
... findAllDeep (pred) (v)
])
: []
const makeSimpleObject = (name1, name2) => ([k, v]) =>
({[name1]: k, [name2]: v})
const makeSimpleObjects = (name1, name2) => (xs) =>
xs .map (makeSimpleObject (name1, name2))
const cstmTest = k =>
k .toLowerCase () .startsWith ('cstm')
const getNamesAndValues = (obj) =>
makeSimpleObjects ('table', 'values') (findAllDeep (cstmTest) (obj))
const response = {data: {Cstm_PF_ADG_URT_Disposition: {child_welfare_placement_value: ""}, Cstm_PF_ADG_URT_Demographics: {school_grade: "family setting", school_grade_code: ""}, Cstm_Precert_Medical_Current_Meds: [{med_name: "med1", dosage: "10mg", frequency: "daily"}, {med_name: "med2", dosage: "20mg", frequency: "daily"}], Cstm_PF_ADG_URT_Substance_Use: {dimension1_comment: "dimension 1 - tab1", Textbox1: "text - tab1"}, Cstm_PF_ADG_Discharge_Note: {prior_auth_no_comm: "auth no - tab2"}, Cstm_PF_ADG_URT_Clinical_Plan: {cca_cs_dhs_details: "details - tab2"}, container: {Cstm_PF_Name: {first_name: "same text for textbox - footer", last_name: "second textbox - footer"}, Cstm_PF_ADG_URT_Demographics: {new_field: "mapped demo - footer"}, grid2: [{Cstm_PF_ADG_COMP_Diagnosis: {diagnosis_label: "knee", diagnosis_group_code: "leg"}}, {Cstm_PF_ADG_COMP_Diagnosis: {diagnosis_label: "ankle", diagnosis_group_code: "leg"}}]}, submit: true}}
console .log (findAllDeep (cstmTest) (response))
.as-console-wrapper {max-height: 100% !important; top: 0}
These are all helper functions of varying degree of reusability:
makeSimpleObject takes two key names, say 'foo', and 'bar' and returns a function which takes a two-element array, say [10, 20] and returns an object matching those up, like {foo: 10, bar: 20}
makeSimpleObjects does the same thing for an array of two-element arrays: makeSimpleObjects('foo', 'bar')([[8, 6], [7, 5], [30, 9]]) //=> [{foo: 8, bar: 6}, {foo: 7, bar: 5}, {foo: 30, bar: 9}].
cstmTest is a simple predicate to test whether a key begins (case-insensitively) with "cstm".
and findAllDeep takes a predicate and returns a function which takes an object and returns an array of two-element arrays, holding the keys/value pairs for any items which match the predicate. (The predicate is supplied both the key and the value; in the current case we only need the key, but it seems sensible for the function to take either.
Our main function, getNamesAndValues, uses findAllDeep (cstmTest) to find the matching values and then makeSimpleObjects ('table', 'values') to convert the result to the final format.
Note that findAllDeep, makeSimpleObject, and makeSimpleObjects are all functions likely to be useful elsewhere. The customization here is only in cstmTest and in the short definition for getNamesAndValues. I would count that as a win.
I have a large array of objects that I need to recursively loop through and find the object I need to update by its uid and update the data property within that object. This array is of unknown size and shape. The following method works, but I was wondering if there was a "cleaner" way of doing this process.
The following code loops through every object / array until it finds an object with property uid, if this uid equals to the block I need to update, I set the data property of this object. Is there a better way to do this?
Traversal Method
function traverse(x) {
if (isArray(x)) {
traverseArray(x)
} else if ((typeof x === 'object') && (x !== null)) {
traverseObject(x)
} else {
}
}
function traverseArray(arr) {
arr.forEach(function (x) {
traverse(x)
})
}
function traverseObject(obj) {
for (var key in obj) {
// check if property is UID
if (key == "uid") {
// check if this uid is equal to what we are looking for
if (obj[key] == "bcd") {
// confirm it has a data property
if (obj.hasOwnProperty('data')) {
// assign new data to the data property
obj['data'] = newData;
return;
}
}
}
if (obj.hasOwnProperty(key)) {
traverse(obj[key])
}
}
}
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]'
}
// usage:
traverse(this.sections)
Sample data
this.sections = [
{
uid: "abc",
layout: {
rows: [
{
columns: [
{
blocks: [
{
uid: "bcd",
data: {
content: "how are you?"
}
}
]
},
{
blocks: [
{
uid: "cde",
data: {
content: "how are you?"
}
}
]
},
],
},
{
columns: [
{
blocks: [
{
uid: "def",
data: {
content: "how are you?"
}
}
]
}
],
}
]
}
}
]
Is there a better way to handle this? Thanks!
I would recommend starting with a generic object mapping function. It takes an object input, o, and a transformation to apply, t -
const identity = x =>
x
const mapObject = (o = {}, t = identity) =>
Object.fromEntries(Object.entries(o).map(([ k, v ]) => [ k, t(v) ]))
Then build your recursive object mapping function using your shallow function. It takes an object to transform, o, and a transformation function, t -
const recMapObject = (o = {}, t = identity) =>
mapObject // <-- using mapObject
( o
, node =>
Array.isArray(node) // <-- recur arrays
? node.map(x => recMapObject(t(x), t))
: Object(node) === node // <-- recur objects
? recMapObject(t(node), t)
: t(node)
)
Now you build the object transformation unique to your program using our recursive object mapping function. It takes the complex nested data, graph, the uid to match, and a transformation to apply to the matched node, t -
const transformAtUid = (graph = {}, uid = "", t = identity) =>
recMapObject // <-- using recMapObject
( graph
, (node = {}) =>
node.uid === uid // if uid matches,
? { ...node, data: t(node.data) } // then transform node.data,
: node // otherwise no change
)
The above step is important because it detangles the specific logic about node.uid and node.data from the rest of the generic object transformation code.
Now we call our function on the input, data, to transform nodes matching node.uid equal to "bcd" using an example transformation -
const result =
transformAtUid
( data // <-- input
, "bcd" // <-- query
, node => ({ ...node, changed: "x" }) // <-- transformation
)
console.log(result)
Output -
{
"uid": "abc",
"layout": {
"rows": [
{
"columns": [
{
"blocks": [
{
"uid": "bcd",
"data": {
"content": "how are you?",
"changed": "x" // <-- transformed
}
}
]
}
// ...
}
Expand the snippet below to verify the results in your own browser -
const identity = x =>
x
const mapObject = (o = {}, t = identity) =>
Object.fromEntries(Object.entries(o).map(([ k, v ]) => [ k, t(v) ]))
const recMapObject = (o = {}, t = identity) =>
mapObject // <-- using mapObject
( o
, node =>
Array.isArray(node) // <-- recur arrays
? node.map(x => recMapObject(t(x), t))
: Object(node) === node // <-- recur objects
? recMapObject(t(node), t)
: t(node) // <-- transform
)
const transformAtUid = (graph = {}, uid = "", t = identity) =>
recMapObject
( graph
, (node = {}) =>
node.uid === uid
? { ...node, data: t(node.data) }
: node
)
const data =
{uid:"abc",layout:{rows:[{columns:[{blocks:[{uid:"bcd",data:{content:"how are you?"}}]},{blocks:[{uid:"cde",data:{content:"how are you?"}}]},],},{columns:[{blocks:[{uid:"def",data:{content:"how are you?"}}]}],}]}}
const result =
transformAtUid
( data
, "bcd"
, node => ({ ...node, changed: "x" })
)
console.log(JSON.stringify(result, null, 2))
In your original question, I see you have an array of objects to change -
// your original
this.sections = [ {...}, {...}, ... ]
To use recMapObject and transformAtUid with an array of objects, we can use Array.prototype.map -
this.sections = this.sections.map(o => transformAtUid(o, "bcd", ...))
It can be simpler if use a queue:
var sections = [
{
uid: "abc",
layout: {
rows: [
{
columns: [
{
blocks: [
{
uid: "bcd",
data: {
content: "how are you?"
}
}
]
},
{
blocks: [
{
uid: "cde",
data: {
content: "how are you?"
}
}
]
}
]
},
{
columns: [
{
blocks: [
{
uid: "def",
data: {
content: "how are you?"
}
}
]
}
]
}
]
}
}
];
var queue = [];
function trav(ss) {
queue = queue.concat(ss);
while (queue.length > 0) {
tObj(queue.pop());
}
console.log("result", ss);
}
function tObj(obj) {
if (obj.uid === "bcd") {
obj.data = "new data";
} else {
Object.keys(obj).forEach(el => {
if (Array.isArray(obj[el])) {
queue = queue.concat(obj[el]);
} else if (typeof (obj[el]) === "object") {
tObj(obj[el]);
}
});
}
}
trav(sections);
I have following object.
var obj = [{
Address1: "dd",
Address2: "qww",
BankAccNo: "44",
BankBranchCode: "44",
BloodGrp: "A+"
},
{
Address1: "dd",
Address2: "qww",
BankAccNo: "44",
BankBranchCode: "44",
BloodGrp: "A+"
}];
How can I make all of the keys uppercase?
I want to be able to access values like this : - obj[0].ADDRESS1
obj = obj.map( function( item ){
for(var key in item){
var upper = key.toUpperCase();
// check if it already wasn't uppercase
if( upper !== key ){
item[ upper ] = item[key];
delete item[key];
}
}
return item;
});
http://jsfiddle.net/07xortqy/
Loop over all the properties in the object (with for in)
Use .toUpperCase() to get the uppercase version of the property name
Copy the value from the original property to the uppercase version
delete the original property
For anyone looking for a solution working with objects, arrays, and nested objects or arrays:
// rename function depending on your needs
const capitalizeKeys = (obj) => {
const isObject = o => Object.prototype.toString.apply(o) === '[object Object]'
const isArray = o => Object.prototype.toString.apply(o) === '[object Array]'
let transformedObj = isArray(obj) ? [] : {}
for (let key in obj) {
// replace the following with any transform function
const transformedKey = key.replace(/^\w/, (c, _) => c.toUpperCase())
if (isObject(obj[key]) || isArray(obj[key])) {
transformedObj[transformedKey] = capitalizeKeys(obj[key])
} else {
transformedObj[transformedKey] = obj[key]
}
}
return transformedObj
}
const t = {
test1: 'hello',
test2: {
aa: 0,
bb: '1',
cc: [ 3, '4', 'world']
},
test3: [{
aa: 5,
bb: '6'
}, {
cc: [ 'hello', 'world', 7 ]
}
]
}
console.log(JSON.stringify(capitalizeKeys(t)))
(this function is to be adapted since I only had to capitalize the first letter, and there is no need for the helper functions to be nested)
$.each(obj, function(i, parent) {
$.each(parent, function(key, record) {
parent[ key.toUpperCase() ] = record[key]; //rename key
delete parent[key]; //delete old key
});
});
let obj = [
{ Address1: "dd",Address2: 'qww',BankAccNo: 44,BankBranchCode: 44,BloodGrp: 'A+' },
{ Address1: "dd",Address2: 'qww',BankAccNo: 44,BankBranchCode: 44,BloodGrp: 'A+' }
];
const uppercaseKeys = (elem) => {
let newObject = {}
Object.keys(elem).reduce( (acc, key, allKeys) => {
acc[key.toUpperCase()] = elem[key]
delete elem[key]
return acc
}, elem)
return newObject
}
obj.forEach( o => uppercaseKeys )
console.log(obj)
You can now also use Object.fromEntries() in combination with Object.entries() - have a look at the Object transformations section.
const obj2 = obj1.map(item => Object.fromEntries(Object.entries(item).map(([key, val]) => [
key.toUpperCase(),
val
])));
I've detailed the steps below:
// Iterate through each item in array
const obj2 = obj1.map(item => {
// Object.entries() method returns array of object's own enumerable string-keyed property [key, value] pairs,
// in the same order as that provided by a for...in loop
const entries = Object.entries(item);
// Convert keys to uppercase
const uppercaseEntries = entries.map(([key, val]) => [
key.toUpperCase(),
val
]);
// Object.fromEntries() method transforms a list of key-value pairs into an object.
return Object.fromEntries(uppercaseEntries);
});`
https://jsfiddle.net/buj5y32x/3/
For wider support, you are better off using Object.keys() with Array.reduce().
const obj2 = obj1.map(item =>
Object.keys(item).reduce((accumulator, key) => {
// accumulator is the new object we are creating
accumulator[key.toUpperCase()] = item[key];
return accumulator;
}, {})
);
https://jsfiddle.net/qf81ezsy/
You could just loop through them and add new entries?
for (index in obj) {
for (key in obj[index]) {
obj[index][key.toUpperCase()] = obj[key];
}
}