How to chunk an array based on another array of numbers? - javascript

I have an array of data, and an array of objects:
const data = ['1', '2', '2'];
const objlist = [{name : 'dummy'} , {name: 'new'}, {name : 'news'}, {name : 'place'}, ...]; // 5 objects
I want to chunk the objects in objlist by the numbers in data so that I get the follow result:
result = [
[{name:'dummy'}],
[{name:'new'}, {name:'news'}],
[{name : 'place'}, ...]
]
As you can see, it should be of the form:
[[{obj1}], [{obj2}, {obj3}], [{obj4}, {obj5}]]

You could push sliced parts to the result.
let array = [1, 2, 2],
objlist = [{ name: 'dummy' }, { name: 'new' }, { name: 'news' }, { name: 'place' }, { name: 'place' }],
result = [],
i = 0,
j = 0;
while (j < array.length) {
result.push(objlist.slice(i, i += array[j++]));
}
console.log(result);

You can loop through your array of numbers and for each number n use .splice(0, n) to get an array chunk from your array of objects. This will modify the array in-place, allowing your next .splice() to get the next consecutive object. For each .splice() you perform, you can .push() this into a resulting array.
See example below:
function partition([...arr], chunks) {
const res = [];
for(const n of chunks)
res.push(arr.splice(0, n)); // +n to turn the string number into a number (splice will do this conversion for you but you can take care of it explicitly as well)
return res;
}
const chunkArr = ['1', '2', '2'];
const arr = [{ name : 'dummy' }, {name: 'new' }, { name : 'news'},{name : 'place'}, {name : 'foo'}];
console.log(partition(arr, chunkArr));
Above I'm using partition([...arr], chunks) which uses the destructuring assignment syntax to perform a shallow copy of your input array. This way when you modify it inside your function using .splice(), it won't change the passed-in array.

Related

How to reduce unsorted array against another sorted array, keeping sorted order?

Given an array of objects named allItems which is pre-sorted, but cannot be sorted again from the information it contains - what is an alternative implementation to the reduce function below that will retain the sorted order of allItems?
The logic below will output:
[{ id: 'd' }, { id: 'a' }, { id: 'b' }]
The desired output is:
[{ id: 'a' }, { id: 'b' }, { id: 'd' }]
// NOTE: allItems is pre-sorted, but lacks the information to re-sort it
const allItems = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const includedIds = ['d', 'a', 'b'];
// QUESTION: How to create the same output, but in the order they appear in allItems
const unsortedIncludedItems = includedIds.reduce((accumulator, id) => {
const found = allItems.find(n => n.id === id);
if (found) accumulator.push(found);
return accumulator;
}, [])
As mentioned in response to #Ben, simply reversing the logic is a deal breaker for performance reasons.
Instead of iterating over the includedIds (in the wrong order) and seeing whether you can find them in allItems, just iterate over allItems (which is the right order) and see whether you can find their ids in includedIds:
const allItems = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const includedIds = ['d', 'a', 'b'];
const includedItems = allItems.filter(item => includedIds.includes(item.id));
The issue you have here is that your code reverses the list. You can simply push to the front of the list instead, and the original order will be maintained.
Unfortunately, pushing to the front of a list is slower, it's O(n) rather than O(1). It looks like Array.prototype.unshift is supposed to be faster, but it's still O(n) according to this blog. Assuming that the number of found elements is small you won't notice any performance issues. In that case, replace push with unshift like so:
// NOTE: allItems is pre-sorted, but lacks the information to re-sort it
const allItems = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const includedIds = ['d', 'a', 'b'];
// QUESTION: How to create the same output, but in the order they appear in allItems
const unsortedIncludedItems = includedIds.reduce((accumulator, id) => {
const found = allItems.find(n => n.id === id);
if (found) accumulator.unshift(found);
return accumulator;
}, [])
Otherwise, these are your options:
Create a wrapper around this object that reverses the indexes rather than the array. This can be done with a function like this:
const getFromEnd = (arr, i) => arr[arr.length - 1 - i]
Note that this can be replaced with arr.at(-i) in new browser versions (last few months). This could be encapsulated within a class if you're feeling OOP inclined.
Remember to manually invert the indices wherever you use this array (this will be bug-prone, as you may forget to invert them)
Reverse the array. As shown in this fiddle, even with 10,000 elements, the performance is not bad. Assuming this isn't a hotpath or user-interactive code, I think that even 100,000 is probably fine.
Update
Example B will use the index of the input array to sort the filtered array.
Try .filter() and .include() to get the desired objects and then .sort() by each object's string value. See Example A.
Another way is to use .flatMap() and .include() to get an array of arrays.
// each index is from the original array
[ [15, {id: 'x'}], [0, {id: 'z'}], [8, {id: 'y'}] ]
Then use .sort() on each sub-array index.
[ [0, {id: 'z'}], [8, {id: 'y'}], [15, {id: 'x'}] ]
Finally, use .flatMap() once more to extract the objects and flatten the array of arrays into an array of objects.
[ {id: 'z'}, {id: 'y'}, {id: 'x'} ]
See Example B
Example A (sort by value)
const all = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const values = ['d', 'a', 'b'];
const sortByStringValue = (array, vArray, key) => array.filter(obj => vArray.includes(obj[key])).sort((a, b) => a[key].localeCompare(b[key]));
console.log(JSON.stringify(sortByStringValue(all, values, 'id')));
Example B (sort by index)
const all = [{id:'a'}, {id:'b'}, {id:'c'}, {id:'d'}, {id:'e'}, {id:'f'}];
const values = ['d', 'a', 'b'];
const alt = [{name:'Matt'}, {name:'Joe'}, {name:'Jane'}, {name:'Lynda'}, {name:'Shelly'}, {name:'Alice'}];
const filter = ['Shelly', 'Matt', 'Lynda'];
const sortByIndex = (array, vArray, key) => array.flatMap((obj, idx) => vArray.includes(obj[key]) ? [[idx, obj]] : []).sort((a, b) => a[0] - b[0]).flatMap(sub => [sub[1]]);
console.log(JSON.stringify(sortByIndex(all, values, 'id')));
console.log(JSON.stringify(sortByIndex(alt, filter, 'name')));
Why not just reverse the logic, Filter out the ids which not suppose to be includes.
// NOTE: allItems is pre-sorted, but lacks the information to re-sort it
const allItems = [
{ id: "a" },
{ id: "b" },
{ id: "c" },
{ id: "d" },
{ id: "e" },
{ id: "f" },
];
const includedIds = ["d", "a", "b"];
const findElms = (items, includedIds) => items.filter((n) => includedIds.includes(n.id))
console.log(findElms(allItems, includedIds));

