Split array with condition on element - javascript

So I have an array looks like this:
[
{ date: '2021-07-07' },
{ date: '2021-07-07' },
{ date: '2021-07-07' },
{ date: '2021-07-08' },
{ date: '2021-07-09' },
{ date: '2021-07-10' },
{ date: '2021-07-10' }
];
How can I split into 3 array (What I mean is 1 group for unique dates, and another group for duplicate, but if there more than 1 duplicate group, it should separate into another group)
It will looks like this after split
Array 1
[{"date": "2021-07-07"},{"date": "2021-07-07"},{"date": "2021-07-07"}]
Array 2
[{"date": "2021-07-08"},{"date": "2021-07-09"}]
Array 3
[{"date": "2021-07-10"},{"date": "2021-07-10"}]
Below is my code so far, but it only work if the duplicate on have 1
const findDuplicates = arr => {
let sorted_arr = arr.slice().sort();
let result = [];
for (let i = 0; i < sorted_arr.length - 1; i++) {
if (sorted_arr[i + 1].date == sorted_arr[i].date) {
result.push(sorted_arr[i]);
}
}
return result;
};
const filterSame = arr => {
let temp = findDuplicates(arr);
const result = arr.filter(date => date.date == temp[0].date);
return result;
};
const filterUnique = array => {
let result = array.filter(
(e, i) => array.findIndex(a => a['date'] === e['date']) === i
);
let temp = findDuplicates(array);
result = result.filter(function(obj) {
return obj.date !== temp[0].date;
});
return result;
};

You could create a map, keyed by dates, and with as value an empty array. Then populate those arrays. Finally extract the arrays that have more than one element, and add to that result a combined array of those single-element arrays:
function group(data) {
let map = new Map(data.map(o => [o.date, []]));
for (let o of data) map.get(o.date).push(o);
return [
...[...map.values()].filter(({length}) => length > 1),
[...map.values()].filter(({length}) => length == 1).flat()
];
}
let data = [{"date":"2021-07-07"},{"date":"2021-07-07"},{"date":"2021-07-07"},{"date":"2021-07-08"},{"date":"2021-07-09"},{"date":"2021-07-10"},{"date":"2021-07-10"}];
console.log(group(data));
Explanation
let map = new Map(data.map(o => [o.date, []]));
This creates a Map. The constructor is given an array of pairs. For the example data that array looks like this:
[
["2021-07-07", []],
["2021-07-07", []],
["2021-07-07", []],
["2021-07-08", []],
["2021-07-09", []],
["2021-07-10", []],
["2021-07-10", []]
]
The Map constructor will create the corresponding Map, which really removes duplicates. You can imagine it as follows (although it is not a plain object):
{
"2021-07-07": [],
"2021-07-08": [],
"2021-07-09": [],
"2021-07-10": []
}
Then the for loop will populate these (four) arrays, so that the Map will look like this:
{
"2021-07-07": [{date:"2021-07-07"},{date:"2021-07-07"},{date:"2021-07-07"}],
"2021-07-08": [{date:"2021-07-08"}],
"2021-07-09": [{date:"2021-07-09"}],
"2021-07-10": [{date:"2021-07-10"},{date:"2021-07-10"}]
}
In the return statement the Map values are converted to an array twice. Once to filter the entries that have more than 1 element:
[
[{date:"2021-07-07"},{date:"2021-07-07"},{date:"2021-07-07"}],
[{date:"2021-07-10"},{date:"2021-07-10"}]
]
...and a second time to get those that have 1 element:
[
[{date:"2021-07-08"}],
[{date:"2021-07-09"}],
]
The second array is flattened with flat():
[
{date:"2021-07-08"},
{date:"2021-07-09"},
]
The final result concatenates the first array (with duplicate dates) with the flattened array (with unique dates), using the spread syntax (...)

