Improve nested forEach - javascript

I have this nested array:
const users = [
['User_1', [[1596232800000, 4]]],
[
'User_2',
[
[1591567200000, 3],
[1591653600000, 16],
],
],
]
and would like this output:
const dataByDate = [
{
user: 'User_2',
date: 1591567200000,
count: 3,
},
{
user: 'User_2',
date: 1591653600000,
count: 16,
},
{
user: 'User_1',
date: 1596232800000,
count: 4,
},
]
To achieve this I am doing this:
const dataByDate: { date: string; count: number; user: string }[] = []
users.forEach((user: string[]) => {
if (user[1].length) {
user[1].forEach((item: any) => {
dataByDate.push({ date: item[0], user: user[0], count: item[1] })
})
}
})
My solution works fine but I am wondering if there is a cleaner and more elegant solution than a nested forEach. Thanks!

You can use .flatMap with an inner .map(). The inner .map() will take the second element of your inner array (ie: the [[date, count], ...] arrays), and map them to objects. Since .map() will result in an array of objects, you can use .flatMap() to merge the resulting objects into one final resulting array.
See example below:
const users = [
['User_1', [[1596232800000, 4]]],
[
'User_2',
[
[1591567200000, 3],
[1591653600000, 16],
],
],
];
const result = users.flatMap(([user, arr]) => arr.map(([date, count]) => ({
user, date, count
})));
console.log(result);
You can then apply the .sort() method for any additional reordering of the output array.

Use a combination of Array.prototype.flatMap() and Array.prototype.reduce(). The reduce loops over each data set for a user and creates and object per entry in that set. That object is then combined in the accumulating array.
Now flatMap returns a new array with objects for each user. The amount of objects are dependant on the amount of [date, count] entries and will be in a nested array. The flat part of flatMap takes care of this by putting every object in a single array.
const users = [
[
'User_1', [
[1596232800000, 4]
]
],
[
'User_2', [
[1591567200000, 3],
[1591653600000, 16],
],
],
];
const formatUserData = users => users.flatMap(([ user, data ]) =>
data.reduce((arr, [ date, count ]) =>
[...arr, { user, date, count }], [])
);
const result = formatUserData(users);
console.log(result);

Using map and array destructuring, you can achieve this as:
const result = users
.map(([ user, items ]) => {
return items.flatMap(([date, count]) => ({ user, date, count }))
})
.flatMap(i => i)
.sort((a,b) => a.date - b.date);
const users = [
['User_1', [[1596232800000, 4]]],
[
'User_2',
[
[1591567200000, 3],
[1591653600000, 16],
],
],
]
const result = users.map(([ user, items ]) => {
return items.flatMap(([date, count]) => ({ user, date, count }))
}).flatMap(i => i).sort((a,b) => a.date - b.date);
console.log({result})

Related

Form into a single array after iterating an object

How to get values of views into single array and get the two largest values in that array. The below is not creating single array. Could someone please advise ?
const data = [
{
id: 1,
views: 5678,
textData: "Sun"
},
{
id: 2,
views: 2500,
textData: "Moon"
},
{
id: 3,
views: 3500,
textData: "Earth"
},
{
id: 4,
views: 1250,
textData: "Sky"
}
]
data.map(({id, views, textData}) =>{
let myArr = [];
myArr.push(views);
let result = Math.max(...myArr);
console.log(result);
})
Desired Array: [5678, 2500, 3500, 1250 ]
Final Output : [5678,3500 ]
You can use Array#map to create an array of the views properties, then sort it.
const data=[{id:1,views:5678,textData:"Sun"},{id:2,views:2500,textData:"Moon"},{id:3,views:3500,textData:"Earth"},{id:4,views:1250,textData:"Sky"}];
let res = data.map(x => x.views).sort((a,b) => b - a).slice(0, 2);
console.log(res);
Get result in one loop without sorting, but the code doesn't look very clean.
const data=[{id:1,views:5678,textData:"Sun"},{id:2,views:2500,textData:"Moon"},{id:3,views:3500,textData:"Earth"},{id:4,views:1250,textData:"Sky"}];
const values1 = []
const values2 = [0, 0]
data.forEach(d => {
values1.push(d.views)
values2[0] = Math.max(values2[0], Math.min(d.views, values2[1]))
values2[1] = Math.max(d.views, values2[1])
})
console.log('single values: ', values1)
console.log('two largest values: ', values2)

