Bucle recursivamente a través de objetos de objeto
Estoy tratando de escribir una función recursiva para pasar por un objeto y devolver elementos basados en una ID. Puedo hacer que la primera parte de esto funcione, pero me está costando mucho intentar obtener esta función de manera recursiva y me vendría bien un par de ojos nuevos. El código está a continuación. Cuando ejecuta el fragmento, obtiene una matriz de 6 elementos que para la primera iteración es lo que quiero, pero ¿cómo puedo llamar a mi función con los parámetros adecuados para obtener los elementos anidados? Mi objetivo final es tener todos los objetos que comienzan con 'Cstm', anidados también, para agregarlos a la matriz tablesAndValues. Estaba tratando de modelar mi código después de esto: obtener todos los valores clave de JavaScript de matriz anidada de varios niveles , pero esto trata con una matriz de objetos y no con un objeto de objetos. Cualquier sugerencia o consejo que pueda obtener es muy apreciado.
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'));
Respuestas
Para lograr el resultado con una sola pulsación, se puede pasar la tabla de resultados a la función cuando se llama de forma recursiva, pero por defecto a una tabla vacía en la primera llamada. También cambié .map
a .forEach
porque el valor de retorno no se usa:
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'));
Solo necesita llamar a push to tablesAndValues dentro de la declaración else con el operador rest y pasar el valor y la identificación como parámetros
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'));
O de manera más corta
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
}, []);
}
Aquí tienes una versión funcional. Llamo a la función principal de forma recursiva si el valor es una matriz o un objeto. También pasando en el estado actual de la matriz de conteo cada vez.
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'));
Aquí hay otro enfoque que usa generadores:
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' }
}
]
Arriba, keySearch
es simplemente una especialización de filter
:
function* filter (t = [], test = v => v)
{ for (const v of traverse(t)){
if (test(v))
yield v
}
}
Que es una especialización de traverse
-
function* traverse (t = {})
{ if (Object(t) === t)
for (const [ k, v ] of Object.entries(t))
( yield [ k, v ]
, yield* traverse(v)
)
}
Expanda el fragmento a continuación para verificar el resultado en su navegador:
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)
Una respuesta recursiva razonablemente elegante podría verse así:
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}
Pero esto no es tan simple como me gustaría. Este código mezcla la búsqueda de coincidencias y la prueba utilizada en esa búsqueda junto con el formato de la salida. Significa que es una función personalizada que es más compleja de entender y menos reutilizable de lo que me gustaría.
Preferiría usar algunas funciones reutilizables, separando tres características de esta funcionalidad. Entonces, si bien lo siguiente implica más líneas de código, creo que tiene más sentido:
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}
Todas estas son funciones auxiliares de diferente grado de reutilización:
makeSimpleObject
toma dos nombres de clave, digamos'foo'
, y'bar'
y devuelve una función que toma una matriz de dos elementos, digamos[10, 20]
y devuelve un objeto que coincide con esos, como{foo: 10, bar: 20}
makeSimpleObjects
hace lo mismo para una serie de matrices de dos elementos:makeSimpleObjects('foo', 'bar')([[8, 6], [7, 5], [30, 9]]) //=> [{foo: 8, bar: 6}, {foo: 7, bar: 5}, {foo: 30, bar: 9}]
.cstmTest
es un predicado simple para probar si una clave comienza (sin distinción entre mayúsculas y minúsculas) con"cstm"
.y
findAllDeep
toma un predicado y devuelve una función que toma un objeto y devuelve una matriz de matrices de dos elementos, que contiene los pares claves / valor para cualquier elemento que coincida con el predicado. (El predicado se proporciona tanto la clave como el valor; en el caso actual solo necesitamos la clave, pero parece sensato que la función tome cualquiera.
Nuestra función principal getNamesAndValues
, utiliza findAllDeep (cstmTest)
para encontrar los valores coincidentes y luego makeSimpleObjects ('table', 'values')
convertir el resultado al formato final.
Tenga en cuenta que findAllDeep
, makeSimpleObject
y makeSimpleObjects
es probable que todas las funciones sean útiles en otros lugares. La personalización aquí es solo en cstmTest
y en la definición corta de getNamesAndValues
. Contaría eso como una victoria.