This could be done in a 2 step process
a typical group by operation based on the date properties
aggregating together all the groups which only have 1 result.
const input = [{"date":"2021-07-07"},{"date":"2021-07-07"},{"date":"2021-07-07"},{"date":"2021-07-08"},{"date":"2021-07-09"},{"date":"2021-07-10"},{"date":"2021-07-10"}]
const grouped = input.reduce ( (acc,i) => {
if(!acc[i.date]) acc[i.date] = []
acc[i.date].push(i);
return acc;
},{});
const final = Object.values(Object.entries(grouped).reduce( (acc,[key,values]) => {
if(values.length>1) {
acc[key] = values;
}
else{
if(!acc.others) acc.others = [];
acc.others.push(values[0]);
}
return acc
},{}))
console.log(final);
Note that if you added, for example, 2021-07-11 to your original array, this would get lumped in with all the other "unique" elements. This may or may not be what you expected, but was not clear from the question.

Another option is to sort the array before grouping. If the current date being looped isn't the same as its neighbors, then it doesn't have duplicates.
const input = [{"date":"2021-07-07"},{"date":"2021-07-07"},{"date":"2021-07-07"},{"date":"2021-07-08"},{"date":"2021-07-09"},{"date":"2021-07-10"},{"date":"2021-07-10"}]
input.sort((a,b) => a.date.localeCompare(b.date))
const grouped = input.reduce((acc, o, i, arr) => {
const key = o.date === arr[i-1]?.date || o.date === arr[i+1]?.date
? o.date
: 'lonely'
acc[key] ||= []
acc[key].push(o);
return acc;
}, {});
console.log(Object.values(grouped));

Related

How to create multiple objects from array of entries in JavaScript?

I receive an array of entries from form using FormData(). It consists of information about the recipe. Like this:
const dataArr = [
['title', 'pizza'],
['image', 'url'],
['quantity-0', '1'],
['unit-0', 'kg'],
['description-0', 'Flour'],
['quantity-1', '2'],
['unit-1', 'tbsp'],
['description-1', 'Olive oil'],
... // more ingredients
];
which I need to reorganize in new object, like this:
const recipe = {
title: 'pizza',
image: 'url',
ingredients: [
{ quantity: '1', unit: 'kg', ingredient: 'Flour' },
{ quantity: '2', unit: 'tbsp', ingredient: 'Olive oil' },
...
],
};
So, for ingredients array I need to create multiple objects from received data. I came up with needed result, but it's not clean. I would appreciate your help coming up with universal function, when number of ingredients is unknown.
My solution: Form receives 6 ingredients max, therefore:
const ingredients = [];
// 1. Create an array with length of 6 (index helps to get ingredient-related data looping over the array)
const arrayOf6 = new Array(6).fill({});
arrayOf6.forEach((_, i) => {
// 2. In each iteration filter over all data to get an array for each ingredient
const ingArr = dataArr.filter(entry => {
return entry[0].startsWith(`unit-${i}`) ||
entry[0].startsWith(`quantity-${i}`) ||
entry[0].startsWith(`ingredient-${i}`);
});
// 3. Loop over each ingredient array and rename future properties
ingArr.forEach(entry => {
[key, value] = entry;
if(key.includes('ingredient')) entry[0] = 'description';
if(key.includes('quantity')) entry[0] = 'quantity';
if(key.includes('unit')) entry[0] = 'unit';
});
// 4. Transform array to object and push into ingredients array
const ingObj = Object.fromEntries(ingArr);
ingredients.push(ingObj);
});
// To finalize new object
const dataObj = Object.fromEntries(dataArr);
const recipe = {
title: dataObj.title,
image: dataObj.image,
ingredients,
};
You'll have to parse the values of the input array to extract the index. To build the result object, you could use reduce:
const dataArr = [['title', 'pizza'],['image', 'url'],['quantity-0', '1'],['unit-0', 'kg'],['description-0', 'Flour'],['quantity-1', '2'],['unit-1', 'tbsp'], ['description-1', 'Olive oil']];
const recipe = dataArr.reduce((recipe, [name, value]) => {
const [, prop, index] = name.match(/^(\w+)-(\d+)$/) ?? [];
if (prop) {
(recipe.ingredients[index] ??= {})[prop] = value;
} else {
recipe[name] = value;
}
return recipe;
}, { ingredients: [] });
console.log(recipe);
You don't need arrayOf6. You never use its elements for anything -- it seems like you're just using it as a replacement for a loop like for (let i = 0; i < 6; i++).
Just loop over dataArr and check whether the name has a number at the end. If it does, use that as an index into the ingredients array, otherwise use the name as the property of the ingredients object. Then you don't need to hard-code a limit to the number of ingredients.
const dataArr = [
['title', 'pizza'],
['image', 'url'],
['quantity-0', '1'],
['unit-0', 'kg'],
['description-0', 'Flour'],
['quantity-1', '2'],
['unit-1', 'tbsp'],
['description-1', 'Olive oil'],
// more ingredients
];
const recipe = {
ingredients: []
};
dataArr.forEach(([name, value]) => {
let match = name.match(/^(\w+)-(\d+)$/);
if (match) {
let type = match[1];
let index = match[2];
if (!recipe.ingredients[index]) {
recipe.ingredients[index] = {};
}
recipe.ingredients[index][type] = value;
} else {
recipe[name] = value;
}
});
console.log(recipe);
Separating the key-parsing logic helps me think about the concerns more clearly:
const orderedKeyRegExp = /^(.+)-(\d+)$/;
function parseKey (key) {
const match = key.match(orderedKeyRegExp);
// Return -1 for the index if the key pattern isn't part of a numbered sequence
if (!match) return {index: -1, name: key};
return {
index: Number(match[2]),
name: match[1],
};
}
function transformRecipeEntries (entries) {
const result = {};
const ingredients = [];
for (const [key, value] of entries) {
const {index, name} = parseKey(key);
if (index >= 0) (ingredients[index] ??= {})[name] = value;
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// Assign an empty object to the element of the array at the index
// (if it doesn't already exist)
else result[name] = value;
}
if (ingredients.length > 0) result.ingredients = ingredients;
return result;
}
const entries = [
['title', 'pizza'],
['image', 'url'],
['quantity-0', '1'],
['unit-0', 'kg'],
['description-0', 'Flour'],
['quantity-1', '2'],
['unit-1', 'tbsp'],
['description-1', 'Olive oil'],
// ...more ingredients
];
const result = transformRecipeEntries(entries);
console.log(result);

