Javascript: Flatten any array values within object (nested) - javascript

I would like to flatten any array values within an object e.g. like sample below. The solution should not just apply to ecommerce but literally anything that could be in the object as an array type. Example:
var sample = {
price: "999",
description: "...",
ecommerce: {
products: [
{
brand: "apple",
category: "phone"
},
{
brand: "google",
category: "services"
}
]
}
};
And I would like the output to be:
{
price: "999",
description: "..."
ecommerce: {
products_1: {
brand: "apple",
category: "phone"
},
products_2: {
brand: "google",
category: "services"
}
}
}
What is the most efficient way to do this in JavaScript (ES6/7)?
Thanks in advance!
Updated due to comment and several minuses!!! Would be nice if those who were so quick to click minuses when the question was initially asked would revoke it!
I've tried this its completely wrong and I'm also sure theres a better functional way of doing this:
function flattenArray(array) {
var obj = array.reduce((acc, cur, i) => {
acc[i] = cur;
return acc;
}, {});
return obj;
}
function cleanObject(object) {
for (let key in object) {
let testObject = object[key];
if (Array.isArray(testObject)) {
testObject = flattenArray(testObject)
} else if (typeof(testObject) === 'object') {
testObject = cleanObject(testObject);
}
return testObject;
}
return object;
}
var clean = cleanObject(sample);
UPDATE 2: seeing as both solutions were so fixated on ecommerce how would the solution work if the next object was:
var sample = {
price: "999",
description: "...",
differentArray: [
{
brand: "apple",
category: "phone"
},
{
brand: "google",
category: "services"
}
]
};
Notice that not only is this different key its also at a different nesting level too.

A recursively applied Array#reduce based approach does the job for this kind of key-value bags, ... a first generic solution then might look like this one ...
function recursivelyMapArrayItemsToGenericKeys(collector, key) {
var
source = collector.source,
target = collector.target,
type = source[key];
if (Array.isArray(type)) {
type.forEach(function (item, idx) {
var
keyList = Object.keys(item || ''),
genericKey = [key, idx].join('_');
if (keyList.length >= 1) {
target[genericKey] = keyList.reduce(recursivelyMapArrayItemsToGenericKeys, {
source: item,
target: {}
}).target;
} else {
target[genericKey] = item;
}
});
} else if (typeof type !== 'string') {
var keyList = Object.keys(type || '');
if (keyList.length >= 1) {
target[key] = keyList.reduce(recursivelyMapArrayItemsToGenericKeys, {
source: type,
target: {}
}).target;
} else {
target[key] = type;
}
} else {
target[key] = type;
}
return collector;
}
var sample = {
price: "999",
description: "...",
ecommerce: {
products: [{
brand: "apple",
category: "phone"
}, {
brand: "google",
category: "services"
}, {
foo: [{
brand: "bar",
category: "biz"
}, {
brand: "baz",
category: "biz"
}]
}]
}
};
var result = Object.keys(sample).reduce(recursivelyMapArrayItemsToGenericKeys, {
source: sample,
target: {}
}).target;
console.log('sample : ', sample);
console.log('result : ', result);
.as-console-wrapper { max-height: 100%!important; top: 0; }
... within a next code sanitizing step one might get rid of duplicated logic, thus ending up with two functions and an alternating/reciprocal recursion ...
function recursivelyAssignItemsFromTypeByKeys(target, type, keyList, key) {
if (keyList.length >= 1) {
target[key] = keyList.reduce(recursivelyMapArrayItemsToGenericKeys, {
source: type,
target: {}
}).target;
} else {
target[key] = type;
}
}
function recursivelyMapArrayItemsToGenericKeys(collector, key) {
var
source = collector.source,
target = collector.target,
type = source[key];
if (Array.isArray(type)) {
type.forEach(function (item, idx) {
var
keyList = Object.keys(item || ''),
genericKey = [key, idx].join('_');
recursivelyAssignItemsFromTypeByKeys(target, item, keyList, genericKey);
});
} else if (typeof type !== 'string') {
var keyList = Object.keys(type || '');
recursivelyAssignItemsFromTypeByKeys(target, type, keyList, key);
} else {
target[key] = type;
}
return collector;
}
var sample = {
price: "999",
description: "...",
ecommerce: {
products: [{
brand: "apple",
category: "phone"
}, {
brand: "google",
category: "services"
}, {
foo: [{
brand: "bar",
category: "biz"
}, {
brand: "baz",
category: "biz"
}]
}]
}
};
var result = Object.keys(sample).reduce(recursivelyMapArrayItemsToGenericKeys, {
source: sample,
target: {}
}).target;
console.log('sample : ', sample);
console.log('result : ', result);
.as-console-wrapper { max-height: 100%!important; top: 0; }