Create an array of objects based on an object if one or more properties have multiple values differentiated by a comma

i'm trying to duplicate objects based on two properties that have multiple values differentiated by a comma.
For example:
I have an object
const obj = {
id: 1
date: "2021"
tst1: "111, 222"
tst2: "AAA, BBB"
}
And I would like the result to be an array of 2 objects in this case (because there are 2 values in tst1 OR tst2, these 2 properties will always have the same nr of values differentiated by a comma)
[{
id: 1,
date: "2021",
tst1: "111",
tst2: "AAA",
},
{
id: 1,
date: "2021",
tst1: "222",
tst2: "BBB",
}]
What I tried is this:
I created a temporary object
const tempObject = {
id: obj.id,
date: obj.date,
}
And then I would split and map the property that has multiple values, like this:
cont newObj = obj.tst1.split(",").map(function(value) {
let finalObj = {}
return finalObj = {
id: tempObject.id,
date: tempObject.date,
tst1: value,
})
And now, the newObj is an array of objects and each object contains a value of tst1.
The problem is I still have to do the same for the tst2...
And I was wondering if there is a simpler method to do this...
Thank you!
Here is an example that accepts an array of duplicate keys to differentiate. It first maps them to arrays of entries by splitting on ',' and then trimming the entries, then zips them by index to create sub-arrays of each specified property, finally it returns a result of the original object spread against an Object.fromEntries of the zipped properties.
const mapDuplicateProps = (obj, props) => {
const splitProps = props.map((p) =>
obj[p].split(',').map((s) => [p, s.trim()])
);
// [ [[ 'tst1', '111' ], [ 'tst1', '222' ]], [[ 'tst2', 'AAA' ], [ 'tst2', 'BBB' ]] ]
const dupeEntries = splitProps[0].map((_, i) => splitProps.map((p) => p[i]));
// [ [[ 'tst1', '111' ], [ 'tst2', 'AAA' ]], [[ 'tst1', '222' ], [ 'tst2', 'BBB' ]] ]
return dupeEntries.map((d) => ({ ...obj, ...Object.fromEntries(d) }));
};
const obj = {
id: 1,
date: '2021',
tst1: '111, 222',
tst2: 'AAA, BBB',
};
console.log(mapDuplicateProps(obj, ['tst1', 'tst2']));
Not sure if that's what you're searching for, but I tried making a more general use of what you try to do:
const duplicateProperties = obj => {
const properties = Object.entries(obj);
let acc = [{}];
properties.forEach(([key, value]) => {
if (typeof value === 'string' && value.includes(',')) {
const values = value.split(',');
values.forEach((v, i) => {
if (!acc[i]) {
acc[i] = {};
}
acc[i][key] = v.trim();
});
} else {
acc.forEach(o => o[key] = value);
}
});
return acc;
};
const obj = {
id: 1,
date: '2021',
tst1: '111, 222',
tst2: 'AAA, BBB',
};
console.log(duplicateProperties(obj));
You could start by determining the length of the result using Math.max(), String.split() etc.
Then you'd create an Array using Array.from(), returning the correct object for each value of the output index.
const obj = {
id: 1,
date: "2021",
tst1: "111, 222",
tst2: "AAA, BBB",
}
// Determine the length of our output array...
const length = Math.max(...Object.values(obj).map(s => (s + '').split(',').length))
// Map the object using the relevant index...
const result = Array.from({ length }, (_, idx) => {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
const a = (value + '').split(/,\s*/);
return [key, a.length > 1 ? a[idx] : value ]
}))
})
console.log(result)
.as-console-wrapper { max-height: 100% !important; }

Efficient method to group multiple objects by object property

