Related
I want to convert an array with this shape into a nested object shape.
I've been thinking about it for hours, but I can't come up with a proper way. Any good way?
before
let files2 = [
"folder/empty_file.txt",
"folder/inner file.txt",
"user16.txt",
"reagjjjd.md",
"folder/folder2/hi.txt",
];
after
let files = [
{ folder: [{ folder2: ["hihi.txt"] }, "empty_file.txt", "inner file.txt"] },
"user16.txt",
"reagjjjd.md",
];
I would appreciate any help or guidance here. Thanks!
Here is a solution that can convert arbitrary folder depth into a tree according to your requirement. It basically builds the tree through text splitting and recursion, which works but is likely not ideal. I wish it were a little simpler, but hopefully it helps if no-one else offers a better solution.
// const path = require("path");
// const sep = path.sep;
const sep = "/";
const input = [
"folder/empty_file.txt",
"folder/inner file.txt",
"user16.txt",
"reagjjjd.md",
"folder/folder2/hi.txt",
];
const get_files = (list) => {
return list.filter((s) => s.length < 2).map((s) => s[0]);
};
const get_folder_names = (list) => {
return [...new Set(list.filter((s) => s.length > 1).map((s) => s[0]))];
};
const get_folder_contents = (list, folder) => {
return list
.filter((s) => s.length > 1)
.filter((s) => s[0] == folder)
.map((s) => s.slice(1));
};
const get_folders = (list) => {
return get_folder_names(list).map((folder) => {
return { [folder]: build_tree(get_folder_contents(list, folder)) };
});
};
function build_tree(list) {
const tree = [];
tree.push(...get_files(list));
tree.push(...get_folders(list));
return tree;
}
const list = input.map((s) => s.split(sep));
const output = build_tree(list);
console.log("Tree:", JSON.stringify(output));
newArr by your structure .
newArr1 by my structure suggest
let files = [
"folder/empty_file.txt",
"folder/inner file.txt",
"user16.txt",
"reagjjjd.md",
"folder/folder2/hi.txt",
]
let newArr = files.reduce((res, path) => {
let convertArr = path.split("/")
if(convertArr.length < 2) return [...res, path]
let parent = res
let treePath = convertArr.forEach( (ele, key) => {
let temParent = parent.find(el => typeof el === 'object' && el.hasOwnProperty(ele))
if (temParent) {
parent = temParent[ele]
} else {
let tmp = key === convertArr.length-1 ? ele : {[ele] : []}
parent.push(tmp)
parent = tmp[ele]
}
})
return res
}, [])
let files2 = [
"folder/empty_file.txt",
"folder/inner file.txt",
"user16.txt",
"reagjjjd.md",
"folder/folder2/hi.txt",
]
let newArr1 = files2.reduce((res, path) => {
let convertArr = path.split("/")
let parent = res
let treePath = convertArr.forEach( ele => {
let temParent = parent.find(el => el.path === ele)
if (!temParent) {
let tmp = {path: ele, children: []}
parent.push(tmp)
parent = tmp.children
} else {
parent = temParent.children
}
})
return res
}, [])
console.log(newArr)
console.log(newArr1)
I think the array structure should follow this format for ease of use :
[
{
path : 'folder',
children : [
{
path : 'empty_file.txt',
children : []
},
{
path : 'inner file.txt',
children : []
},
{
path : 'folder2',
children : [
{
path : 'hi.txt',
children : []
}
]
},
]
},
{
path : 'user16.txt',
children : []
},
{
path : 'reagjjjd.md',
children : []
}
]
In Node.js, I have 3 sets of data like
[
{
"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
"dailyData":159392.235451,
"dailyDataInUSC":255.284807
}
]
and
[
{
"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
"monthlyData":159392.235451,
"monthlyDataInUSC":255.284807
},
{
"userId":"23fs6fds3-34k4-17de-3123-d2ec81e8aaf3",
"monthlyData":349392.455451,
"monthlyDataInUSC":655.234807
}
]
and
[
{
"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
"threeMonthsData":159392.235451,
"threeMonthsDataInUSC":255.284807
},
{
"userId":"23fs6fds3-34k4-17de-3123-d2ec81e8aaf3",
"threeMonthsData":349392.455451,
"threeMonthsDataInUSC":655.234807
},
{
"userId":"34sdf34-67j4-54nd-6763-d2ec81e8aaf3",
"threeMonthsData":6789392.455451,
"threeMonthsDataInUSC":905.655807
}
]
How can I combine this to one object based on userId(filter) inside an array.
Eg, output should be like
[
{
"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
"dailyData":159392.235451,
"dailyDataInUSC":255.284807,
"monthlyData":159392.235451,
"monthlyDataInUSC":255.284807,
"threeMonthsData":159392.235451,
"threeMonthsDataInUSC":255.284807
}
]
Please help me to achieve this.
A combination of spread, reduce and findIndex can be used to solve the problem.
Combine the original arrays into a single array using the spread operator.
Use reduce to group the elements by key (in this case userId)
Something like this :
const dailyData = [{"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3","dailyData":159392.235451,"dailyDataInUSC":255.284807}];
const monthlyData = [{"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3","monthlyData":159392.235451,"monthlyDataInUSC":255.284807}, {"userId":"23fs6fds3-34k4-17de-3123-d2ec81e8aaf3","monthlyData":349392.455451,"monthlyDataInUSC":655.234807}]
const triMonthlyData = [{"userId":"54c7f3ef-64d4-40de-8100-d2ec81e8aaf3","threeMonthsData":159392.235451,"threeMonthsDataInUSC":255.284807}, {"userId":"23fs6fds3-34k4-17de-3123-d2ec81e8aaf3","threeMonthsData":349392.455451,"threeMonthsDataInUSC":655.234807}, {"userId":"34sdf34-67j4-54nd-6763-d2ec81e8aaf3","threeMonthsData":6789392.455451,"threeMonthsDataInUSC":905.655807}]
const combinedData = [...dailyData, ...monthlyData, ...triMonthlyData].reduce((mergedResult, curElement) => {
let matchingElementIdx = mergedResult.findIndex(ele => ele.userId === curElement.userId);
if (matchingElementIdx !== -1) {
mergedResult[matchingElementIdx] = {...mergedResult[matchingElementIdx], ...curElement};
} else {
mergedResult = [...mergedResult, curElement];
}
return mergedResult;
}, []);
console.log(combinedData);
const aa = () => {
let aa = [
{
userId: "54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
dailyData: 159392.235451,
dailyDataInUSC: 255.284807
}
];
let bb = [
{
userId: "54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
monthlyData: 159392.235451,
monthlyDataInUSC: 255.284807
},
{
userId: "23fs6fds3-34k4-17de-3123-d2ec81e8aaf3",
monthlyData: 349392.455451,
monthlyDataInUSC: 655.234807
}
];
let cc = [
{
userId: "54c7f3ef-64d4-40de-8100-d2ec81e8aaf3",
threeMonthsData: 159392.235451,
threeMonthsDataInUSC: 255.284807
},
{
userId: "23fs6fds3-34k4-17de-3123-d2ec81e8aaf3",
threeMonthsData: 349392.455451,
threeMonthsDataInUSC: 655.234807
},
{
userId: "34sdf34-67j4-54nd-6763-d2ec81e8aaf3",
threeMonthsData: 6789392.455451,
threeMonthsDataInUSC: 905.655807
}
];
let newArrObj = aa;
bb.forEach(item => {
let index = newArrObj.findIndex(item1 => item1.userId === item.userId);
if (index === -1) {
newArrObj = [...newArrObj, item];
} else {
newArrObj[index] = { ...newArrObj[index], ...item };
}
});
cc.forEach(item => {
let index = newArrObj.findIndex(item1 => item1.userId === item.userId);
if (index === -1) {
newArrObj = [...newArrObj, item];
} else {
newArrObj[index] = { ...newArrObj[index], ...item };
}
});
console.log(newArrObj);
};
How can I manipulate my source array using javascript in order to get the following:
[ {Metric: totalRevenue, 2019-12-28: 91722000000, 2019-09-30: 63936000000}, {Metric: costOfRevenue, 2019-12-28: 56773000000, 2019-09-30: 39771000000}, ...etc. ]
Where Each financial statement item is considered a new metric that has a new key value that combines fiscalDate value and the value for that Line Item.
Example JSON here:
iex = [
{
reportDate: "2019-12-31",
fiscalDate: "2019-12-28",
currency: "USD",
totalRevenue: 91722000000,
costOfRevenue: 56773000000,
grossProfit: 34949000000,
researchAndDevelopment: 4451000000,
sellingGeneralAndAdmin: 5197000000,
operatingExpense: 66421000000,
operatingIncome: 25301000000,
otherIncomeExpenseNet: 617000000,
ebit: 25301000000,
interestIncome: 785000000,
pretaxIncome: 25918000000,
incomeTax: 3682000000,
minorityInterest: 0,
netIncome: 22236000000,
netIncomeBasic: 22236000000,
},
{
"reportDate": "2019-09-30",
"fiscalDate": "2019-09-28",
"currency": "USD",
"totalRevenue": 63936000000,
"costOfRevenue": 39771000000,
"grossProfit": 24165000000,
"researchAndDevelopment": 4110000000,
"sellingGeneralAndAdmin": 4578000000,
"operatingExpense": 48459000000,
"operatingIncome": 15477000000,
"otherIncomeExpenseNet": 650000000,
"ebit": 15477000000,
"interestIncome": 810000000,
"pretaxIncome": 16127000000,
"incomeTax": 2441000000,
"minorityInterest": 0,
"netIncome": 13686000000,
"netIncomeBasic": 13686000000
}
];
Image of Data Source: IEX data array
Code I've tried below:
function generateChartData(iex) {
var chartData = [],
categories = {};
for (var i = 0; i < iex.length; i++) {
var totalRevenue = iex[i].totalRevenue;
var fiscalDate = iex[i].fiscalDate;
// add new data point
if (categories[fiscalDate] === undefined) {
categories[fiscalDate] = {
Metric: totalRevenue,
};
chartData.push(categories[fiscalDate]);
}
// add value to existing data point
categories[fiscalDate][fiscalDate] = totalRevenue;
}
return chartData;
}
I have sorted the method, now can be done one loop.
function generateChartData(iex) {
return iex.reduce(
(m, item) => {
const fiscalDate = item["fiscalDate"];
if (!m[0][fiscalDate]) m[0][fiscalDate] = 0;
if (!m[1][fiscalDate]) m[1][fiscalDate] = 0;
if (!m[2][fiscalDate]) m[2][fiscalDate] = 0;
m[0][fiscalDate] += item.totalRevenue;
m[1][fiscalDate] += item.costOfRevenue;
m[2][fiscalDate] += item.grossProfit;
return m;
},
[
{ Metric: "totalRevenue" },
{ Metric: "costOfRevenue" },
{ Metric: "grossProfit" },
]
);
}
const iex = [{"reportDate":"2019-12-31","fiscalDate":"2019-12-28","currency":"USD","totalRevenue":91722000000,"costOfRevenue":56773000000,"grossProfit":34949000000,"researchAndDevelopment":4451000000,"sellingGeneralAndAdmin":5197000000,"operatingExpense":66421000000,"operatingIncome":25301000000,"otherIncomeExpenseNet":617000000,"ebit":25301000000,"interestIncome":785000000,"pretaxIncome":25918000000,"incomeTax":3682000000,"minorityInterest":0,"netIncome":22236000000,"netIncomeBasic":22236000000},{"reportDate":"2019-09-30","fiscalDate":"2019-09-28","currency":"USD","totalRevenue":63936000000,"costOfRevenue":39771000000,"grossProfit":24165000000,"researchAndDevelopment":4110000000,"sellingGeneralAndAdmin":4578000000,"operatingExpense":48459000000,"operatingIncome":15477000000,"otherIncomeExpenseNet":650000000,"ebit":15477000000,"interestIncome":810000000,"pretaxIncome":16127000000,"incomeTax":2441000000,"minorityInterest":0,"netIncome":13686000000,"netIncomeBasic":13686000000}]
console.log(
generateChartData(iex)
);
Another way:
function generateChartData(keys, iex) {
const [k1, k2, k3] = keys;
const def = keys.map((k) => ({ Metric: k }));
return iex.reduce((m, item) => {
const fiscalDate = item.fiscalDate;
if (!m[0][fiscalDate]) m[0][fiscalDate] = 0;
if (!m[1][fiscalDate]) m[1][fiscalDate] = 0;
if (!m[2][fiscalDate]) m[2][fiscalDate] = 0;
m[0][fiscalDate] += item[k1];
m[1][fiscalDate] += item[k2];
m[2][fiscalDate] += item[k3];
return m;
}, def);
}
const iex = [{"reportDate":"2019-12-31","fiscalDate":"2019-12-28","currency":"USD","totalRevenue":91722000000,"costOfRevenue":56773000000,"grossProfit":34949000000,"researchAndDevelopment":4451000000,"sellingGeneralAndAdmin":5197000000,"operatingExpense":66421000000,"operatingIncome":25301000000,"otherIncomeExpenseNet":617000000,"ebit":25301000000,"interestIncome":785000000,"pretaxIncome":25918000000,"incomeTax":3682000000,"minorityInterest":0,"netIncome":22236000000,"netIncomeBasic":22236000000},{"reportDate":"2019-09-30","fiscalDate":"2019-09-28","currency":"USD","totalRevenue":63936000000,"costOfRevenue":39771000000,"grossProfit":24165000000,"researchAndDevelopment":4110000000,"sellingGeneralAndAdmin":4578000000,"operatingExpense":48459000000,"operatingIncome":15477000000,"otherIncomeExpenseNet":650000000,"ebit":15477000000,"interestIncome":810000000,"pretaxIncome":16127000000,"incomeTax":2441000000,"minorityInterest":0,"netIncome":13686000000,"netIncomeBasic":13686000000}]
console.log(
generateChartData(["totalRevenue", "costOfRevenue", "grossProfit"], iex)
);
I have an array like
[
"parent1|child1|subChild1",
"parent1|child1|subChild2",
"parent|child2|subChild1",
"parent1|child2|subChild2",
"parent2|child1|subChild1",
"parent2|child1|subChild2",
"parent2|child2|subChild1",
.
.
.
]
Wherein my first string before | is the parent and the second string before | is the child and the third string after the second | is the subchild
How can I convert this array into an object like
[
{
"id": "parent1",
"children":[
{
"id": "child1",
"children":[
{
"id": "subChild1"
}
]
}
]
}
]
Parent -> child -> subchild object
Based on Sebastian's answer I tried below using typescript
private genTree(row) {
let self = this;
if (!row) {
return;
}
const [parent, ...children] = row.split('|');
if (!children || children.length === 0) {
return [{
id: parent,
children: []
}];
}
return [{
id: parent,
children: self.genTree(children.join('|'))
}];
}
private mergeDeep(children) {
let self = this;
const res = children.reduce((result, curr) => {
const entry = curr;
const existing = result.find((e) => e.id === entry.id);
if (existing) {
existing.children = [].concat(existing.children, entry.children);
} else {
result.push(entry);
}
return result;
}, []);
for (let i = 0; i < res.length; i++) {
const entry = res[i];
if (entry.children && entry.children.length > 0) {
entry.children = self.mergeDeep(entry.children);
}
};
return res;
}
private constructTree(statKeyNames){
let self = this;
const res = this.mergeDeep(statKeyNames.map(self.genTree).map(([e]) => e));
console.log(res);
}
but this gives me:
Cannot read property 'genTree' of undefined" error
Update:
As per Sebastian's comment changed self.genTree to this.genTree.bind(this) and it worked without any issues
You could use a mapper object which maps each object to it's unique path (You could map the object with each id, but id is not unique here). Then reduce each partial item in the array. Set the root object as the initialValue. The accumulator will be the parent object for the current item. Return the current object in each iteration.
const input = [
"parent1|child1|subChild1",
"parent1|child1|subChild2",
"parent1|child2|subChild1",
"parent1|child2|subChild2",
"parent2|child1|subChild1",
"parent2|child1|subChild2",
"parent2|child2|subChild1"
],
mapper = {},
root = { children: [] }
for (const str of input) {
let splits = str.split('|'),
path = '';
splits.reduce((parent, id, i) => {
path += `${id}|`;
if (!mapper[path]) {
const o = { id };
mapper[path] = o; // set the new object with unique path
parent.children = parent.children || [];
parent.children.push(o)
}
return mapper[path];
}, root)
}
console.log(root.children)
You have to use recursion for that. Take a look here:
const arr = [
"parent1|child1|subChild1",
"parent1|child1|subChild2",
"parent|child2|subChild1",
"parent1|child2|subChild2",
"parent2|child1|subChild1",
"parent2|child1|subChild2",
"parent2|child2|subChild1"
];
function genTree(row) {
const [parent, ...children] = row.split('|');
if (!children || children.length === 0) {
return [{
id: parent,
children: []
}];
}
return [{
id: parent,
children: genTree(children.join('|'))
}];
};
function mergeDeep(children) {
const res = children.reduce((result, curr) => {
const entry = curr;
const existing = result.find((e) => e.id === entry.id);
if (existing) {
existing.children = [].concat(existing.children, entry.children);
} else {
result.push(entry);
}
return result;
}, []);
for (let i = 0; i < res.length; i++) {
const entry = res[i];
if (entry.children && entry.children.length > 0) {
entry.children = mergeDeep(entry.children);
}
};
return res;
}
const res = mergeDeep(arr.map(genTree).map(([e]) => e));
console.log(JSON.stringify(res, false, 2));
I used two helpers here: genTree(row) which recursively generates a simple tree from each row, and mergeDeep(children) which reduces the first-level trees in the result of arr.map(genTree).map(([e]) => e), and then iterates over the array and recursively does the same thing to all children of each entry.
I have a javascript structure like below (nested arrays of objects)
var categoryGroups = [
{
Id: 1, Categories: [
{ Id: 1 },
{ Id: 2 },
]
},
{
Id: 2, Categories: [
{ Id: 100 },
{ Id: 200 },
]
}
]
I want to find a child Category object matching an Id, assuming the Category Id's are all unique.
I've got this below, but was wondering if there is a more concise way of doing it:
var category, categoryGroup, found = false;
for (i = 0; i < categoryGroups.length ; i++) {
categoryGroup = categoryGroups[i];
for (j = 0; j < categoryGroup.Categories.length; j++) {
category = categoryGroup.Categories[j];
if (category.Id === id) {
found = true;
break;
}
}
if (found) break;
}
Using flatMap in ES2019
const category = categoryGroups.flatMap(cg => cg.Categories).find(c => c.Id === categoryId);
Caveat: This uses a couple of Array.prototype functions that were only added in ECMAScript 5 and thus will not work with older browsers unless you polyfill them.
You can loop over all first-level objects in your array, and then filter the categories based on your condition and collect all matches in an array. Your final result will be the first element in the array of matches (no match found if array is empty).
var matches = [];
var needle = 100; // what to look for
arr.forEach(function(e) {
matches = matches.concat(e.Categories.filter(function(c) {
return (c.Id === needle);
}));
});
console.log(matches[0] || "Not found");
JSFiddle: http://jsfiddle.net/b7ktf/1/
References:
Array.prototype.forEach
Array.prototype.concat
Array.prototype.filter
Using only Array.prototype.filter():
If you are sure that the id you are looking for exists, you can do:
var id = 200; // surely it exists
var category = arr.filter(g => g.Categories.filter(c => c.Id === id)[0])[0].Categories.filter(c => c.Id === id)[0];
If you are not sure that it exists:
var id = 201; // maybe it doesn't exist
var categoryGroup = arr.filter(e => e.Categories.filter(c => c.Id === id)[0])[0];
var category = categoryGroup ? categoryGroup.Categories.filter(c => c.Id === id)[0] : null;
jsfiddle
Using reduce and recursion :
function nestedSearch(value) {
return categoryGroups.reduce(function f(acc, val) {
return (val.Id === value) ? val :
(val.Categories && val.Categories.length) ? val.Categories.reduce(f, acc) : acc;
});
}
> try on JSFiddle
check the code in the fiddle
var categoryGroups = [
{
Id: 1, Categories: [
{ Id: 1 },
{ Id: 2 },
]
},
{
Id: 2, Categories: [
{ Id: 100 },
{ Id: 200 },
]
}
]
var id = 100;
var x = 'not found';
var category, categoryGroup, found = false;
for (i = 0; i < categoryGroups.length ; i++) {
categoryGroup = categoryGroups[i];
for (j = 0; j < categoryGroup.Categories.length; j++) {
category = categoryGroup.Categories[j];
if (category.Id == id) {
var x = category.Id;
found = true;
break;
}
}
if (found) break;
}
alert(x);
The above code checks if id = 100 is found in the array. If found will alert the value else alerts that its not found. value '100' has been hardcoded for the sake of demo
You could wrap it inside a function to get rid of the awkward break; syntax and you can load each element into a variable inside the for(;;) construct to shave off a few lines.
function subCategoryExists(groups, id)
{
for (var i = 0, group; group = groups[i]; ++i) {
for (var k = 0, category; category = group.Categories[k]; ++k) {
if (category.Id == id) {
return true;
}
}
}
return false;
}
var found = subCategoryExists(categoryGroups, 100);
Easy way using lodash library of NodeJS (assuming you are using NodeJS):
const _ = require('lodash');
let category ;
let categoryGroup = _.find(categoryGroups, (element)=>{
category = _.find(element.Categories, {Id : 100});
return category;
});
console.log(categoryGroup); // The category group which has the sub category you are looking for
console.log(category); // The exact category you are looking for
If you want to actually return the inner category (instead of just checking for it's presence) you can use reduce:
return categoryGroups.reduce((prev, curr) => {
//for each group: if we already found the category, we return that. otherwise we try to find it within this group
return prev || curr.Categories.find(category => category.Id === id);
}, undefined);
This short-circuits on the inner categories, and touches each categoryGroup once. It could be modified to short-cicuit on the categoryGroups as well.
Here's a JS Fiddle demonstration.
You could use underscore:
var cat = _(categoryGroups).
chain().
pluck('Categories').
flatten().
findWhere({Id: 2}).
value();
What I'm doing here is that I'm extracting all Categories values in a single array and then grepping for the correct categories.
EDIT: sorry, didn't get your question right the first time. As the comments suggest, you might not want to use underscore just for that, but that's how I would do it :)
We are using object-scan for our data processing now. It's very powerful once you wrap your head around it. For your questions this would look like this:
// const objectScan = require('object-scan');
const lookup = (id, data) => objectScan(['Categories.Id'], {
useArraySelector: false,
abort: true,
rtn: 'parent',
filterFn: ({ value }) => value === id
})(data);
const categoryGroups = [{ Id: 1, Categories: [{ Id: 1 }, { Id: 2 }] }, { Id: 2, Categories: [{ Id: 100 }, { Id: 200 }] }];
console.log(lookup(1, categoryGroups));
// => { Id: 1 }
console.log(lookup(100, categoryGroups));
// => { Id: 100 }
console.log(lookup(999, categoryGroups));
// => undefined
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan