Matching data depending on chosen preferences - javascript

I am trying to build a workout app using Vue, where you can generate a workout depending on chosen preferences. An user can select some options and then click button to generate a workout. Collected data is an object of arrays and each array contains objects of chosen options (for example: allowed duration of a workout, difficulty, prefered exercises)
this.generatorData = {
types: [],
style: [],
muscleGroups: [],
difficulty: [
{ title: 'Beginner', ..some properties },
{ title: 'Intermediate', .. }
]
}
I also have a set of exercises that have the same properties as a generated object, but are predefined.
exercises: [
{
title: 'Name',
type: 'Weights',
style: ['Strength Building'],
muscleGroups: ['Back', 'Chest'],
difficulty: 'Intermediate'
},
{
title: 'Name',
type: 'Weights',
style: ['Strength Building'],
muscleGroups: ['Back', 'Chest'],
difficulty: 'Intermediate'
}
]
I would like to match exercises with data/preferences object. Here's a function but unfortunately I was only able to hardcode it and it doesn't work as expected. I need to compare data from this.generatorData with exercises - loop through all exercises and find these that match the requirements. Is there a way to make it work and if it is possible how do I automatise this function?
match() {
let categories = Object.values(this.generatorData)
for(let category of categories) {
if(category.length > 1) {
this.exercises.filter(exercise => {
if(exercise.type === category[0].name || exercise.type === category[1].name || exercise.type === category[2].name) {
if(exercise.difficulty === category[categories.length - 1].name) {
this.matchedExercies.push(exercise)
}
}
})
}
else if(category.length === 1) {
let filtered = this.exercises.filter(exercise => exercise.type === category[0].name)
console.log(filtered)
this.matchedExercies = filtered
}
}
}
Here's a codeSandbox.

so this is a matter of plain js, not so much vue.
Assuming the filtering is done using AND across filters and OR across filter choices, here is a working version (requiring some changes to the schema)
// the values are in an array, to use the `title` some changes may be needed
const generatorData = {
types: [],
style: [],
muscleGroups: [{name:'Back'}],
difficulty: [{name:'Beginner'},{name:'Intermediate'}]
}
const exercises = [
{
title: 'Name',
type: 'Weights',
style: ['Strength Building'],
muscleGroups: ['Toes', 'Chest'],
difficulty: 'Intermediate'
},
{
title: 'Name',
type: 'Weights',
style: ['Strength Building'],
muscleGroups: ['Back', 'Chest'],
difficulty: 'Intermediate'
},
{
title: 'Name',
type: 'Weights',
style: ['Strength Building'],
muscleGroups: ['Back', 'Chest'],
difficulty: 'Effin Hard'
}
]
// I loop over the categories first, removing any that are not needed
const categories = Object.keys(generatorData).map(k => {
// if you want to keep using `title`, this is a good place to do that (val from all titles)
if (generatorData[k].length > 0) return { key: k, val: generatorData[k].map(g => g.name) };;
return false
}).filter(i => i !== false);
let filtered = exercises.filter(e => {
// filter all categories, and compare length of matching filters with the number of filters (enforces AND rule)
return categories.filter(f => {
// if key is missing, assume true
if (e[f.key] === undefined) return true;
// loop through filter values and make sure at leas one matches (OR condition)
return f.val.filter(v => {
// handle string as direct match
if (typeof e === "string") return e[f.key] === v;
// handle array with `includes`
return e[f.key].includes(v)
}).length > 0
}).length === categories.length;
})
console.log(filtered)
update
Looking at the codesandbox, it looks like your store is actually providing the generatorData with name instead of title
instead of:
difficulty: [
{ title: 'Beginner', ..some properties },
{ title: 'Intermediate', .. }
]
it uses:
difficulty: [
{ name: 'Beginner'},
{ name: 'Intermediate'}
]
the code was updated to use array of objects with name

This can be better for complex selections.
matchPreferencesWithExercises() {
let categories = this.generatorData;
this.exercises.map(exercise => {
let error = 0;
let matched;
for (let categoryName in categories) {
if (exercise[categoryName]) {
if (typeof exercise[categoryName] === "string") {
!categories[categoryName]
.map(item => item.name)
.includes(exercise[categoryName]) && error++;
} else {
matched = 0;
exercise[categoryName].map(exerciseAttr => {
categories[categoryName].includes(exerciseAttr) && matched++;
});
}
}
}
(error === 0 || matched > 0) && this.matchedExercies.push(exercise);
});
}
https://codesandbox.io/s/qo74o6z9

Related

Filtered object search using allowed keys and also a search value