What I'd do :
const sample = {
price: "999",
description: "...",
ecommerce: {
products: [
{
brand: "apple",
category: "phone"
},
{
brand: "google",
category: "services"
}
]
}
}
sample.ecommerce.products.forEach((p, i) => {
sample.ecommerce['products_' + i] = p
})
delete sample.ecommerce.products
console.log(sample)

Just reduce ecommerce array and destruct each element as you wish.
const sample = {
price: "999",
description: "...",
ecommerce: {
products: [
{
brand: "apple",
category: "phone"
},
{
brand: "google",
category: "services"
}
]
}
}
const flattenArray = (arr, key) =>
arr.reduce((prev, curr, index) => {
return {
...prev,
[`${key}_${index + 1}`]: curr,
}
}, {})
const result = {
...sample,
ecommerce: flattenArray(sample.ecommerce.products, 'products'),
}
console.log(result)

Related

Creating a Tree out of PathStrings

I have a similar problem to this (Get a tree like structure out of path string). I tried to use the provided solution but can not get it to work in Angular.
The idea is to the separate incoming path strings (see below) and add them to an object and display them as a tree.
pathStrings: string[] = [
"PathA/PathA_0",
"PathA/PathA_1",
"PathA/PathA_2/a",
"PathA/PathA_2/b",
"PathA/PathA_2/c"
];
let tree: Node[] = [];
for (let i = 0; i < this.pathStrings.length; i++) {
tree = this.addToTree(tree, this.pathStrings[i].split("/"));
}
addToTree(root: Node[], names: string[]) {
let i: number = 0;
if (names.length > 0) {
for (i = 0; i < root.length; i++) {
if (root[i].name == names[0]) {
//already in tree
break;
}
}
if (i == root.length) {
let x: Node = { name: names[0] };
root.push(x);
}
root[i].children = this.addToTree(root[i].children, names.slice(1));
}
return root;
}
The result is supposed to look like this:
const TREE_DATA: Node[] = [
{
name: "PathA",
children: [
{ name: "PathA_0" },
{ name: "PathA_1" },
{
name: "PathA_2",
children: [{ name: "a" }, { name: "b" }, { name: "c" }]
}
]
},
{
name: "PathB",
children: [
{ name: "PathB_0" },
{ name: "PathB_1", children: [{ name: "a" }, { name: "b" }] },
{
name: "PathC_2"
}
]
},
{
name: "PathC",
children: [
{ name: "PathB_0" },
{ name: "PathB_1", children: [{ name: "a" }, { name: "b" }] },
{
name: "PathC_2"
}
]
}
];
Here is the Stackblitz Link (https://stackblitz.com/edit/angular-h3btn5?file=src/app/tree-flat-overview-example.ts) to my intents.. Im trying for days now without success.. Thank you so much!!
In plain Javascript, you could reduce the array by using a recursive function for thesearching and assign then new child to a given node.
const
getTree = (node, names) => {
const name = names.shift();
let child = (node.children ??= []).find(q => q.name === name);
if (!child) node.children.push(child = { name });
if (names.length) getTree(child, names);
return node;
},
pathStrings = ["PathA/PathA_0", "PathA/PathA_1", "PathA/PathA_2/a", "PathA/PathA_2/b", "PathA/PathA_2/c"],
tree = pathStrings
.reduce((target, path) => getTree(target, path.split('/')), { children: [] })
.children;
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Reduce json api document to camel case

I would like to reduce a json api standard document to a camel version. I was able to make a camelCase objects and single primitive types keys.
I would like the desired output to be every key to be camelCase. So in the example above first-name would turn into firstName, snake-case would turn into snakeCase. The problem I'm facing is how to approach an array of objects using the same recursive call I'm using right now.
import _ from "lodash";
const data = {
id: 1,
type: "user",
links: { self: "/movies/1" },
meta: { "is-saved": false },
"first-name": "Foo",
"last-name": "Bar",
locations: ["SF"],
actors: [
{ id: 1, type: "actor", name: "John", age: 80 },
{ id: 2, type: "actor", name: "Jenn", age: 40 }
],
awards: [
{
id: 4,
type: "Oscar",
links: ["asd"],
meta: ["bar"],
category: "Best director",
'snake_case': 'key should be snakeCase'
}
],
name: { id: 1, type: "name", title: "Stargate" }
};
const needsCamelCase = str => {
return str.indexOf("-") > -1 || str.indexOf("_") > -1;
};
const strToCamelCase = function(str) {
return str.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
if (p2) return p2.toUpperCase();
return p1.toLowerCase();
});
};
const toCamelCase = obj => {
Object.keys(obj).forEach(key => {
if (_.isPlainObject(obj[key])) {
return toCamelCase(obj[key]);
}
if (_.isArray(obj[key])) {
// console.log(obj[key]);
obj[key].forEach(element => {
console.log(element);
});
//obj[key].foreach(element_ => toCamelCase);
}
if (needsCamelCase(key)) {
obj[strToCamelCase(key)] = obj[key];
delete obj[key];
}
});
return obj;
};
// toCamelCase(data);
console.log(toCamelCase(data));
Here is a codesand box: https://codesandbox.io/s/javascript-l842u
the logic is quite simple: if it's an object, just call to toCamelCase, if it's an array, iterate over it to create a new array. If it's an array of object, transform it with toCamelCase, if it's an array of something else keep it as is.
The solution would probably look like this:
const _ = require('lodash');
const data = {
id: 1,
type: "user",
links: { self: "/movies/1" },
meta: { "is-saved": false },
"first-name": "Foo",
"last-name": "Bar",
locations: ["SF"],
actors: [
{ id: 1, type: "actor", name: "John", age: 80 },
{ id: 2, type: "actor", name: "Jenn", age: 40 }
],
awards: [
{
id: 4,
type: "Oscar",
links: ["asd"],
meta: ["bar"],
category: "Best director",
'snake_case': 'key should be snakeCase'
}
],
name: { id: 1, type: "name", title: "Stargate" }
};
const needsCamelCase = str => {
return str.indexOf("-") > -1 || str.indexOf("_") > -1;
};
const strToCamelCase = function(str) {
return str.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
if (p2) return p2.toUpperCase();
return p1.toLowerCase();
});
};
const toCamelCase = obj => {
Object.keys(obj).forEach(key => {
const camelCasedKey = needsCamelCase(key) ? strToCamelCase(key) : key;
const value = obj[key];
delete obj[key];
obj[camelCasedKey] = value;
if (_.isPlainObject(value)) {
obj[camelCasedKey] = toCamelCase(value);
}
if (_.isArray(value)) {
obj[camelCasedKey] = value.map(item => {
if (_.isPlainObject(item)) {
return toCamelCase(item);
} else {
return item;
}
});
}
});
return obj;
};
// toCamelCase(data);
console.log(toCamelCase(data));

