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

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.

Related

Fastest way to clean path collisions in array of strings

This is a hard one to explain, but here goes. I need to clean an array of 'path' strings where if a path has sub properties it not include the top level property. but only the child properties
E.g
[
'firstName',
'address',
'address.local.addressLine1',
'address.local.addressLine2',
'address.local',
]
Should become:
[
'firstName',
'address.local.addressLine1',
'address.local.addressLine2',
'address.local',
]
I have a fairly verbose function kind of working so far, but looking to see if there is a more elegant/better solution than this:
function cleanCollisions(array) {
var output = [];
// return [...new Set(array)];
var map = array.reduce(function(set, field) {
if (!Boolean(field)) {
return set;
}
////////////////
var rootKey = field.split('.')[0];
if(!set[rootKey]) {
set[rootKey] =[];
}
var count = field.split('.').length -1;
if(count) {
set[rootKey].push(field);
}
return set;
}, {})
for(const key in map) {
value = map[key];
if(value.length) {
output.push(value);
} else {
output.push(key);
}
}
////////////////
return output.flat();
}
I'd first iterate over the array to extract the top property of all strings that have sub properties, then filter out all those top properties.
const input = [
'firstName',
'address',
'address.local.addressLine1',
'address.local.addressLine2',
'address.local',
];
const topLevelProps = new Set();
for (const str of input) {
const match = str.match(/^(.*?)\./);
if (match) {
topLevelProps.add(match[1]);
}
}
const output = input.filter(str => !topLevelProps.has(str));
console.log(output);
A variation of the answer by CertainPerformance but using filter and map instead of regex:
const paths = [
'firstName',
'address',
'address.local.addressLine1',
'address.local.addressLine2',
'address.local',
];
const roots = paths.filter(p => p.includes('.')).map(p => p.split('.')[0]);
const cleansed = paths.filter(p => p.includes('.') || !roots.includes(p));
console.log(cleansed);

Split array with condition on element

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));

After adding in Array element change oher element but not adding to array