How to merge an array of objects into one array and then filter out the duplicates

Firstly, I am trying to merge an array of many objects into a single array with every key in each object.
Lastly, any duplicate items in the array should be removed as well as any elements named "name".
Input:
const data = [
{
name: '10/20',
Tyler: 1,
Sonia: 0,
Pedro: 0,
},
{
name: '10/23',
Tyler: 0.5,
Sonia: 0.25,
Pedro: 0.75,
George: 0.5,
},
];
Output:
["Tyler", "Sonia", "Pedro", "George"]
This is what I've tried so far:
const mergedData = data.reduce((prev, cur) => {
const obj = cur[0];
const keys = Object.keys(obj);
const names = keys.splice(1);
return { names };
}, []);
I am trying to capture any key name other than "name" and add it to the final array. However, this is where I get stuck because I get this error, TypeError: Cannot convert undefined or null to object
Note: Objects may be different lengths, contain a mix of names, but never any duplicates.
An option is to find all keys put in a set and remove the name key
const data = [
{
name: '10/20',
Tyler: 1,
Sonia: 0,
Pedro: 0,
},
{
name: '10/23',
Tyler: 0.5,
Sonia: 0.25,
Pedro: 0.75,
George: 0.5,
},
];
const set = new Set(data.reduce((acc, i) => [...acc, ...Object.keys(i)], []));
set.delete('name');
const result = [...set];
console.log(result);
If you have access to ES6 methods, you can do this using a Set (unique values are ensured at creation) and converting it back into an array if you want through Destructuring.
data = [{name: '0', Tyler: '1', Dan: '2', Carl: '3'}, {name: '0', Tyler: '1', Dan: '2', Eric: '3', Danny: '4'}];
const output = (data) => {
let output = [];
// This makes sure you get each array and then strips just the keys as desired
data.forEach(item => {
output = output.
concat(Object.keys(item))
});
// This creates the set, strips our dups, and then spreads itself into an array
return [...new Set(output)]
// Strip out the 'name' key as needed
// NOTE: This should be a param instead of hard-coded, but this is easier to show
.filter(res => res != 'name');
}
console.log(output(data));
This should be fairly performant considering it only navigates the full array one time and each object itself shouldn't have millions of properties to cause .keys() any issues.

How to convert objects inside array to nested array JavaScript?