Get all parent in a nested object using recursion

I have the following object
const object = {
id: "1",
name: "a",
children: [
{
id: "2",
name: "b",
children: [
{
id: "3",
name: "c"
}
]
},
{
id: "4",
name: "d"
}
]
};
I need a function that accept the object and the id of the last child and return the path, for example, the following call: getPath(object, '3'); should return [{id: 1}, {id: 2}, {id: 3}].
I created the function but I can access only to the first parent.
function getPath(model, id, parent) {
if (model == null) {
return;
}
if (model.id === id) {
console.log(model.id, parent.id)
}
if (model.children) {
model.children.forEach(child => getPath(child, id, model));
}
}
PS: The object has an unknown depth.
You could use a short circuit for iterating the children and hand over the path from the function with the target object.
function getPath(model, id) {
var path,
item = { id: model.id };
if (!model || typeof model !== 'object') return;
if (model.id === id) return [item];
(model.children || []).some(child => path = getPath(child, id));
return path && [item, ...path];
}
const object = { id: "1", name: "a", children: [{ id: "2", name: "b", children: [{ id: "3", name: "c" }] }, { id: "4", name: "d" }] };
console.log(getPath(object, '42')); // undefined
console.log(getPath(object, '3')); // [{ id: 1 }, { id: 2 }, { id: 3 }]
.as-console-wrapper { max-height: 100% !important; top: 0; }
This is pretty close. Consider passing the entire path array in your recursive function. The following is a slightly modified version of what you have that accomplishes this.
function getPath(model, id, path) {
if (!path) {
path = [];
}
if (model == null) {
return;
}
if (model.id === id) {
console.log(model.id, path)
}
if (model.children) {
model.children.forEach(child => getPath(child, id, [...path, model.id]));
}
}
const object = {
id: "1",
name: "a",
children: [
{
id: "2",
name: "b",
children: [
{
id: "3",
name: "c"
}
]
},
{
id: "4",
name: "d"
}
]
};
getPath(object, "3");
const object = {
id: "1",
name: "a",
children: [
{
id: "2",
name: "b",
children: [
{
id: "3",
name: "c"
},
{
id: "5",
name: "c"
}
]
},
{
id: "4",
name: "d"
}
]
};
const getPath = (obj, id, paths = []) => {
if (obj.id == id) return [{ id: obj.id }];
if (obj.children && obj.children.length) {
paths.push({ id: obj.id });
let found = false;
obj.children.forEach(child => {
const temPaths = getPath(child, id);
if (temPaths) {
paths = paths.concat(temPaths);
found = true;
}
});
!found && paths.pop();
return paths;
}
return null;
};
console.log(getPath(object, "5"));
console.log(getPath(object, "2"));
console.log(getPath(object, "3"));
console.log(getPath(object, "4"));
.as-console-row {color: blue!important}

