I have an array containing stories, each story also has a "day", I'd like to be able to merge stories on the same "day" into a new array.
stories = [
{
id: 1,
day: '18-02-2017',
user: 1,
story_data: //JSON containing a single story + other data
},
{
id: 2,
day: '18-02-2017',
user: 1,
story_data: //JSON containing a single story + other data
},
{
id: 3,
day: '17-02-2017',
user: 1,
story_data: //JSON containing a single story + other data
}
]
Here is what I'd like my output array to look like:
feed = [
{
day: '18-02-2017',
stories: [
//multiple JSON story items
]
},
{
day: '17-02-2017',
stories: [
//multiple JSON story items
]
}
]
I'm using the Async library in NodeJS for the necessary FOR loops since I also need to work with the data asynchronously before it's added to this final array - I understand what needs to happen to make the new array it's just been going completely over my head how to put it into code.
Try running this code:
var jsonStories = {};
stories.forEach(function(story) {
if (!jsonStories[story['day']]) {
jsonStories[story['day']] = {
'day': story['day'],
'stories': []
};
}
jsonStories[story['day']]['stories'].push(story);
});
var storiesArr = [];
Object.keys(jsonStories).forEach(function(key) {
storiesArr.push(jsonStories[key]);
});
You will get an ordered array.
You can also get it as JSON array if you remove the four last rows.
function groupByDay(arr) {
var hash = arr.reduce(function(h, s) { // h is the hash object, s is the current story
h[s.day] = h[s.day] || {'day': s.day, 'stories': []}; // if the hash doesn't have an entry for this story's day, then add it
h[s.day].stories.push(s); // add this story to the stories array of the object that acummulates the result for this day
return h;
}, {});
return Object.keys(hash).map(function(key) { // unwrap the objects from the hash object and return them as an array (the result)
return hash[key];
});
}
Here is the MDN docs for Array.prototype.reduce, Array.prototype.map and Object.keys.
You could do it in a single loop with a closure over a hash table.
var stories = [{ id: 1, day: '18-02-2017', user: 1, story_data: '01' }, { id: 2, day: '18-02-2017', user: 1, story_data: '02' }, { id: 3, day: '17-02-2017', user: 1, story_data: '03' }],
result = stories.reduce(function (hash) {
return function (r, a) {
if (!hash[a.day]) {
hash[a.day] = { day: a.day, stories: [] };
r.push(hash[a.day]);
}
hash[a.day].stories.push(a);
return r;
};
}(Object.create(null)), []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
let day2story = {};
stories.forEach((story) => {
let curr = day2story[story.day] || [];
curr.push(story.story_data);
day2story[story.day] = curr;
});
let feed = [];
Object.keys(day2story).forEach((day) => feed.push({day: day, stories: day2story[day]}));
console.log(JSON.stringify(feed))
You could use this ES6 code:
const result = Array.from(
stories.reduce(
(acc, {day, story_data}) => acc.set(day, (acc.get(day) || []).concat(story_data)),
new Map
),
([day, story_data]) => ({day, story_data})
);
const stories = [
{
id: 1,
day: '18-02-2017',
user: 1,
story_data: "story1"
},
{
id: 2,
day: '18-02-2017',
user: 1,
story_data: "story2"
},
{
id: 3,
day: '17-02-2017',
user: 1,
story_data: "story3"
}
];
const result = Array.from(
stories.reduce(
(acc, {day, story_data}) => acc.set(day, (acc.get(day) || []).concat(story_data)),
new Map
),
([day, story_data]) => ({day, story_data})
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Explanation
The reduce method creates a Map, keyed by the dates, and with the corresponding story arrays as values. So if the key does not yet exist in the map, an empty array is taken (|| []), otherwise its current value, and then the new story is added to it.
The map returned by reduce would in itself be a nice structure to work with, but as you asked for an array of objects, Array.from is applied to that map. This results in an array of pairs (sub-arrays with key / value entries), but Array.from accepts a callback function with which this pair can be transformed into the object with 2 properties.
This solution uses arrow functions, destructuring and Map which are all ES6 features.
Related
I have two lists:
let originalDocs = [
{
id: "sara",
date: new Date("01/01/2022")
},
{
id: "vic",
date: new Date("10/26/2020")
}
];
let newDocs = [
{
id: "vic",
date: new Date("01/02/2022")
},
{
id: "raul",
date: new Date("05/05/2021")
}
];
I need to mix both lists, ordering by date (in descending order) and without repetitions.
In order to do that, I have decided to:
Delete the set of elements of the original array that are included in the new array (I mean, deleting the intersection) without modifying the original array.
Mix the resulted array ordering by date.
This is my current code:
function removeIntersection(originalDocs, newDocs) {
return originalDocs.filter((originalDoc) => {
const index = newDocs.findIndex(
(newDoc) => newDoc.id === originalDoc.id
);
return index === -1;
});
}
function mixInOrder(originalDocs, newDocs) {
return [...newDocs, ...originalDocs]
.sort((a, b) => b.date - a.date);
}
//
// MAIN
//
let originalDocs = [
{
id: "sara",
date: new Date("01/01/2022")
},
{
id: "vic",
date: new Date("10/26/2020")
}
];
let newDocs = [
{
id: "vic",
date: new Date("01/02/2022")
},
{
id: "raul",
date: new Date("05/05/2021")
}
];
const result = mixInOrder(
removeIntersection(originalDocs, newDocs),
newDocs
);
console.log(result);
How can I do the same in a more optimal way, I mean, without the need of merging (using the spread syntax) the list before sorting it, or deleting the intersection in O(1).
I mean, is it possible to just insert in order avoiding the copy?
You can easily achieve the result using Set and reduce as:
Create a new array by adding the originalDocs first and then newDocs afterwards.
Use reduce to loop over the newly created array and then filter the objects.
You can use Set to check for existence of a key in efficient way
After filtering, you can then sort it accordingly.
let originalDocs = [
{
id: 'sara',
date: new Date('01/01/2022'),
},
{
id: 'vic',
date: new Date('10/26/2020'),
},
];
let newDocs = [
{
id: 'vic',
date: new Date('01/02/2022'),
},
{
id: 'raul',
date: new Date('05/05/2021'),
},
];
const set = new Set();
const result = [...originalDocs, ...newDocs].reduce((acc, curr) => {
if (!set.has(curr.id)) {
acc.push(curr);
set.add(curr.id);
}
return acc;
}, [])
.sort((a, b) => b.date - a.date);
console.log(result);
Here's how to do this using a JS Map, where you use the id as identifier:
let originalDocs = [
{
id: "sara",
date: new Date("01/01/2022")
},
{
id: "vic",
date: new Date("10/26/2020")
}
];
let newDocs = [
{
id: "vic",
date: new Date("01/02/2022")
},
{
id: "raul",
date: new Date("05/05/2021")
}
];
const map = new Map(originalDocs.map(el => [el.id, el]));
newDocs.forEach(el => map.set(el.id, el));
const sorted = [...map.values()].sort((a, b) => a.date - b.date);
console.log(sorted)
This overrides any originalDocs entry with a newDocs entry if they have the same id.
I have an array of object something like this.
[
{
channelName: "WhatsApp"
count: 1
date: "2021-06-05"
},{
channelName: "RCS"
count: 1
date: "2021-06-09"
}
]
There are two types of channel names 1. WhatsApp and 2nd are RCS. I want to filter out count with specific channel names and store it in a separate array. But the problem here is I want both the array length should be the same. If there is data for WhatsApp then it will add the count otherwise it will add 0 in place of it.
For that, I did something like this but this does not work .
const filterData = (data: any) => {
const category: any = [];
const whatsAppCount: any = [];
const rcsCount: any = [];
data.filter((item: any, i: number) => {
if (item.channelName === "WhatsApp") {
whatsAppCount[i] = item.count;
} else if (item.channelName === "RCS") {
rcsCount[i] = item.count;
}
category.push(item.date);
});
setGraphData({
category: category,
whatsApp: whatsAppCount,
rcs: rcsCount,
});
console.log("handleRun", { category, whatsAppCount, rcsCount });
};
Here the console log gives something like this.
whatsAppCount: [1, 2, 13, 21, empty × 2, 8, 5, empty, 18, empty, 12, 4]
rcsCount: [empty × 4, 1, 12, empty × 2, 1, empty, 8]
Here in the place of empty, I want 0. I am not sure how to do that any help would be great.
When you create the arrays, but before populating them, there are two functions that can help with initialization:
// create an array with 10 slots preallocated but empty (not `undefined`)
let arr = new Array(10);
// set all allocated slots to a value (`0` in this case)
arr = arr.fill(0);
Since you know the lengths you want ahead of time, you can use that to pre-size the arrays on construction. Then use .fill to initialize the values to 0. Once, that's done, you can continue with your counting and updating the arrays.
Reference:
Array constructor
Array.prototype.fill()
I would suggest you use the map-function, mapping the unwanted values to undefined, letting the other values "pass through" (unmodified), eg.:
const filtered = data.map((each) => {
if (wantToKeep) {
return each;
} else {
return undefined;
}
});
Note, this is not the exact solution - but a general idea.
You can use forEach and push(0) for the empty records.
const data = [
{
channelName: "WhatsApp",
count: 1,
date: "2021-06-05",
},
{
channelName: "RCS",
count: 1,
date: "2021-06-01",
},
{
channelName: "RCS",
count: 1,
date: "2021-06-06",
},
{
channelName: "WhatsApp",
count: 5,
date: "2021-06-11",
},
{
channelName: "WhatsApp",
count: 7,
date: "2021-06-23",
},
{
channelName: "RCS",
count: 1,
date: "2021-06-09",
},
];
const category = [];
const whatsAppCount = [];
const rcsCount = [];
data.forEach(x => {
if (x.channelName === "WhatsApp") {
whatsAppCount.push(x.count);
rcsCount.push(0);
} else if (x.channelName === "RCS") {
whatsAppCount.push(0);
rcsCount.push(x.count);
}
category.push(x.date);
});
console.log({ whatsAppCount });
console.log({ rcsCount });
console.log({ category });
I have an api that return me data in following format:
[
{
"_id": 1567362600000,
"KIDate": "2019-09-02",
"KITools": [
{
"data": 1,
"tool": "A"
},
{
"data": 2,
"tool": "B"
}
]
},
{
"_id": 1567519839316,
"KIDate": "2019-09-01",
"KITools": [
{
"data": 2,
"tool": "A"
},
{
"data": 1,
"tool": "C"
}
]
},
{
"_id": 1567519839317,
"KIDate": "2019-08-31",
"KITools": [
{
"data": 0,
"tool": "C"
}
]
},
]
I want to transform this data to get the following arrays:
Result 1 - [“2019-09-02”,”2019-09-01”,”2019-08-31”]
Result 2 - [ {name: ‘A’, data:[1, 2, 0] }, { name: 'B', data: [2, 0, 0] }, { name: 'C', data: [0, 1, 0]}]
Currently I am able to achieve this by using loops and per-defining variables with the tool name like following and looping the api data to push into this variable.
var result2 = [{
name: 'A',
data: []
}, {
name: 'B',
data: []
}, {
name: 'C',
data: []
}];
But this is not the expected behavior, the tool names can change and I have to figure that out dynamically based on the data returned by the api.
What is the best way to achieve this without looping like crazy.
You could use reduce method to get the result with array of dates and object of values for each tool.
const data = [{"_id":1567362600000,"KIDate":"2019-09-02","KITools":[{"data":1,"tool":"A"},{"data":2,"tool":"B"}]},{"_id":1567519839316,"KIDate":"2019-09-01","KITools":[{"data":2,"tool":"A"},{"data":1,"tool":"C"}]},{"_id":1567519839317,"KIDate":"2019-08-31","KITools":[{"data":0,"tool":"C"}]}]
const result = data.reduce((r, {KIDate, KITools}, i) => {
r.dates.push(KIDate);
KITools.forEach(({data: dt, tool}) => {
if(!r.values[tool]) r.values[tool] = Array(data.length).fill(0);
r.values[tool][i] = dt
})
return r;
}, {dates: [], values: {}})
console.log(result)
You can use reduce and forEach with Set and Map
Initialize accumulator as object with dates and data key, dates is a Set and data is Map
For every element add the KIDate to dates key,
Loop over KITools, check if that particular too exists in data Map if it exists update it's value by adding current values to id, if not set it's value as per current values
let data = [{"_id": 1567362600000,"KIDate": "2019-09-02","KITools": [{"data": 1,"tool": "A"},{"data": 2,"tool": "B"}]},{"_id": 1567519839316,"KIDate": "2019-09-01","KITools": [{"data": 2,"tool": "A"},{"data": 1,"tool": "C"}]},{"_id": 1567519839317,"KIDate": "2019-08-31","KITools": [{"data": 0,"tool": "C"}]},]
let final = data.reduce((op,{KIDate,KITools})=>{
op.dates.add(KIDate)
KITools.forEach(({data,tool})=>{
if(op.data.has(data)){
op.data.get(data).data.push(tool)
} else{
op.data.set(data, {name: data, data:[tool]})
}
})
return op
},{dates:new Set(),data: new Map()})
console.log([...final.dates.values()])
console.log([...final.data.values()])
The result1 array can be obtained via a direct .map(). To build the result2 array will require additional work - one approach would be to do so via .reduce() as detailed below:
const data=[{"_id":1567362600000,"KIDate":"2019-09-02","KITools":[{"data":1,"tool":"A"},{"data":2,"tool":"B"}]},{"_id":1567519839316,"KIDate":"2019-09-01","KITools":[{"data":2,"tool":"A"},{"data":1,"tool":"C"}]},{"_id":1567519839317,"KIDate":"2019-08-31","KITools":[{"data":0,"tool":"C"}]}];
const result1 = data.map(item => item.KIDate);
const result2 = data.reduce((result, item) => {
item.KITools.forEach(kitool => {
/* For current item, search for matching tool on name/tool fields */
let foundTool = result.find(i => i.name === kitool.tool);
if (foundTool) {
/* Add data to data sub array if match found */
foundTool.data.push(kitool.data);
} else {
/* Add new tool if no match found and init name and data array */
result.push({
name: kitool.tool,
data: [kitool.data]
});
}
});
return result;
}, []).map((item, i, arr) => {
/* Second phase of processing here to pad the data arrays with 0 values
if needed */
for (let i = item.data.length; i < arr.length; i++) {
item.data.push(0);
}
return item;
});
console.log('result1:', result1);
console.log('result2:', result2);
I've been playing around with Lodash and not getting close to a solution that doesn't involve a lot of extra looping and overhead.
data: [
{
name: "FirstResult", values: [
{
value: { NameCount: 1, OtherCount: 1 },
date: 2019-05-15T07:00:00+0000
},
{
value: { NameCount: 1 },
date: 2019-05-16T07:00:00+0000
}
]
},
{
name: "SecondResult",
values: [
{
value: { NameCount: 1 },
date: 2019-05-15T07:00:00+0000
},
{
value: { BuyCount: 2, SellCount: 1 },
date: 2019-05-16T07:00:00+0000
}
]
}
]
I'd like to flatten this and have it combined and aggregated by using the date as the key returning some configuration like:
[
{ date: 2019-05-15T07:00:00+0000, values: { NameCount: 2, OtherCount: 1 } },
{ date: 2019-05-16T07:00:00+0000, values: { NameCount: 1, BuyCount: 2, SellCount: 1 } }
]
Or even just a flat object array is fine like:
[
{ date: 2019-05-15T07:00:00+0000, NameCount: 2, OtherCount: 1 },
{ date: 2019-05-16T07:00:00+0000, NameCount: 1, BuyCount: 2, SellCount: 1 }
]
Does anyone have any ideas on how to do this with either a Lodash or Vanilla solution?
You can use a lodash's chain to flatten, group by the date, and then map and merge each group to a single object:
const fn = data => _(data)
.flatMap('values') // flatten to array of objects
.groupBy(o => o.date.toISOString()) // group by the iso representation
.map(group => { // map the groups by merging, and converting to require format
const { date, value } = _.mergeWith({}, ...group, (objValue, srcValue) =>
_.isNumber(objValue) ? objValue + srcValue : undefined // combine numeric values
)
return {
date,
...value,
}
})
.value()
const data = [{"name":"FirstResult","values":[{"value":{"NameCount":1,"OtherCount":1},"date": new Date("2019-05-15T07:00:00.000Z")},{"value":{"NameCount":1},"date": new Date("2019-05-16T07:00:00.000Z")}]},{"name":"SecondResult","values":[{"value":{"NameCount":1},"date":new Date("2019-05-15T07:00:00.000Z")},{"value":{"BuyCount":2,"SellCount":1},"date": new Date("2019-05-16T07:00:00.000Z")}]}]
const result = fn(data)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
Or you can use _.flow() to generate the function (I'm using lodash/fp here):
const { flow, flatMap, groupBy, map, mergeAllWith, cond, isNumber, add } = _
const fn = flow(
flatMap('values'), // flatten to array of objects
groupBy(o => o.date.toISOString()), // group by the iso representation
map(mergeAllWith(cond([[isNumber, add]]))), // combine numeric values
map(({ date, value }) => ({ date, ...value })) // format the objects
)
const data = [{"name":"FirstResult","values":[{"value":{"NameCount":1,"OtherCount":1},"date": new Date("2019-05-15T07:00:00.000Z")},{"value":{"NameCount":1},"date": new Date("2019-05-16T07:00:00.000Z")}]},{"name":"SecondResult","values":[{"value":{"NameCount":1},"date":new Date("2019-05-15T07:00:00.000Z")},{"value":{"BuyCount":2,"SellCount":1},"date": new Date("2019-05-16T07:00:00.000Z")}]}]
const result = fn(data)
console.log(result)
<script src='https://cdn.jsdelivr.net/g/lodash#4(lodash.min.js+lodash.fp.min.js)'></script>
Here is pure ES6 solution based on Array.reduce and Array.forEach for the object keys:
const data = [{"name":"FirstResult","values":[{"value":{"NameCount":1,"OtherCount":1},"date": new Date("2019-05-15T07:00:00.000Z")},{"value":{"NameCount":1},"date": new Date("2019-05-16T07:00:00.000Z")}]},{"name":"SecondResult","values":[{"value":{"NameCount":1},"date":new Date("2019-05-15T07:00:00.000Z")},{"value":{"BuyCount":2,"SellCount":1},"date": new Date("2019-05-16T07:00:00.000Z")}]}]
let result = data.reduce((r, { values }) => {
values.forEach(({ value, date }) => {
let keys = Object.keys(value), d = date.toISOString()
r[d] = r[d] || Object.assign({}, ...keys.map(x => ({ date: d, [x]: 0 })))
keys.forEach(k => r[d][k] = (r[d][k] || 0) + value[k])
})
return r
}, {})
console.log(Object.values(result))
The main idea is to get the keys of the value object white iterating and compose an object with them while grouping by the date. Then last forEach just sums each result object value.
How can I get an array with all the unique values based on a property name?
In my case my object looks like this and I want an array with the unique documentID's.
const file = {
invoice: {
invoiceID: 1,
documentID: 5
},
reminders: [
{
reminderID: 1,
documentID: 1
},
{
reminderID: 2,
documentID: 1
}
]
}
The result should be an array [5, 1] //The unique documentID's are 5 and 1
It doesn't seem like possible to add a property name to the Object.values() function.
You can use Set to get unique documentID.
const file = {
invoice: {
invoiceID: 1,
documentID: 5
},
reminders: [
{
reminderID: 1,
documentID: 1
},
{
reminderID: 2,
documentID: 1
}
],
payments: {
documentID : 5
}
};
var keys = Object.keys(file).map(key=>file[key].map ? file[key].map(i=>i.documentID) : file[key].documentID)
var keysFlattened= [].concat.apply([], keys);
var unique = new Set(keysFlattened);
console.log(Array.from(unique));
I use something like this that does what you want I think
const keepUniqueBy = key => (array, item) => {
if (array.find(i => item[key] === i[key])) {
return array;
} else {
return [ ...array, item ];
}
};
Then you can simply: const unique = reminders.reduce(keepUniqueBy('documentID'))
NB: It's probably low performing, but for small arrays it doesn't matter.