as seen in my title I am facing a problem that needs a solution. I am trying to filter my object, but not only based on keys. But also based on the value of the allowed keys.
This is what I got so far:
const handleSearch = (searchValue) => {
if(searchValue !== '') {
const allowed = ['name', 'title'];
let list = props.search.list;
const filtered = Object.keys(list)
.filter((key) => allowed.includes(key))
.reduce((obj, key) => {
obj[key] = list[key];
return obj;
}, {});
const filteredArray = Object.entries(filtered)
props.search.onChange(filteredArray)
} else {
props.search.onChange(props.search.list)
}
}
Structure of the list object:
0: {name: "John', title: 'Owner'}
1: {name: "Jane", title: 'Admin'}
Wanted results:
Filtered array that shows the keys filtered aswell as the search value.
And I don't know where I should integrate the filtering by the values aswell. This has been giving me a headache for the past few hours. And hope someone here is experienced with these kinds of issues/logic.
Thanks for reading.
Kind regards.
const handleSearch = (searchValue) => {
if (searchValue !== '') {
const allowed = ['name', 'title'];
const list = props.search.list;
const filtered = list
.filter(obj =>
Object.keys(obj)
.some(k => allowed.includes(k))
)
.filter(obj =>
Object.values(obj)
.map(v => v.toLocaleLowerCase())
.some(v => v.includes(searchValue.toLowerCase()))
)
props.search.onChange(filtered)
} else {
props.search.onChange(props.search.list)
}
}
Example
Let's assume props as:
const props = {
search: {
list: [
{ name: "John", title: 'Owner' },
{ name: "Jane", title: 'Admin' },
{ name: "Reza", title: 'Owner' }
],
onChange: x => console.log(x)
},
}
handleSearch("own")
// [ { name: 'John', title: 'Owner' }, { name: 'Reza', title: 'Owner' } ]
handleSearch("jane")
// [ { name: 'Jane', title: 'Admin' } ]
handleSearch("something")
// []

Retain array structure when filtering nested array

My brain froze with this advanced filtering. This task has exceeded my basic knowledge of filter, map etc.
Here I have an array with nested objects with array:
const DATA = [
{
title: 'Spongebob',
data: [
{ id: 1, name: 'Mr Crabs' },
{ id: 2, name: 'Sandy' }
]
},
{
title: 'Dragon Balls Z',
data: [
{ id: 1, name: 'GoKu' },
{ id: 2, name: 'Zamasu' }
]
}
];
You may have seen this sort of style if you've worked with React Native (RN). This question is not for RN. I need to perform a filter on the name property in the nested array and when I get a match, I must return the format as the DATA variable.
const handleFiltering = (value) => {
const _value = value.toLowerCase();
const results = DATA.map(o => {
return o.data.filter(o => o.name.toLowerCase().indexOf(_value) != -1)
});
console.log(results);
};
My limited knowledge of deep filtering returns the basic filtering for the data array but need to retain the structure for DATA. The expected results I'd expect:
// I'm now querying for "ZAMASU"
const handleFiltering = (value='ZAMA') => {
const _value = value.toLowerCase();
const results = DATA.map(o => {
return o.data.filter(o => o.name.toLowerCase().indexOf(_value) != -1)
});
// console.log(results) should now be
// [
// {
// title: 'Dragon Balls Z',
// data: [
// { id: 2, name: 'Zamasu' }
// ]
// }
// ];
};
What comes to mind is the use of {...DATA, something-here } but my brain has frozen as I need to get back the title property. How to achieve this, please?
Another solution would be first use filter to find only objects containing the name in data passed through the argument, subsequently mapping data.
Here is your adjusted filter method
const handleFiltering = (value) => {
const _value = value.toLowerCase();
const results = DATA.filter((obj) =>
obj.data.some((character) => character.name.toLowerCase() === _value)
).map((obj) => ({
title: obj.title,
data: obj.data.filter(
(character) => character.name.toLowerCase() === _value
),
}));
console.log(results);
};
You can use reduce method of array. First find out the object inside data array and then add that to accumulator array as new entry by preserving the original structure.
const DATA = [
{
title: 'Spongebob',
data: [
{ id: 1, name: 'Mr Crabs', where: 'tv' },
{ id: 2, name: 'Sandy' }
]
},
{
title: 'Dragon Balls Z',
data: [
{ id: 1, name: 'GoKu' },
{ id: 2, name: 'Zamasu' }
]
}
];
let handleFiltering = (value='tv') => {
return DATA.reduce((acc,d) => {
let obj = d.data.find(a => a.name?.toLowerCase().includes(value.toLowerCase())
|| a.where?.toLowerCase().includes(value.toLowerCase()));
obj ? acc.push({...d, data:[obj]}) : null;
return acc;
}, []);
}
let result = handleFiltering();
console.log(result);

Change deeply nested property on object, but with arrays mixed in

I'm trying to change the title and description properties on an individual task without mutating the state. The nesting is complex
dayColumns: [
{
id: 'monday',
tasks: [{
key: cellTime,
dayOfWeek: columnDay,
date: date,
timeStart: cellTime,
timeEnd: cellTime + blockInterval * 60000,
initalBlockSize: blockInterval,
title: '',
description: '',
},
{
key: cellTime,
dayOfWeek: columnDay,
date: date,
timeStart: cellTime,
timeEnd: cellTime + blockInterval * 60000,
initalBlockSize: blockInterval,
title: '',
description: '',
}],
},
{
id: 'tuesday',
tasks: [],
},
{
id: 'wednesday',
tasks: [],
},
{
id: 'thursday',
tasks: [],
},
{
id: 'friday',
tasks: [],
},
{
id: 'saturday',
tasks: [],
},
{
id: 'sunday',
tasks: [],
},
];
This is the function I have so far. I have adjusted this many times and I think my errors are with the returns.
case SET_TASK_TEXT:
const { textType, newText, key, dayOfWeek } = action.payload;
const newTextDayColumns = state.dayColumns.map((column) => {
if (column.id === dayOfWeek) {
const newTasks = column.tasks.map(
(task) => {
if (task.key === key && textType === 'title') {
return { ...task, title: newText };
}
if (task.key === key && textType === 'description') {
return { ...task, description: newText };
}
}
/** Change title or description according to #param textType */
);
return [...column.tasks, newTasks];
}
return {...state.dayColumns, newTextDayColumns}
});
return {
...state,
dayColumns: newTextDayColumns,
};
your last {...state.dayColumns, newTextDayColumns} is wrong, you should only return the same column or a new one with updated tasks. Also, you should not return [...column.tasks, newTasks] from your map.
const newTextDayColumns = state.dayColumns.map((column) => {
// return column if no match
if (column.id !== dayOfWeek) return column;
const newTasks = column.tasks.map(
(task) => {
if (task.key === key && textType === 'title') {
return { ...task, title: newText };
}
if (task.key === key && textType === 'description') {
return { ...task, description: newText };
}
// it's good to return task if conditionals above fail
return task;
}
);
// return new column with updated task
return {...column, tasks: newTasks};
});
Using the spread operator almost always mutates your current state since while using it with an array, you are taking all its individual elements and creating a new array to place then into.
Same goes for objects.
Although this can be done in a slightly better way it would still result in such enormous, confusing and hard to maintain code.
Immutability Helper package solves this exact problem by simplifying the way you update states.

Loop over array of objects, remove duplicates, take one out and assign boolean to original

I have the following sample arr:
const fetchedArr = [
{ id: "3cc74658-a984-4227-98b0-8c28daf7d3d4", type: a },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: a },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: b }
]
i need the following output :
const arr = [
{ id: "3cc74658-a984-4227-98b0-8c28daf7d3d4", type: a, checked: true },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: a, checked: true, hasPair: true }
]
I have the following snippet which works
const newLegendItems = fetchedArr
.reduce((acc, curr, idx, arr) => {
const singleComponentLines = arr.filter((g) => g.id === curr.id);
const exists = !!acc.find((x) => x.id === curr.id);
if (!exists) {
if (singleComponentLines.length === 2 && singleComponentLines.includes(curr)) {
acc[idx] = {...curr, hasPair: true};
} else {
acc[idx] = curr;
}
}
return acc;
}, [])
.map((l) => ({ ...l, checked: true }));
, but i was thinking if there's simpler way to achieve this?
I should clarify that in the fetchedArr, the type does not matter, and that there won't be more than two same Id's, hence my idea for singleComponentLines.length === 2.
Like this?
const fetchedArr = [
{ id: "3cc74658-a984-4227-98b0-8c28daf7d3d4", type: "a" },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: "a" },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: "b" }
];
let result = fetchedArr.reduce((acc,v) => {
//first i need to check if i already have an element with the same ID in my accumulator. i either get -1 for not found or the index where the element is.
let i = acc.findIndex(el => el.id === v.id);
if(i !== -1) {
//if there is an element then access the element in the array with a[i] and add a new property to the object with ["hasPair"] and set it to true
acc[i]["hasPair"] = true;
return acc;
}
//in case i = -1 what means not found
return [...acc, {...v, checked: true}];
},[])
console.log(result);
I don't fully understand your question but it should help:
const result = [{
id: "3cc74658-a984-4227-98b0-8c28daf7d3d4",
type: 'a'
},
{
id: "9b96e055-dc2a-418c-9f96-ef449e34db60",
type: 'a'
},
{
id: "9b96e055-dc2a-418c-9f96-ef449e34db60",
type: 'b'
}
].reduce((acc, el) => {
const idx = acc.findIndex(it => it.id === el.id);
if (idx > -1) {
acc[idx] = { ...acc[idx],
hasPair: true
}
} else {
acc.push({ ...el,
checked: true
});
}
return acc;
}, []);
console.log(result)
I rather use a Map for this kind of things since it brings more readability IMO.
Start by checking if we already have it
Update our component and add it to the Map
The only "tricky" thing is that we need to iterate over .values() to grab our updated components, but thanks to spread operator it's quite easy.
const components = [
{ id: "3cc74658-a984-4227-98b0-8c28daf7d3d4", type: 'a' },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: 'a' },
{ id: "9b96e055-dc2a-418c-9f96-ef449e34db60", type: 'b' },
];
const newLegendItems = components
.reduce((acc, component) => {
if (acc.has(component.id)) {
acc.get(component.id)['hasPair'] = true;
} else {
acc.set(component.id, { ...component, checked: true });
}
return acc;
}, new Map());
console.log([...newLegendItems.values()]);