I have an array containing objects that every element but the last one are objects, but I want to convert them into an array of arrays and add the last element.
To be more explicit here is how I have it:
[
{ 0: [1,2], 1: [6,2], name: "" },
{ 0: [3,4], 1: [2,2], name: "" }
]
and the result I want is this one:
[
{ multipolygon: [ [1,2], [6,2] ], name: ""},
{ multipolygon: [ [3,4], [2,2] ], name: ""}
]
Each single array contained inside the original array is converted into an array of those arrays.
I have tried doing this:
const zonesArray = zones.map(el => Object.values(el)) // obj => array
const polygons = zonesArray.filter(el => el.pop()) // array without name
to get all the arrays contained inside the obj but then I realized how can I replace this into the original objects.
I have tried to modify the groupBy function found on MDN:
function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
let key = obj[property]
if (!acc[key]) {
acc[key] = []
}
acc[key].push(obj)
return acc
}, {})
}
But I can't seem to find the answer
It doesn't look like you're trying to group by a property, you're trying to transform each object in the array separately - which can be done by taking the name and the numeric properties together when mapping, then returning the shape of the new object:
const arr = [
{ 0: [1,2], 1: [6,2], name: "" },
{ 0: [3,4], 1: [2,2], name: "" }
];
const result = arr.map(({ name, ...rest }) => ({
name,
multipolygon: Object.values(rest)
}));
console.log(result);

Convert multidimensional array to objects using dynamically generated and predefined array of items I want to set as keys

Are the steps I'm taking to solve this problem correct?
I'm working on turning a data structure of an array of arrays such as
this.arrayofAnimalsAndValues = [
[ "Ant", 1287, 12956],
[ "Lion", 2574, 25826],
[ "Bear", 3861, 38696],
.....
]
into this
this.jsonOfAnimalsAndValues = [
{category: "Ant", value_1: 1287, value_2:12956},
{category: "Lion", value_1: 2574, value_2:25826},
{category: "Bear", value_1: 3861, value_2:38696},
.....
]
where the first item in the array is always assigned to 'category' in the array object and the following items assigned to value_# depending on their order in the array. So the 2nd array item would have key value_1 and so on. For example, for 1 nested array:
[[ "Ant", 5148, 51566]] to =>
[{category: "Ant", value_1: "5148", value_2: 51566}]
I've created a hardcoded way to achieve this however I'm trying to make it dynamic:
'hardcoded' way:
this.variableOfKeys = ["value_1", "value_2", "value_3", ......]
this.jsonOfAnimalsAndValues = this.arrayofAnimalsAndValues(function(x) {
return {
category: x[0],
no1: x[1],
no2: x[2],
.....
};
});
where I just hardcode the keys and their values (values using their index).
My attempt to make it dynamic:
this.variableOfKeys.forEach(element => {
this.jsonOfAnimalsAndValues = this.arrayofAnimalsAndValues.map(function(x) {
for (var i = 0; i<=this.arrayOfValuesToUseAsKeys.length; ++i) {
return {
category: x[0],
element: x[i+1],
};
}
});
});
where my logic is that for each item in
this.variableOfKeys = ["value_1", "value_2", "value_3", ......],
I created this.jsonOfAnimalsAndValues such that the first item (item with the 0th index) in the array this.arrayofAnimalsAndValues is assigned to the key category and the following items (depending on their index) are assigned to the values in this.variableOfKeys in order starting from no1, then no2 etc.
However, I don't think this is written correctly and I keep getting this error:
"TypeError: Cannot read property 'variableOfKeys' of undefined"
Can I ask how it might be written incorrectly and so how I might be able to create this.jsonOfAnimalsAndValues from this.arrayofAnimalsAndValues?
You can map each subarray to an array of entries, then turn it into an object to return with Object.fromEntries:
const arrayofAnimalsAndValues = [
[ "Ant", 1287, 12956],
[ "Lion", 2574, 25826],
[ "Bear", 3861, 38696],
];
const output = arrayofAnimalsAndValues.map(
([category, ...rest]) => Object.fromEntries([
['category', category],
...rest.map((value, i) => ['value_' + (i + 1), value])
])
);
console.log(output);
Given an array of the keys you can map() the main array and use reduce() on each subarray to generate each object
const arr=[["Ant",1287,12956],["Lion",2574,25826],["Bear",3861,38696]],
keys = ['category','value_1','value_2'];
const res = arr.map(e => e.reduce((a,c,i) => (a[keys[i]] = c, a),{}))
console.log(res)
.as-console-wrapper { max-height: 100%!important;top:0}

Merge objects with corresponding key values from two different arrays of objects