i've got an array:
dataSet: [
{ name: "Имя1", image: "img.jpeg", author: "Александр Полтавченко", date: "21.02.2020", id: 1 },
{ name: "Имя2", image: "img.png", author: "Александр Полтавченко", date: "21.02.2020", id: 2 },
],
addedToCart: []
and here is the function which put value from dataSet to addedToCart according ID from props:
added = (id) => {
this.setState (( { addedToCart, dataList } )=>{
const newItem = dataList.filter(el=>el.id===id);
const testArr = [...addedToCart ];
const filteredATC = testArr.filter((item, el)=>{
if(addedToCart.indexOf(item)===el){
item.count++
return item, el
}
else {
return item
}
it is works well (only one element with count ++) but if click add to another element it is just change element in array (with correct count surprisingly).
How to put another element into addedToCart, just like
[
{el1},
{el2}
]
filter returns an array instead of the desired element, you should use find instead.
I believe you would desire an approach like this:
added = (id) => {
this.setState (( { addedToCart, dataList } ) => {
const newItem = dataList.find(el=> el.id === id);
const testArr = [...addedToCart ];
const filteredATCIndex = testArr.findIndex((_item, id) => newItem.id === id)
// if there is an added item
if (filteredATCIndex !== -1) {
const count = testArr[filteredATCIndex].count + 1
testArr[filteredATCIndex] = { ...testArr[filteredATCIndex], count }
return { addedToCart: testArr }
}
// for new item
const newItemAdded = { ...newItem, count: 1 }
testArr.push(newItemAdded)
return { addedToCart: testArr }
})
}
though this approach duplicates data, which is not desirable. I suggest you consider to change addedToCart to an object where key value pairs are the id and count respectively from added items. This way you would avoid duplicating data.
then your update state would look like:
added = (id) => {
this.setState (( { addedToCart } ) => {
const count = typeof addedToCart[id] === 'undefined' ? 1 : ++addedToCart[id]
return { addedToCart: { ...addedToCart, [id]: count } }
})
}

Dynamically get values of object from array

Let's say I have an Object myBook and an array allCategories.
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {
isItScienceFiction: true,
isItManga: false,
isItForKids: false
}
What I want : Loop over categories to check the value of Book, for example, check if "sciencefiction" exists in my Book Object and then check it's value
What I have tried :
1) With indexOf
allCategories.map((category) => {
Object.keys(myBook).indexOf(category)
// Always returns -1 because "sciencefiction" doesn't match with "isItScienceFiction"
});
2) With includes
allCategories.map((category) => {
Object.keys(myBook).includes(category)
// Always returns false because "sciencefiction" doesn't match with "isItScienceFiction"
});
Expected output :
allCategories.map((category) => {
// Example 1 : Returns "sciencefiction" because "isItScienceFiction: true"
// Example 2 : Returns nothing because "isItManga: false"
// Example 3 : Returns nothing because there is not property in myBook with the word "school"
// Example 4 : Returns nothing because there is not property in myBook with the word "art"
// If category match with myBook categories and the value is true then
return (
<p>{category}</p>
);
});
If you need more information, just let me know, I'll edit my question.
You could use filter and find methods to return new array of categories and then use map method to return array of elements.
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {isItScienceFiction: true, isItManga: false, isItForKids: false}
const result = allCategories.filter(cat => {
const key = Object.keys(myBook).find(k => k.slice(4).toLowerCase() === cat);
return myBook[key]
}).map(cat => `<p>${cat}</p>`)
console.log(result)
You can also use reduce instead of filter and map and endsWith method.
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {isItScienceFiction: true,isItManga: false,isItForKids: false}
const result = allCategories.reduce((r, cat) => {
const key = Object.keys(myBook).find(k => k.toLowerCase().endsWith(cat));
if(myBook[key]) r.push(`<p>${cat}</p>`)
return r;
}, [])
console.log(result)
You can use
Object.keys(myBook).forEach(function(key){console.log(myBook[key])})
... place you code instead of console.log. This can do the trick without hard coding and also the best practice.
You should really not keep a number of properties containing booleans. While that might work for 1, 2 or 3 categories, for a few hundred it won't work well. Instead, just store the categories in an array:
const myBook = {
categories: ["sciencefiction", "manga", "kids"],
};
If you got some object with the old structure already, you can easily convert them:
const format = old => {
const categories = [];
if(old.isItScienceFiction)
categories.push("sciencefiction");
if(old.isItManga)
categories.push("manga");
if(old.isItForKids)
categories.push("kids");
return { categories };
};
Now to check wether a book contains a certain category:
const isManga = myBook.categories.includes("manga");
And your rendering is also quite easy now:
myBook.categories.map(it => <p>{it}</p>)
Use Array.filter() and Array.find() with a RegExp to find categories that have matching keys. Use Array.map() to convert the categories to strings/JSX/etc...
const findMatchingCategories = (obj, categories) => {
const keys = Object.keys(obj);
return allCategories
.filter(category => {
const pattern = new RegExp(category, 'i');
return obj[keys.find(c => pattern.test(c))];
})
.map(category => `<p>${category}</p>`);
};
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {
isItScienceFiction: true,
isItManga: false,
isItForKids: false
};
const result = findMatchingCategories(myBook, allCategories);
console.log(result);
You can modify the key names in myBook object for easy lookup like:
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {
isItScienceFiction: true,
isItManga: false,
isItForKids: false
}
const modBook = {}
Object.keys(myBook).map((key) => {
const modKey = key.slice(4).toLowerCase()
modBook[modKey] = myBook[key]
})
const haveCategories = allCategories.map((category) => {
if (modBook[category]) {
return <p>{category}</p>
}
return null
})
console.log(haveCategories)
Converting sciencefiction to isItScienceFiction is not possible and looping all the keys of myBook for every category is not optimal.
But converting isItScienceFiction to sciencefiction is pretty easy, so you can create newMyBook from yourmyBook and use it instead to check.
Creating newMyBook is a one time overhead.
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {isItScienceFiction: true,isItManga: false,isItForKids: false};
const newMyBook = Object.keys(myBook).reduce((a, k) => {
return { ...a, [k.replace('isIt', '').toLowerCase()]: myBook[k] };
}, {});
console.log(
allCategories.filter(category => !!newMyBook[category]).map(category => `<p>${category}</p>`)
);
You can try like this:
const allCategories = ["sciencefiction", "manga", "school", "art"];
const myBook = {
isItScienceFiction: true,
isItManga: false,
isItForKids: false
};
const myBookKeys = Object.keys(myBook);
const result = allCategories.map(category => {
const foundIndex = myBookKeys.findIndex(y => y.toLowerCase().includes(category.toLowerCase()));
if (foundIndex > -1 && myBook[myBookKeys[foundIndex]])
return `<p>${category}</p>`;
});
console.log(result);
You could create a Map for the the categories and keys of object:
const allCategories = ["sciencefiction", "manga", "school", "art"],
myBook = { isItScienceFiction:true, isItManga:false, isItForKids:false }
const map = Object.keys(myBook)
.reduce((r, k) => r.set(k.slice(4).toLowerCase(), k), new Map);
/* map:
{"sciencefiction" => "isItScienceFiction"}
{"manga" => "isItManga"}
{"forkids" => "isItForKids"}
*/
allCategories.forEach(key => {
let keyInObject = map.get(key); // the key name in object
let value = myBook[keyInObject]; // value for the key in object
console.log(key, keyInObject, value)
if(keyInObject && value) {
// do something if has the current key and the value is true
}
})

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