How to use the result of an iteration to re-iterate?

I need to create a new array from another with the condition:
for example from an array
mainArr: [
{
"id":1,
"name":"root"
},
{
"id":2,
"parentId":1,
"name":"2"
},
{
"id":148,
"parentId":2,
"name":"3"
},
{
"id":151,
"parentId":148,
"name":"4"
},
{
"id":152,
"parentId":151,
"name":"5"
}
]
I need to make an array ['1','2','148','151'] which means the path from "parentId"'s to "id":152 - (argument for this function).
I think main logic can be like this:
const parentsArr = [];
mainArr.forEach((item) => {
if (item.id === id) {
parentsArr.unshift(`${item.parentId}`);
}
and the result {item.parentId} should be used to iterate again. But I don't understand how to do it...
You could use a recursive function for this. First you can transform your array to a Map, where each id from each object points to its object. Doing this allows you to .get() the object with a given id efficiently. For each object, you can get the parentId, and if it is defined, rerun your traverse() object again searching for the parent id. When you can no longer find a parentid, then you're at the root, meaning you can return an empty array to signify no parentid object exist:
const arr = [{"id":1,"name":"root"},{"id":2,"parentId":1,"name":"2"},{"id":148,"parentId":2,"name":"3"},{"id":151,"parentId":148,"name":"4"},{"id":152,"parentId":151,"name":"5"}];
const transform = arr => new Map(arr.map((o) => [o.id, o]));
const traverse = (map, id) => {
const startObj = map.get(+id);
if("parentId" in startObj)
return [...traverse(map, startObj.parentId), startObj.parentId];
else
return [];
}
console.log(traverse(transform(arr), "152"));
If you want to include "152" in the result, you can change your recursive function to use the id argument, and change the base-case to return [id] (note that the + in front of id is used to convert it to a number if it is a string):
const arr = [{"id":1,"name":"root"},{"id":2,"parentId":1,"name":"2"},{"id":148,"parentId":2,"name":"3"},{"id":151,"parentId":148,"name":"4"},{"id":152,"parentId":151,"name":"5"}];
const transform = arr => new Map(arr.map((o) => [o.id, o]));
const traverse = (map, id) => {
const startObj = map.get(+id);
if("parentId" in startObj)
return [...traverse(map, startObj.parentId), +id];
else
return [+id];
}
console.log(traverse(transform(arr), "152"));
I would start by indexing the data by id using reduce
var byId = data.reduce( (acc,i) => {
acc[i.id] = i
return acc;
},{});
And then just go through using a loop and pushing the id to a result array
var item = byId[input];
var result = []
while(item.parentId) {
result.push(item.parentId)
item = byId[item.parentId];
}
Live example:
const input = 152;
const data = [ { "id":1, "name":"root" }, { "id":2, "parentId":1, "name":"2" }, { "id":148, "parentId":2, "name":"3" }, { "id":151, "parentId":148, "name":"4" }, { "id":152, "parentId":151, "name":"5" } ]
var byId = data.reduce( (acc,i) => {
acc[i.id] = i
return acc;
},{});
var item = byId[input];
var result = []
while(item.parentId) {
result.push(item.parentId)
item = byId[item.parentId];
}
console.log(result.reverse());
Try changing this line
parentsArr.unshift(`${item.parentId}`);
To this
parentsArr.push(`${item.parentId}`);
Then try
console.log(parentsArr);
This is what I ended up with. Basically a mix of Janek and Nicks answers. It's just 2 steps:
transform code to a map.
extract the ancester_id's with a little function
let data = [
{"id":1,"name":"root"},
{"id":2,"parentId":1,"name":"2"},
{"id":148,"parentId":2,"name":"3"},
{"id":151,"parentId":148,"name":"4"},
{"id":152,"parentId":151,"name":"5"}
];
data = data.reduce( (acc, value) => {
// could optionally filter out the id here
return acc.set(value.id, value)
}, new Map());
function extract_ancestors( data, id ) {
let result = [];
while( data.get( id ).parentId ) {
id = data.get( id ).parentId;
result.push(id)
}
return result;
}
// some visual tests
console.log( extract_ancestors( data, 152 ) );
console.log( extract_ancestors( data, 148 ) );
console.log( extract_ancestors( data, 1 ) );
PS: My OOP tendencies start to itch so much from this haha.

How to make a sorted array of objects have distinct values

Lets say I have the following array of objects:
let teams = [
{ Name: "Los Angeles Lakers", Championships: "17" },
{ Name: "Boston Celtics", Championships: "17" },
{ Name: "Cleveland Cavaliers", Championships: "01" },
{ Name: "San Antonio Spurs", Championships: "05" },
];
I sort the array:
teams.sort((a, b) => parseInt(a.Championships).localeCompare(parseInt(b.Championships)));
And so I have:
The thing is now I want to have distinct values for the property Championships, so the final result would be (I don't care which final result I will have, I just want to make it work):
Or the following:
How can I do it properly?
Thank you all!
There are a couple ways of doing this.
A naïve approach would be just to iterate over the teams collection, search the results to see if you are already including a team with that specific championship count. If there is no team, add it. This can be slow because you will need to iterate over the results N times for N items but is probably good enough for normal use cases.
const distinctBy = (array, keySelector) => {
return array.reduce((result, i) => {
const index = result.findIndex((j) => keySelector(j) === keySelector(i));
if (index === -1) {
result.push(i);
}
return result;
}, []);
};
distinctBy(teams, (team) => team.Championships);
The performance of this can be improved on my using a set to store known keys potentially reducing iteration count by an order of magnitude from the previous solution.
const distinctBy = (array, keySelector) => {
const result = [];
const keys = new Set();
for (let item of array) {
const key = keySelector(item);
if (!keys.has(key)) {
keys.add(key);
result.push(item);
}
}
return result;
};
distinctBy(teams, (team) => team.Championships);
If you already know your data set is sorted, you can simply iterate the collection and include values as long as the current key does not match the previous key. This common technique in databases and is very fast. Again it requires data to be sorted.
const distinctBy = (array, keySelector) => {
const result = [];
let previous = "";
for (let item of array) {
const key = keySelector(item);
if (key != previous) {
result.push(item);
}
previous = key;
}
return result;
};
distinctBy(teams, (team) => team.Championships);

Two object arrays: merge with same key

I have two object arrays. I want to merge with key with value
var a = [{"fit":["34","32","30","28"],"size":["x"]}]
var b = [{"size":["s","m","xl"],"fit":["36"]}]
Expected Output should be
Obj=[{"fit":["34","32","30","28","36"],"size":["x,"s","m","xl"]}]
My Code is
let arr3 = [];
b.forEach((itm, i) => {
arr3.push(Object.assign({}, itm, a[i]));
});
alert(JSON.stringify(arr3))
it gives [{"size":["x"],"fit":["34","32","30","28"]}] which wrong.
Use Array.reduce().
// Combine into single array (spread operator makes this nice)
const myArray = [...a, ...b];
// "reduce" values in array down to a single object
const reducedArray = myArray.reduce((acc, val) => {
return [{fit: [...acc.fit, ...val.fit], size: [...acc.size, ...val.size]}];
});
Edit: if you want the reducer to merge objects regardless of what keys and fields it has then you can do by iterating over the keys of the objects and merging them dynamically:
const reducedArray = myArray.reduce((acc, val) => {
const returnObject = {};
for (const eaKey in acc) {
returnObject[eaKey] = [...acc[eaKey], ...val[eaKey]];
}
return [returnObject];
});
If the fields of the objects aren't guaranteed keys then you will need to get even more dynamic in detecting the type of merge and how to do it, but it's possible and I will leave that as an exercise for you to figure out. :)
Note that if there are duplicate values in each of the "fit" and "size" arrays, they will not be deduplicated. You'd have to do that manually as a separate step either with extra logic in the reduce function or afterwards.
combine a and b in a single array then reduce it starting with an array having an object with empty fit and size arrays:
var a = [{ fit: ["34", "32", "30", "28"], size: ["x"] }];
var b = [{ size: ["s", "m", "xl"], fit: ["36"] }];
var obj = [...a, ...b].reduce(
(acc, curr) => {
Object.keys(curr).forEach(k => {
acc[0][k] = [...new Set([...(acc[0][k] || []), ...curr[k]])];
});
return acc;
},
[{}]
);
console.log(obj);
You can create a combine function that takes fit and size from any two objects and merges them.
Use it as a reducer to combine everything.
let combine = ({fit, size}, {fit: fit2, size: size2}) =>
({ fit: [...fit, ...fit2], size: [...size, ...size2] });
let result = [...a, ...b].reduce(combine);
Example:
var a = [{"fit":["34","32","30","28"],"size":["x"]}, {"fit": ["10", "11"], "size":["xxxxxxxxl"]}]
var b = [{"size":["s","m","xl"],"fit":["36"]}];
let combine = ({fit, size}, {fit: fit2, size: size2}) =>
({ fit: [...fit, ...fit2], size: [...size, ...size2] });
let result = [...a, ...b].reduce(combine);
console.log(result);
If you don't want to use the keys directly you could try
const arr3 = b.reduce((carry, current, index) => {
Object.keys(current)
.forEach(key => {
Object.assign(carry, { [key]: Array.prototype.concat.call(current[key], a[index][key])});
});
return carry;
}, {});

How to reduce an array with duplicate value?

I have an array like this
let oldArray=[
{type:16,img:['1']},
{type:16,img:['2']},
{type:16,img:['3']},
{type:17,img:['4']}
]
if the type is the same, i want to concat the value.
The result I want is:
let newArray=[
{type:16,img:['1','2','3']},
{type:17,img:['4']}
]
I tried to used reduce function:
oldArray.reduce((acc,cur,idx,src)=>{
if(cur.type===a[idx+1].type){
cur.img.concat(a[idx+1].img);
acc.push(cur)
} else {
acc.push(a[idx+1])
}
return acc
},[])
It seems that there is an error
Can anyone help? Thanks.
Alternative to Bibberty's solution:flatMap is much clearer than reduce
let newArray = [...new Set(oldArray.map(e => e.type))]
.map(e => {
return {
type: e,
img: (oldArray.filter(i => i.type === e).map(x => x.img)).reduce((acc,cur,idx,src)=>{
let length=src.length
let tep=cur.concat(src[idx+1]);
src[idx+1]=tep
return src[idx=length-1]
},[])
}
});
console.log(newArray);
You can use reduce:
let oldArray = [{type: 16,img: ['1']},{type: 16,img: ['2']},{type: 16,img: ['3']},{type: 17,img: ['4']}];
let newArray = oldArray.reduce((acc, curr) => {
acc.some(({
type
}) => type == curr.type) ? acc.find(({
type
}) => type == curr.type).img.push(curr.img[0]) : acc.push(curr);
return acc;
}, []);
console.log(newArray);
We use a Set and then a map.
The Set is populate with the unique types by using a map to extract.
We wrap in [] to give us an array the we then re map to build our object back.
The map then rebuilds our objects and note the use of filter and map to get the img values from the original host array.
let oldArray=[
{type:16,img:['1']},
{type:16,img:['2']},
{type:16,img:['3']},
{type:17,img:['4']}
]
let newArray = [...new Set(oldArray.map(e => e.type))]
.map(e => {
return {
type: e,
img: oldArray.filter(i => i.type === e).flatMap(x => x.img)
}
});
console.log(newArray);
This solution is not a reduce but return result you are looking for is the same
let oldArray = [
{type:16,img:['1']},
{type:16,img:['2']},
{type:16,img:['3']},
{type:17,img:['4']}
];
const transitoryMap = new Map();
for (const item of oldArray) {
if (!transitoryMap.has(item.type)) {
transitoryMap.set(item.type, [item.img[0]])
} else {
const value = transitoryMap.get(item.type)
value.push(item.img[0])
transitoryMap.set(item.type, value)
}
}
const newArray = [];
for (const item of transitoryMap.keys()) {
newArray.push({type:item,img:transitoryMap.get(item)})
}
console.log(newArray)
Here is an example using reduce. I have added a tracker to keep track of type in the newArray.
let oldArray = [
{type:16,img:['1']},
{type:16,img:['2']},
{type:16,img:['3']},
{type:17,img:['4']}
];
oldArray.reduce((a,c)=>{
let index = a.tracker.indexOf(c.type);
if(index === -1) {
a.tracker.push(c.type);
a.newArray.push({...c, img:[...c.img]});
} else {
a.newArray[index].img.push(...c.img);
}
return a;
},{tracker:[],newArray:[]}).newArray;
You might want to consider breaking up the processing into separate simple steps, for example:
Create a flattened object with the appropriate data.
build a new array with the wanted structure.
This will not only keep your code simple, but will allow you to focus on what your code is actually doing instead of how it is doing the task.
var oldArray=[
{type:16,img:['1']},
{type:16,img:['2']},
{type:16,img:['3']},
{type:17,img:['4']}
]
flattenMyObject = (arr) =>
arr.reduce((accum, current) => {
!!accum[current.type] ? accum[current.type].push(...current.img) : accum[current.type] = current.img;
return accum;
}, {});
buildNewArray = (type) => {
return {type: type, img: flattenedObject[type] }
}
Object
.keys(flattenMyObject(oldArray))
.map(buildNewArray);

Categories

Resources