Advanced filter object in js

I'm trying filter Object of Arrays with Object but i don't have idea what can I do it.
Sample:
{
245: [
{
id: "12",
name: "test",
status: "new"
},
{
id: "15",
name: "test2",
status: "old"
},
{
id: "12",
name: "test2",
status: "old"
}],
2815: [
{
id: "19",
name: "test",
status: "new"
},
{
id: "50",
name: "test2",
status: "old"
},
{
id: "120",
name: "test2",
status: "new"
}]
}
Need filter if status = "new" but struct must not change:
{
245: [{
id: "12",
name: "test",
status: "new"
}],
2815: [{
id: "19",
name: "test",
status: "new"
},
{
id: "120",
name: "test2",
status: "new"
}]
}
Loop over entries and create a new object with filtered values
const obj = {
245:[
{id:"12",name:"test",status:"new"},{id:"15",name:"test2",status:"old"},{id:"12",name:"test2",status:"old"}],
2815:[
{id:"19",name:"test",status:"new"},{id:"50",name:"test2",status:"old"},{id:"120",name:"test2",status:"new"}]
}
console.log(filter(obj, item => item.status === "new"))
function filter(obj, pred) {
return Object.fromEntries(Object.entries(obj).map(([name, value]) => [name, value.filter(pred)]))
}
You need to map over the object keys and then over the array elements to filter out the final result
var obj = {
245:[
{id:"12",name:"test",status:"new"},{id:"15",name:"test2",status:"old"},{id:"12",name:"test2",status:"old"}],
2815:[
{id:"19",name:"test",status:"new"},{id:"50",name:"test2",status:"old"},{id:"120",name:"test2",status:"new"}]
}
var res = Object.entries(obj).reduce((acc, [key, value]) => {
acc[key] = value.filter(item => item.status === "new");
return acc;
}, {})
console.log(res);
you can do it for this specific case like this:
const myObj = {
245:[
{id:"12",name:"test",status:"new"},
{id:"15",name:"test2",status:"old"},
{id:"12",name:"test2",status:"old"}
],
2815:[
{id:"19",name:"test",status:"new"},
{id:"50",name:"test2",status:"old"},
{id:"120",name:"test2",status:"new"}
]
};
const filteredObj = filterMyObj(myObj);
console.log(filteredObj);
function filterMyObj(myObj){
const myObjCopy = {...myObj};
for (const key in myObjCopy){
const myArrCopy = [...myObjCopy[key]];
myObjCopy[key] = myArrCopy.filter(item => item.status == "new");
}
return myObjCopy;
}
You can do it with filter :
for(let key in obj){
obj[key] = obj[key].filter(el => el.status == "new")
}