not using find twice when retrieving deeply nested object in javascript

I am able to find the object in the javascript below but it is pretty horrible and I am doing the find twice.
I'd be interested in a better way and one that did not involve lodash which I am current not using.
const statuses = [{
items: [{
id: 1,
name: 'foo'
}, {
id: 5,
name: 'bar'
}]
}, {
items: [{
id: 1,
name: 'mook'
}, {
id: 2,
name: 'none'
}, {
id: 3,
name: 'soot'
}]
}]
const selected = statuses.find(status => {
const none = status.items.find(alert => {
return alert.name === 'none';
});
return !!none;
});
console.log(selected)
const a = selected.items.find(s => s.name === 'none');
console.log(a)
You could use a nested some and find like this. This way you can skip the the operation once a match is found.
const statuses=[{items:[{id:1,name:'foo'},{id:5,name:'bar'}]},{items:[{id:1,name:'mook'},{id:2,name:'none'},{id:3,name:'soot'}]}];
let found;
statuses.some(a => {
const s = a.items.find(i => i.name === "none");
if (s) {
found = s;
return true
} else {
return false
}
})
console.log(found)
You could do something like this using map, concat and find. This is a bit slower but looks neater.
const statuses=[{items:[{id:1,name:'foo'},{id:5,name:'bar'}]},{items:[{id:1,name:'mook'},{id:2,name:'none'},{id:3,name:'soot'}]}]
const found = [].concat(...statuses.map(a => a.items))
.find(a => a.name === "none")
console.log(found)
Here's a jsperf comparing it with your code. The first one is the fastest.
You could combine all the status items into one array with reduce() and then you only have to find() once:
const statuses = [{
items: [{
id: 1,
name: 'foo'
}, {
id: 5,
name: 'bar'
}]
}, {
items: [{
id: 1,
name: 'mook'
}, {
id: 2,
name: 'none'
}, {
id: 3,
name: 'soot'
}]
}]
const theNones = statuses
.reduce(function(s, t) { return s.items.concat(t.items); })
.find(function(i) { return i.name === 'none'; });
console.log(theNones);
You could use flatMap and find:
Note: Array.prototype.flatMap is in Stage 3 and not part of the language yet but ships in most environments today (including Babel).
var arr = [{items:[{id:1,name:'foo'},{id:5,name:'bar'}]},{items:[{id:1,name:'mook'},{id:2,name:'none'},{id:3,name:'soot'}]}]
console.log(
arr
.flatMap(({ items }) => items)
.find(({
name
}) => name === 'none')
)
We could polyfill the flatting with concating (via map + reduce):
var arr = [{items:[{id:1,name:'foo'},{id:5,name:'bar'}]},{items:[{id:1,name:'mook'},{id:2,name:'none'},{id:3,name:'soot'}]}];
console.log(
arr
.map(({ items }) => items)
.reduce((items1, items2) => items1.concat(items2))
.find(({
name
}) => name === 'none')
)
You could also reduce the result:
var arr = [{items:[{id:1,name:'foo'},{id:5,name:'bar'}]},{items:[{id:1,name:'mook'},{id:2,name:'none'},{id:3,name:'soot'}]}]
console.log(
arr.reduce((result, {
items
}) =>
result ? result : items.find(({
name
}) => name === 'none'), null)
)
One reduce function (one iteration over everything), that returns an array of any object matching the criteria:
const [firstOne, ...others] = statuses.reduce((found, group) => found.concat(group.items.filter(item => item.name === 'none')), [])
I used destructuring to mimic your find idea, as you seem chiefly interested in the first one you come across. Because this iterates only once over each item, it is better than the alternative answers in terms of performance, but if you are really concerned for performance, then a for loop is your best bet, as the return will short-circuit the function and give you your value:
const findFirstMatchByName = (name) => {
for (let group of statuses) {
for (let item of group.items) {
if (item.name === name) {
return item
}
}
}
}
findFirstMatchByName('none')

Categories