I've got two arrays that have multiple objects
[
{
"name":"paul",
"employee_id":"8"
}
]
[
{
"years_at_school": 6,
"department":"Mathematics",
"e_id":"8"
}
]
How can I achieve the following with either ES6 or Lodash?
[
{
"name":"paul",
"employee_id":"8"
"data": {
"years_at_school": 6
"department":"Mathematics",
"e_id":"8"
}
}
]
I can merge but I'm not sure how to create a new child object and merge that in.
Code I've tried:
school_data = _.map(array1, function(obj) {
return _.merge(obj, _.find(array2, {employee_id: obj.e_id}))
})
This merges to a top level array like so (which is not what I want):
{
"name":"paul",
"employee_id":"8"
"years_at_school": 6
"department":"Mathematics",
"e_id":"8"
}
The connector between these two is "employee_id" and "e_id".
It's imperative that it's taken into account that they could be 1000 objects in each array, and that the only way to match these objects up is by "employee_id" and "e_id".
In order to match up employee_id and e_id you should iterate through the first array and create an object keyed to employee_id. Then you can iterate though the second array and add the data to the particular id in question. Here's an example with an extra item added to each array:
let arr1 = [
{
"name":"mark",
"employee_id":"6"
},
{
"name":"paul",
"employee_id":"8"
}
]
let arr2 = [
{
"years_at_school": 6,
"department":"Mathematics",
"e_id":"8"
},
{
"years_at_school": 12,
"department":"Arr",
"e_id":"6"
}
]
// empObj will be keyed to item.employee_id
let empObj = arr1.reduce((obj, item) => {
obj[item.employee_id] = item
return obj
}, {})
// now lookup up id and add data for each object in arr2
arr2.forEach(item=>
empObj[item.e_id].data = item
)
// The values of the object will be an array of your data
let merged = Object.values(empObj)
console.log(merged)
If you perform two nested O(n) loops (map+find), you'll end up with O(n^2) performance. A typical alternative is to create intermediate indexed structures so the whole thing is O(n). A functional approach with lodash:
const _ = require('lodash');
const dataByEmployeeId = _(array2).keyBy('e_id');
const result = array1.map(o => ({...o, data: dataByEmployeeId.get(o.employee_id)}));
Hope this help you:
var mainData = [{
name: "paul",
employee_id: "8"
}];
var secondaryData = [{
years_at_school: 6,
department: "Mathematics",
e_id: "8"
}];
var finalData = mainData.map(function(person, index) {
person.data = secondaryData[index];
return person;
});
Sorry, I've also fixed a missing coma in the second object and changed some other stuff.
With latest Ecmascript versions:
const mainData = [{
name: "paul",
employee_id: "8"
}];
const secondaryData = [{
years_at_school: 6,
department: "Mathematics",
e_id: "8"
}];
// Be careful with spread operator over objects.. it lacks of browser support yet! ..but works fine on latest Chrome version for example (69.0)
const finalData = mainData.map((person, index) => ({ ...person, data: secondaryData[index] }));
Your question suggests that both arrays will always have the same size. It also suggests that you want to put the contents of array2 within the field data of the elements with the same index in array1. If those assumptions are correct, then:
// Array that will receive the extra data
const teachers = [
{ name: "Paul", employee_id: 8 },
{ name: "Mariah", employee_id: 10 }
];
// Array with the additional data
const extraData = [
{ years_at_school: 6, department: "Mathematics", e_id: 8 },
{ years_at_school: 8, department: "Biology", e_id: 10 },
];
// Array.map will iterate through all indices, and gives both the
const merged = teachers.map((teacher, index) => Object.assign({ data: extraData[index] }, teacher));
However, if you want the data to be added to the employee with an "id" matching in both arrays, you need to do the following:
// Create a function to obtain the employee from an ID
const findEmployee = id => extraData.filter(entry => entry.e_id == id);
merged = teachers.map(teacher => {
const employeeData = findEmployee(teacher.employee_id);
if (employeeData.length === 0) {
// Employee not found
throw new Error("Data inconsistency");
}
if (employeeData.length > 1) {
// More than one employee found
throw new Error("Data inconsistency");
}
return Object.assign({ data: employeeData[0] }, teacher);
});
A slightly different approach just using vanilla js map with a loop to match the employee ids and add the data from the second array to the matching object from the first array. My guess is that the answer from #MarkMeyer is probably faster.
const arr1 = [{ "name": "paul", "employee_id": "8" }];
const arr2 = [{ "years_at_school": 6, "department": "Mathematics", "e_id": "8" }];
const results = arr1.map((obj1) => {
for (const obj2 of arr2) {
if (obj2.e_id === obj1.employee_id) {
obj1.data = obj2;
break;
}
}
return obj1;
});
console.log(results);

Categories