Find object by property path in nested array of objects

I have such object:
var obj = [
{
name: 'ob_1',
childFields: [],
},
{
name: 'ob_2',
childFields: [
{
name: 'ob_2_1',
childFields: [
{
name: 'ob_3_1',
childFields: [],
test: 124
},
],
},
],
},
]
function getObjectByNamePath(path, fieds) {
const pathArr = path.split('.');
const result = fieds.find(field => {
if (pathArr.length > 1) {
if (field.name === pathArr[0] && field.childFields.length) {
const newPath = pathArr.slice(1, pathArr.length).join('.');
return getObjectByNamePath(newPath, field.childFields);
}
return false;
} else {
if (field.name === pathArr[0]) {
return true;
} else {
return false;
}
}
});
return result;
}
I want to get object by name values path:
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1', obj))
I tried this, but it doesn't work correct and i feel that there is more elegant way to achieve what i want. Thanks.
You could iterate the childFields and find the name for this level, then take the next level name.
function getObjectByNamePath(path, array) {
return path
.split('.')
.reduce(
({ childFields = [] } = {}, name) => childFields.find(o => o.name === name),
{ childFields: array }
);
}
var obj = [{ name: 'ob_1', childFields: [], }, { name: 'ob_2', childFields: [ { name: 'ob_2_1', childFields: [ { name: 'ob_3_1', childFields: [], test: 124 }] }] }];
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1', obj));
console.log(getObjectByNamePath('ob_1', obj));
console.log(getObjectByNamePath('foo.bar', obj));
.as-console-wrapper { max-height: 100% !important; top: 0; }
A recursive solution.
var obj = [{ name: 'ob_1', childFields: [], }, { name: 'ob_2', childFields: [ { name: 'ob_2_1', childFields: [ { name: 'ob_3_1', childFields: [], test: 124 }] }] }]
function getObjectByNamePath(path, obj) {
const [currentPath, ...restPaths] = path.split('.');
const nextObj = (obj.find(e => e.name === currentPath))
if(!restPaths.length) return nextObj;
return getObjectByNamePath(restPaths.join('.'), nextObj.childFields || []);
}
// Test Cases
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1', obj))
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1.fakePath', obj))
console.log(getObjectByNamePath('ob_1', obj))
console.log(getObjectByNamePath('', obj))

Categories

Resources