Creating an array of custom objects from nested data structure - javascript

I'm working on a project that requires me to massage some API data (shown in the snippet below as 'apiData'). The data structure I ultimately need for the charting library I'm using (Recharts) is this:
[
{ date: '2018-04-24', TSLA: 283.37, AAPL: 250.01 },
{ date: '2018-04-25', AAPL: 320.34 }
]
I've put together the function below and it works well enough, but I'm having trouble getting all the data to show up, even if there's no match between dates. In the below example, you'll notice that the object for date "2018-04-23" in the apiData is excluded. Ideally, the final ds would look like this:
[
{ date: '2018-04-23', TSLA: 285.12 }
{ date: '2018-04-24', TSLA: 283.37, AAPL: 250.01 },
{ date: '2018-04-25', AAPL: 320.34 }
]
Also, there's probably a more performant way to do this, but I've been hacking away for a while and not seeing a better solution at the moment. E.g. the forEach isn't ideal as the data set grows, which it will when I need to plot long time periods of data.
So my questions are: 1) How can I make sure objects that match in date are combined while objects that don't are still included and 2) what's a more performant way to do this operation?
If anyone has any input or critique of my approach and how I can improve it, I'd greatly appreciate it.
Here's a link to a repl if it's more convenient then the code snippet below.
formatChartData = (data) => {
const chartData = data
.reduce((arr, stock) => {
const stockArr = stock.chart.map((item) => {
let chartObj = {};
chartObj.date = item.date;
chartObj[stock.quote.symbol] = item.close;
if (arr.length > 0) {
arr.forEach((arrItem) => {
if (arrItem.date === item.date) {
chartObj = { ...arrItem, ...chartObj };
}
});
}
return chartObj;
});
return stockArr;
}, []);
console.log(chartData)
}
const apiData = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
formatChartData(apiData)
EDIT: I ended up using charlietfl's solution with an inner forEach as I found this easier to read than using two reduce methods. The final function looks like this:
const chartData = data
.reduce((map, stock) => {
stock.chart.forEach((chart) => {
const chartObj = map.get(chart.date) || { date: chart.date };
chartObj[stock.quote.symbol] = chart.close;
map.set(chart.date, chartObj);
});
return map;
}, new Map());`

A cleaner way than having to loop through the accumulated new array each time to look for a date is to use one master object with dates as keys
Following I use reduce() to return a Map (could be an object literal also) using dates as keys and then convert the Map values iterable to array to get the final results
const dateMap = apiData.reduce((map,stock)=>{
return stock.chart.reduce((_, chartItem)=>{
// get stored object for this date, or create new object
const dayObj = map.get(chartItem.date) || {date: chartItem.date};
dayObj[stock.quote.symbol] = chartItem.close;
// store the object in map again using date as key
return map.set(chartItem.date, dayObj);
},map);
}, new Map)
const res = [...dateMap.values()];
console.log(res)
.as-console-wrapper {max-height: 100%!important;}
<script>
const apiData = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
</script>

Just correcting your code only, else reduce should be used instead of map, for charts also.
formatChartData = (data) => {
const chartData = data
.reduce((arr, stock) => {
const stockArr = stock.chart.map((item) => {
let chartObj = {};
chartObj.date = item.date;
chartObj[stock.quote.symbol] = item.close;
if (arr.length > 0) {
arr.forEach((arrItem, i) => {
if (arrItem.date === item.date) {
chartObj = { ...arrItem, ...chartObj };
delete(arr[i]);
}
});
}
return chartObj;
});
return [...arr, ...stockArr].filter(e=>!!e); //to remove undefined elements left by delete above.
}, []);
console.log(chartData)
}
const apiData = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
formatChartData(apiData)

The problem is that your reduce function is running for each item in the data array. When it runs on the first item in the data array it returns:
[
{ date: '2018-04-23', TSLA: 285.12 },
{ date: '2018-04-24', TSLA: 283.37 }
]
When it runs on the second item in the array it returns this:
[
{ date: '2018-04-24', TSLA: 283.37, AAPL: 250.01 },
{ date: '2018-04-25', AAPL: 320.34 }
]
This is because when the reduce runs on the last array item it is returning the result from that item. You are only merging items from the accumulator variable "arr" if their date is in the current array item as well. Since 2018-04-23 is in the first but not the second it is not being added. I have added two things to your code. First if the current date being looped on is in the accumulator variable "arr" I delete it from "arr" after it is merged in. The second change is after each .reduce loop there is still some dates left in "arr" that aren't in the current "stockArr". To deal with this I merge both "arr" and "stockArr" which will give you what you are looking for.
formatChartData = (data) => {
const chartData = data
.reduce((arr, stock) => {
const stockArr = stock.chart.map((item) => {
let chartObj = {};
chartObj.date = item.date;
chartObj[stock.quote.symbol] = item.close;
if (arr.length > 0) {
arr.forEach((arrItem, index) => {
if (arrItem.date === item.date) {
chartObj = { ...arrItem, ...chartObj };
arr.splice(index, 1);
}
});
}
return chartObj;
});
return [...arr, ...stockArr];
}, []);
console.log(chartData)
}
const data = [
{
chart: [
{
date: "2018-04-23",
open: 291.29,
close: 285.12,
},
{
date: "2018-04-24",
open: 291.29,
close: 283.37,
},
],
news: [],
quote: {
symbol: "TSLA"
},
},
{
chart: [
{
date: "2018-04-24",
open: 200.29,
close: 250.01,
},
{
date: "2018-04-25",
open: 290.20,
close: 320.34,
},
],
news: [],
quote: {
symbol: "AAPL"
},
}
]
formatChartData(data)

Related

Javascript - transforming an object of array list to new formated one?

I'm trying to transform an object contain array to another one with javascript. Below is an example of the object field and what the formatted one should look like.
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
I need The new Fields to looks like this
let newFields = {
name: 'GAME',
tags:[
{ name: 'playPES', value: "{{PES}}" },
{ name: 'playFIFA', value: "{{FIFA}}" }
]},
One contributor suggested me a method like this but i think something need to modify in it but couldn't figure it out.
export const transform = (fields) => ({
tags: Object .entries (fields) .map (([name, innerFields]) => ({
name,
tags: innerFields.map(({code, title: title: {en})=>({name: en, value: code}))
}))
});
// newFields= transform(Fields)
I'm new working with javascript so any help is greatly appreciated, Thanks.
const transform = (o) => {
return Object.entries(o).map((e)=>({
name: e[0],
tags: e[1].map((k)=>({name: (k.title)?k.title.en:undefined, value: k.code}))
}))[0]
}
console.log(transform({
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
}))
Using the entries method you posted:
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
// 1. Obtain keys and values from first object
Fields = Object.entries(oldFields);
// 2. Create new object
const newFields = {};
// 3. Create the name key value pair from new Fields array
newFields.name = Fields[0][0];
// 4. Create the tags key value pair by mapping the subarray in the new Fields array
newFields.tags = Fields[0][1].map(entry => ({ name: entry.title.en, value: entry.code }));
Object.entries(Fields) will return this:
[
"GAME",
[TagsArray]
]
And Object.entries(Fields).map will be mapping this values.
The first map, will receive only GAME, and not an array.
Change the code to something like this:
export const transform = (Fields) => {
const [name, tags] = Object.entries(Fields);
return {
name,
tags: tags.map(({ code, title }) => ({
name: title.en,
value: code
}))
}
}
Hope it help :)
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
let newFields = {
name: 'GAME',
tags:[
{ name: 'playPES', value: "{{PES}}" },
{ name: 'playFIFA', value: "{{FIFA}}" }
]
}
let answer = {
name: "Game",
tags: [
]
}
Fields.GAME.map(i => {
var JSON = {
"name": i.title.en,
"value": i.code
}
answer.tags.push(JSON);
});
console.log(answer);
I think that this is more readable, but not easier... If you want the result as object you need to use reduce, because when you do this
Object.keys(Fields)
Your object transform to array, but reduce can change array to object back.
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
const result = Object.keys(Fields).reduce((acc, rec) => {
return {
name: rec,
tags: Fields[rec].map(el => {
return {
name: el.title.en,
value: el.code
}
})
}
}, {})
console.log(result)
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
const transform = (fields) => ({
tags: Object .entries (fields) .map (([name, innerFields]) => ({
name,
tags: innerFields.map(({code, title: title,en})=>({name: title.en, value: code}))
}))
});
//check required output in console
console.log(transform(Fields));

Search implementation for Multilevel array

i have JSON like below.
const testData = [
{
menu: 'Test',
submenu: [
{
menu: 'Test1',
submenu: [
{
menu: 'Test1.1',
},
{
menu: 'Test1.2',
},
{
secondLevel: [
{
menu: 'Test1.3',
submenu: [
{
menu: 'Test1.4',
},
{
menu: 'Test1.5',
},
],
},
],
},
],
},
i have used reduce function to traverse to search the expected word like Test1.1 i am getting proper value, whereas while search the Test1.4 is not coming up properly as it has secondLevel as parent object.
The code i used is below which is suggested in stackoverflow.
function search(data, value) {
return data.reduce((r, e) => {
const object = { ...e }
const result = search(e.submenu || [], value)
if (result.length) object.submenu = result
if (e.menu == value || result.length) r.push(object)
return r;
}, [])
}
Please suggest best way to search the element in secondLevel object as well. Thanks in advance

Updated nested object by matching ID

I have an array with nested objects that I need to update from another array of objects, if they match.
Here is the data structure I want to update:
const invoices = {
BatchItemRequest: [
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "11110" },
},
Amount: 2499,
},
],
},
},
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10111" },
},
Amount: 2499,
},
],
},
},
],
};
Here is the array of objects I want to update it from:
const accounts = [
{ AccountCode: "10110", Id: "84" },
{ AccountCode: "11110", Id: "5" },
{ AccountCode: "10111", Id: "81" },
];
I want to update invoices, using accounts, by inserting Id if AccountCode matches, to get the following structure:
const invoices = {
BatchItemRequest: [
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110", Id: "84" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "11110", Id: "5" },
},
Amount: 2499,
},
],
},
},
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110", Id: "84" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10111", Id: "81" },
},
Amount: 2499,
},
],
},
},
],
};
I have tried various methods, such as the following:
const mapped = invoices.BatchItemRequest.map((item1) => {
return Object.assign(
item1,
accounts.find((item2) => {
return item2 && item1.Invoice.Line.ItemAccountRef.AccountCode === item2.AccountCode;
})
);
});
Problem with this approach (it doesn't work as I think I need to do another nested map), but it also creates a new array, only including the nested elements of invoices.
Does anyone know a good approach to this?
This isn't the cleanest of code but it gets the job done:
function matchInvoiceWithAccount(invoices, accounts) {
const mappedInvoices = invoices.BatchItemRequest.map((request) => {
// Shouldn't modify input parameter, could use Object.assign to create a copy and modify the copy instead for purity
request.Invoice.Line = request.Invoice.Line.map((line) => {
const accountCode = line.SalesItemLineDetail.ItemAccountRef.AccountCode;
// If accounts was a map of AccountCode to Id you would't need to search for it which would be more effective
const account = accounts.find((account) => account.AccountCode === accountCode);
if (account) {
line.SalesItemLineDetail.ItemAccountRef.Id = account.Id;
}
return line;
});
return request;
});
return {
BatchItemRequest: mappedInvoices,
};
}
What you could and probably should do to improve this is to not modify the input parameters of the function, but that requires that you in a better way copy the original, either using Object.assign or spread operator.
At first, it will be good to create Map from your accounts array. We will go one time for array with O(n) and then will read ids by code with O(1). And nested fors is O(m*n), that will be much more slower at big arrays.
const idsByAccountCodes = new Map();
accounts.forEach((data) => {
idsByAccountCodes.set(data.AccountCode, data.Id);
})
or shorter:
const idsByAccountCode = new Map(accounts.map((data) => [data.AccountCode, data.Id]))
then if you want to mutate original values you can go through all nesting levels and add values
for ( const {Invoice:{ Line: line }} of invoices.BatchItemRequest){
for ( const {SalesItemLineDetail: {ItemAccountRef: item}} of line){
item.Id = idsByAccountCodes.get(item.AccountCode) || 'some default value'
// also if you don't have ids for all codes you need to define logic for that case
}
}
If you don't need to mutate original big object "invoices" and all of nested objects, then you can create recursive clone of if with something like lodash.cloneDeep

How to extract paths to 'enabled' objects in nested object arrays

I'm a novice to recursion and I have a JSON structure with arrays of nested objects. Some of these objects have a boolean enabled: true. I'm trying to figure out how to extract the paths to all enabled objects and their children.
I tried both cleaning up the original object by removing unused paths but I got lost in accessing the parents. I also tried building a separate array of paths using dot-notation, as I can probably build a new nested object from that. My latest attempt at the dot-notation extract:
const sourceData = {
title: "Work",
tags: [
{
title: "Cleaning",
tags: [
{
title: "Floors"
},
{ title: "Windows", enabled: true },
{ title: "Ceilings", enabled: true }
]
},
{
title: "Maintenance",
tags: [
{
title: "Walls",
enabled: true,
tags: [
{
title: "Brickwall"
},
{
title: "Wooden wall"
}
]
},
{
title: "Roof"
}
]
},
{
title: "Gardening"
}
]
};
function getEnabledPaths(level, acc) {
for (const tag of level.tags) {
if (tag.enabled) {
return tag.title;
} else if (tag.hasOwnProperty("tags")) {
var path = this.getEnabledPaths(tag);
if (path) acc.push(tag.title + "." + path);
}
}
return acc;
}
console.log(getEnabledPaths(sourceData, []));
I only get:
[
"Cleaning.Windows",
"Maintenance.Walls"
]
I would ideally end up with something like this:
[
'Work.Cleaning.Windows',
'Work.Cleaning.Ceilings',
'Work.Maintenance.Walls.Brickwall',
'Work.Maintenance.Walls.Wooden Wall'
]
In a perfect world (but I tried for days and went back to getting the dot notation results):
{
title: "Work",
tags: [
{
title: "Cleaning",
tags: [
{
title: "Windows",
enabled: true
},
{
title: "Ceilings",
enabled: true
}
]
},
{
title: "Maintenance",
tags: [
{
title: "Walls",
enabled: true,
tags: [
{
title: "Brickwall"
},
{
title: "Wooden wall"
}
]
}
]
}
]
};
The key to the recursion function is to both a) deal with children and b) the item itself.
Here's my take, which seems to work:
const sourceData = {title:"Work",tags:[{title:"Cleaning",tags:[{title:"Floors"},{title:"Windows",enabled:true},{title:"Ceilings",enabled:true}]},{title:"Maintenance",tags:[{title:"Walls",enabled:true,tags:[{title:"Brickwall"},{title:"Woodenwall"}]},{title:"Roof"}]},{title:"Gardening"}]};
function itemFilter(item) {
// enabled? done with this item
if (item.enabled) return item;
// not enabled and no tags? set to null
if (!item.tags) return null;
// filter all children, remove null children
item.tags = item.tags.map(child => itemFilter(child)).filter(child => child);
return item;
}
console.log(itemFilter(sourceData));
.as-console-wrapper {
max-height: 100vh !important;
}
You could pass enabled parameter down to lower levels of recursion if true value is found on some of the upper levels and based on that add path to the results or not.
const data ={"title":"Work","tags":[{"title":"Cleaning","tags":[{"title":"Floors"},{"title":"Windows","enabled":true},{"title":"Ceilings","enabled":true}]},{"title":"Maintenance","tags":[{"title":"Walls","enabled":true,"tags":[{"title":"Brickwall"},{"title":"Wooden wall"}]},{"title":"Roof"}]},{"title":"Gardening"}]}
function paths(data, prev = '', enabled = false) {
const result = [];
prev += (prev ? "." : '') + data.title;
if (!enabled && data.enabled) enabled = true;
if (!data.tags) {
if (enabled) {
result.push(prev);
}
} else {
data.tags.forEach(el => result.push(...paths(el, prev, enabled)))
}
return result;
}
const result = paths(data)
console.log(result)

Array with object sorting with Underscore sortBy

I have this array. How do I use underscore '_.sortBy' to sort it according to start date?
[
{
id: 'oljw832021kjnb389xzll323jk',
start: { dateTime: '2013-09-26T13:30:00-07:00' },
end: { dateTime: '2013-09-26T14:30:00-07:00' },
},
{
id: 'ed7l5tmckdp0lm90nvr4is3d4c',
start: { dateTime: '2013-09-26T15:30:00-07:00' },
end: { dateTime: '2013-09-26T16:30:00-07:00' },
},
{
id: 'etmasdsackdp0kjl0nvrkopioqw',
start: { dateTime: '2013-09-26T18:00:00-07:00' },
end: { dateTime: '2013-09-26T19:00:00-07:00' },
}
]
Use an iterator function, not a single string for a property:
_.sortBy(arr, function(o) { return o.start.dateTime; })
I did it this way:
var sorted = _(list).sortBy(
function (item) {
return [new Date(item.effectiveDate).getTime(), item.batchId];
}), "batchId");
If you want it descending then it's the same thing but *-1
var sorted = _(list).sortBy(
function (item) {
return [new Date(item.effectiveDate).getTime()*-1, item.batchId];
}), "batchId");
In this example I am ordering by two fields, you can forget about the item.batchId.
Hope this helps someone.
var sortedItem = _.sortBy(yourArrayName, ["start"])

Categories

Resources