what is the most efficient way to group by multiple object properties? It groups by category and then by group.
I have detailed my implementation using a single array reduce which I will push into an array (expected output). I am not sure how to conditionally push to sources array which is created within the reduce.
Without using lodash groupBy or third party. I was also thinking of filtering by a key I create e.g.
const key = `${category}-${group}`;
const items = [
{
"name": "Halloumi",
"group": "Cheese",
"category": "Dairy"
},
{
"name": "Mozzarella",
"group": "Cheese",
"category": "Dairy"
}
];
// my current implementation
const groupedItems = items.reduce((map, item) => {
const { category, name, group } = item;
if (map.has(category)) {
map.get(category).push({
name,
group,
});
} else {
map.set(category, [
{
name,
group,
},
]);
}
return map;
}, new Map());
console.log(Object.fromEntries(groupedItems));
// Another attempt
const groupedItems2 = items.reduce((map, item) => {
const { category, group, name } = item;
acc[category] = acc[category] || { category, themes: [] };
const source = {
id,
name
};
const totalSources = [source].length;
const uniqueSources = [...new Set([source])].length;
acc[category].themes.push({
theme,
totalSources,
uniqueSources,
sources: [source],
});
return acc;
return map;
}, {});
console.log(Object.entries(groupedItems2));
// expected output
[
{
"category": "Dairy",
"groups": [
{
"group": "Cheese",
"totalSources": 2,
"sources": [
{
"name": "Halloumi"
},
{
"name": "Mozzarella"
}
]
}
]
}
]
You need a two-level map.
There are many ways to do that. Here is one:
const items = [{"name": "Halloumi","group": "Cheese","category": "Dairy"},{"name": "Mozzarella","group": "Cheese","category": "Dairy"}];
const dict = {};
for (const {name, group, category} of items) {
((dict[category] ??= {})[group] ??= []).push({name});
}
const categories = Object.entries(dict).map(([category, groups]) => ({
category,
groups: Object.entries(groups).map(([group, sources]) => ({
group,
totalSources: sources.length,
sources
}))
}));
console.log(categories);
I was interested in a more generic approach to this problem. It might or might not be useful for you, but I think it offers another way to think about such problems. The idea is that we have a function which takes a configuration like this:
const config = {
prop: 'category',
childName: 'groups',
children: {
prop: 'group',
childName: 'sources',
totalName: 'totalSources',
children: {prop: 'name'}
}
}
It returns a new function which takes an array of flat objects and nests them, giving totals, and changing property names as necessary. Here is an implementation, tested only on this problem (with substantially more data added to test various scenarios):
const regroup = ({prop, newName = prop, childName, totalName, children}) => (xs) =>
childName && children
? Object .entries (groupBy (x => x [prop]) (xs))
.map (([k, vs]) => [k, vs .map (omit ([prop]))])
.map (([k, vs, kids = regroup (children) (vs)]) => ({
[newName]: k,
... (totalName ? {[totalName]: kids .length} : {}),
[childName]: kids
})
)
: prop && newName
? xs .map (({[prop]: n, ...rest}) => ({[newName]: n, ...rest}))
: [ ...xs]
const groupBy = (fn, k) => (xs) => xs .reduce (
(a, x) => ((k = fn (x)), (a [k] = a [k] || []), (a [k] .push (x)), a)
, {}
)
const omit = (names) => (o) => Object .fromEntries (
Object .entries (o) .filter (([k, v]) => ! names.includes (k))
)
const config = {
prop: 'category',
childName: 'groups',
children: {
prop: 'group',
childName: 'sources',
totalName: 'totalSources',
children: {prop: 'name'}
}
}
const groupFoods = regroup (config)
const items = [{name: "Halloumi", group: "Cheese", category: "Dairy"}, {name: "Mozzarella", group: "Cheese", category: "Dairy"}, {name: "Whole", group: "Milk", category: "Dairy"}, {name: "Skim", group: "Milk", category: "Dairy"}, {name: "Potatos", group: "Root", category: "Vegetable"}, {name: "Turnips", group: "Root", category: "Vegetable"}, {name: "Strawberry", group: "Berry", category: "Fruit"}, {name: "Baguette", group: "Yeast", category: "Bread"}]
console .log (JSON.stringify (groupFoods (items), null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
Note the two helper functions. omit clones an object removing certain property names. So, for instance, omit ([age, id]) ({id: 101, first: 'Fred', last: 'Flintstone', age: 27}) returns {first: 'Fred', last: 'Flintstone'} groupBy accepts a function which returns a key value for a value, and returns a function which takes an array of values, and returns an object with the keys found, assbociated with an array of values that generate that key. For instance, groupBy (n => n % 10) ([1, 3, 4, 21, 501, 23, 43, 25, 64]) //=> {"1": [1, 21, 501], "3": [3, 23, 43], "4": [4, 64], "5": [25]}. These are genuinely reusable functions that you might keep in your personal utility library.
The main function simply builds our output using the configuration values supplied. If we need to nest, then we recur on the children node of the configuration.

Filter an object with multiple arrays based on value of one array

This is how my dataset looks like:
const data = {
"VS_factor": [
"FA1_n",
"FA1_y",
"FA2_n"
],
"coord.Dim.1": [
-0.232849099744328,
0.875136458459595,
-0.0810629616429348,
],
"coord.Dim.2": [
0.0223397885030092,
-0.0839615159119212,
-0.334981738274959,
],
"cluster": [
0,
5,
0,
]
}
I want to filter the object and every array inside based the value of the last variable. In this example I only want to keep the values where cluster" === 5.
const desired = {
"VS_factor": [
"FA1_y",
],
"coord.Dim.1": [
0.875136458459595,
],
"coord.Dim.2": [
-0.0839615159119212,
],
"cluster": [
5,
]
}
I have trouble with this since I can not use .filter on an Object. Does anyone know a solution how to archieve my desired result?
You could get the expected index and then filter
const data = {
VS_factor: ["FA1_n", "FA1_y", "FA2_n"],
"coord.Dim.1": [-0.232849099744328, 0.875136458459595, -0.0810629616429348],
"coord.Dim.2": [0.0223397885030092, -0.0839615159119212, -0.334981738274959],
cluster: [0, 5, 0],
}
const expectedIndex = data.cluster.findIndex((c) => c === 5)
const res = Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, [value[expectedIndex]]])
)
console.log(res)
Assuming you want to filter those elements which are not at a specific index (index where cluster === 5).
const data = { "VS_factor": [ "FA1_n", "FA1_y", "FA2_n" ], "coord.Dim.1": [ -0.232849099744328, 0.875136458459595, -0.0810629616429348, ], "coord.Dim.2": [ 0.0223397885030092, -0.0839615159119212, -0.334981738274959, ], "cluster": [ 0, 5, 0, ] },
targetCluster = 5,
targetIndex = data.cluster.findIndex(c => c === targetCluster),
result = Object.entries(data).map(([key, array]) => ({[key]: [array[targetIndex]]}));
console.log(result);
I would define a filter function that takes three arguments: the key to filter on, and the value desired, and the data object
const filterData = (key, value, data) => {
const result = {}
for (let i = 0; i < data[key].length; i++) {
if (data[key][i] === value) { // found the desired Index
Object.entries(data).forEach(([dataKey, dataArr]) => {
if (!result[dataKey]) {
result[dataKey] = []
}
result[dataKey].push(dataArr[i])
})
}
}
return result
}
This function will work on different keys and will also extract multiple 'records' in there are multiple elements with the target value (e.g. there are 2 records with cluster equal to 5.
Some assumption made by the code, add some checks if they are not valid:
The arrays only contain primitive values, to it's safe to check equality with ===.
All keys in the original data have an array has value with the same number of entries
The key passed as argument actually exists in the data
Get the index from the cluster, check if it's found otherwise return object without data. Iterate over Object.values with this index and push the new entries to the properties-arrays.
const data = {
VS_factor: ["FA1_n", "FA1_y", "FA2_n"],
"coord.Dim.1": [-0.232849099744328, 0.875136458459595, -0.0810629616429348],
"coord.Dim.2": [0.0223397885030092, -0.0839615159119212, -0.334981738274959],
cluster: [0, 5, 0],
};
function filterClusterId(id, data) {
let ind = data.cluster.indexOf(id);
let result = {VS_factor: [], "coord.Dim.1": [], "coord.Dim.2": [], cluster: []};
if (ind===-1) return result;
Object.entries(data).forEach(([key,value]) => {
result[key].push(value[ind]);
})
return result;
}
console.log(filterClusterId(5, data));

Using underscore to get all keys and list of unique values for each

Starting with an array of objects, I need get a list of all keys and all the unique values for each. The problem is that I don't know the keys beforehand. There are plenty of solutions if you know the key, but in this case each object can have any number of keys and each key has an array of values. The code below works, but it's quite complicated and there must be a simpler solution.
Made a working JSBIN here
Input:
[
{
key_1: [ attribute_value_1, attribute_value_2, ... ],
key_2: [ attribute_value_3, attribute_value_4, ... ],
},
...
]
Output:
[
{
label: key_1,
options: [ attribute_value_1, attribute_value_2, ... ]
},
{
label: key_2,
options: [ attribute_value_3, attribute_value_4, ... ]
},
...
]
Suggested Solution:
_.chain(input)
.map(function (attr) {
return _.keys(attr).map(function (key) {
return {
key: key,
value: attr[key]
};
});
})
.flatten()
.groupBy('key')
.map(function (grouped_values, key) {
// value = array of { key, value }
return {
label: key,
options: _.chain(grouped_values)
.pluck('value')
.flatten()
.uniq()
.value()
};
})
.value();
Using lodash - Apply _.mergeWith() to the input array, and use the customizer function to combine the arrays and get the unique values. Afterwards _.map() the result to the required format:
var input = [
{
key_1: [ "attribute_value_1", "attribute_value_2" ],
key_2: [ "attribute_value_3", "attribute_value_4" ]
},
{
key_1: [ "attribute_value_1", "attribute_value_5" ],
key_2: [ "attribute_value_2", "attribute_value_3" ],
key_5: [ "attribute_value_2", "attribute_value_3" ]
}
];
var params = [{}].concat(input).concat(function (objValue, srcValue) { // create the params to apply to mergeWith
if (_.isArray(objValue)) {
return _.union(objValue, srcValue); // merge the arrays, and get the unique values
}
});
var result = _.map(_.mergeWith.apply(_, params), function(value, key) { // merge all objects in the array, and map the results to required format
return {
label: key,
options: value
};
});
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.2/lodash.min.js"></script>
And you can clean up the ugly params array if you use ES6:
const input = [
{
key_1: [ "attribute_value_1", "attribute_value_2" ],
key_2: [ "attribute_value_3", "attribute_value_4" ]
},
{
key_1: [ "attribute_value_1", "attribute_value_5" ],
key_2: [ "attribute_value_2", "attribute_value_3" ],
key_5: [ "attribute_value_2", "attribute_value_3" ]
}
];
const customizer = (objValue, srcValue) => {
if (_.isArray(objValue)) {
return _.union(objValue, srcValue); // merge the arrays, and get the unique values
}
};
const result = _.map(_.mergeWith({}, ...input, customizer), (value, key) => ({ // merge all objects in the array, and map the results to required format
label: key,
options: value
}));
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.2/lodash.min.js"></script>
You could write this in Underscore if you prefer, but there's no pressing need to do so.
const input = [
{key_1: [1, 2], key_2: [3, 4]},
{key_3: [5, 6], key_2: [7, 42]}
];
var result = [].concat(...input.map(obj =>
Object.keys(obj).map(key =>
({label: key, options: obj[key]})
)
));
console.log(result);
I'm not quite sure how you want to uniqify the result. Do you mean that you want to run unique within each array such as [ attribute_value_1, attribute_value_2, ... ]?

Categories