I have a data set of exercises that can be done when working out, in JSON format. I'm implementing a filtering system on this data, which has three different types of filters users can apply: sort filter, (which takes the current set of exercises and orders them by name a-z or vice versa), muscle group (exercises that fall under the same targeted muscle group) and training program (exercises that fall under the same training program).
Users should be able to select any of the filters and it will apply it straight away, but not all 3 types of filters have to be applied. Therefore, I have come up with the following function in JS:
function applyFilters(filters) {
const sort = filters[0];
const muscles = filters[1];
const programs = filters[2];
const exerciseJSONData = "exercises.json";
$.getJSON(exerciseJSONData, {
format: JSON,
}).done(function (result) {
let filteredArr = [];
if (muscles.length === 0 && programs.length === 0) {
filteredArr = result;
} else if (muscles.length !== 0 && programs.length === 0) {
$.each($(result), function (key, val) {
if (muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0)) {
filteredArr.push(this);
}
});
} else if (muscles.length === 0 && programs.length !== 0) {
$.each($(result), function (key, val) {
if (programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)) {
filteredArr.push(this);
}
});
} else {
$.each($(result), function (key, val) {
if (
muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0) &&
programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)
) {
filteredArr.push(this);
}
});
}
$("#number-of-exercises").text(filteredArr.length + " Exercises Found");
$("#exercises").empty();
createExerciseHTML(filteredArr);
});
}
This function works as intended, however, I think the readability of the if else statement could be hard to understand for others and breaks the rule of not repeating code. I am wondering if there is a more concise and efficient way of coding this function? Thanks in advance.
P.S. Sorry if there was too much info at the beginning of this post, I wanted to give some context to avoid any confusion readers may have.
You can start by taking the $each out and keep the if in the each.
function applyFilters(filters) {
const sort = filters[0];
const muscles = filters[1];
const programs = filters[2];
const exerciseJSONData = "exercises.json";
$.getJSON(exerciseJSONData, {
format: JSON,
}).done(function (result) {
let filteredArr = [];
var hasMuschle = muscles.length > 0 ? 1 : 0;
var hasProgram = programs.length > 0 ? 1 : 0;
var muscleAndProgram = hasMuschle - hasProgram;
$.each($(result), function (key, val) {
if (muscles.length === 0 && programs.length === 0) {
filteredArr = result;
} else if (muscles.length !== 0 && programs.length === 0) {
if (muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0)) {
filteredArr.push(this);
}
} else if (muscles.length === 0 && programs.length !== 0) {
if (programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)) {
filteredArr.push(this);
}
} else {
if (
muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0) &&
programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)
) {
filteredArr.push(this);
}
}
})
$("#number-of-exercises").text(filteredArr.length + " Exercises Found");
$("#exercises").empty();
createExerciseHTML(filteredArr);
});
}
Related
I have a problem with the Vue vuelidate. Every time the boxes in the picture are pressed, an object is sent to an array. All boxes can be selected.
The condition is that a maximum of 3 boxes must be selected, if more than 3 are selected, I don't want to send the form.
Below is the code that runs when any box is pressed.
valueSelected(value,index) {
// console.log(index)
// console.log(value)
const i = this.mySelectedValue.indexOf(value)
// console.log('const i',i)
if (i === -1) {
this.mySelectedValue.push(value)
} else {
this.mySelectedValue.splice(i, 1)
}
const findIndex = this.user.positions.findIndex(v => {
return v.position === value.position
})
if (findIndex === -1) {
this.user.positions.push(value)
} else {
this.user.positions.splice(findIndex, 1)
}
},
Considering this is the whole function you call. You can just put an if around it so if 3 options are already selected then it will not trigger. I think this is the easy way out.
valueSelected(value,index) {
// console.log(index)
// console.log(value)
const i = this.mySelectedValue.indexOf(value)
// console.log('const i',i)
if (i === -1) {
if(this.mySelectedValue.length < 3){
this.mySelectedValue.push(value)
}
} else {
this.mySelectedValue.splice(i, 1)
}
const findIndex = this.user.positions.findIndex(v => {
return v.position === value.position
})
if (findIndex === -1) {
if(this.user.positions.length < 3){
this.user.positions.push(value)
}
} else {
this.user.positions.splice(findIndex, 1)
}
},
I have lots of conditions and if I wrote it with if .. else it works fine but may be hard to read for others (especially if it will grow in future). Is there any better way how to rewrite it in more readable way?
My code:
func(el: IHeadlines): boolean => {
if (el.type === 'Cars' && el.label) { return true; }
if (el.type === 'Bikes' && el.storage) {
if (el.storage.filter(el => el.id === 1).length >= 1) { return true; }
if (el.storage.filter(el => el.id === 2).length > 1) { return true; }
} else return false;
}
interface IHeadlines {
type: string;
label: string;
storage: [{id: number; name: string}]
}
If you ask me, I create a function that counts the value and the code will be much more readable.
function count(arr, tar) {
cnt = 0;
for(let val of arr) {
if(val === tar)
cnt += 1;
}
return cnt;
}
You can write your function this way, this is way more readable to me:
const func = (el) => {
if (el.type === 'Cars' && el.label)
return true;
if (el.type === 'Bikes' && el.storage)
if(count(el.storage,1) >= 1 || count(el.storage,2) > 1)
return true;
return false;
}
Or this way:
const func = (el) => {
if (el.type === 'Cars' && el.label)
return true;
if ((el.type === 'Bikes' && el.storage) &&
(count(el.storage,1) >= 1 || count(el.storage,2) > 1))
return true;
return false;
For this is much more readable, however you can change the count function to any other way you like, and I would prefer using this code even if it is longer but it is much more readable.
Have you tried using switch-case?
for example:
function(el)=>{
switch(el.type):
case 'Cars':
return true;
break;
case 'Bikes':
return true;
break;
default:
return false;
}
After this, you can perhaps put if-else before 'return' in each case.
hello, i do some optimize. I wish this can help you.
const oldFunc = (el) => {
if (el.type === 'News' && el.label) {
return true;
}
if (el.type === 'Research' && el.storage) {
if (el.storage.filter(el => el.id === 1).length >= 1) {
return true;
}
if (el.storage.filter(el => el.id === 2).length > 1) {
return true;
}
} else return false;
}
// do some optimize
const newFunc = (el) => {
let flag = false;// default return false
flag = ((el.type === 'News' && el.label) ||
((el.type === 'Research' && el.hasOwnProperty('storage')) ? el.storage.some(o=>[1,2].includes(o.id)): false)) && true;
return flag;
}
// test code
const testData = {
type: 'News',
label: 'test'
};
console.log(oldFunc(testData));
console.log(newFunc(testData));
const testData2 = {
type: 'Research',
storage: [
{
id: 1,
name: "John"
}
]
};
console.log(oldFunc(testData2));
console.log(newFunc(testData2));
// test result
// true
// true
// true
// true
Two ways come to my mind, but none of them will make your code very clear because conditions are dirty stuff.
If you return boolean, you don't need if..else blocks. Just return the conditions.
func(el) => {
return (el.type === 'Cars' && el.label) ||
(el.type === 'Bikes' &&
(el.storage?.filter(el => el.id == 1).length >= 1 ||
el.storage?.filter(el => el.id == 2).length > 1)
)
}
You can extract the group of conditions to separate functions and call them in the main function.
const checkForCars = (el) => { return el.type === 'Cars' && el.label }
const checkForBikes = (el) => { return // your conditions}
const mainFunction (el) {
return checkForCars(el) || checkForBikes(el);
}
I generally try to be as descriptive as possible. Rename func to what the function does.
For readability, you could also create a function in el called, isCar(), isBike(), hasStorage() etc etc which would encapsulate that logic. I'm not sure if that makes sense based on what you provided. You are also inline hard coding ids. It would make it clearer if el contained some const / var or something in your app had them, which described the id. You could also rename el to something descriptive. You can remove some if by doing what Guerric P said.
Even the filter functions could be moved if they were going to be reused..
const shedFilter = (el) => ...(function code here)
Then provide some comments if anything is not clear.
const STORAGE_SHED = 1;
const SOTRAGE_GARAGE = 2;
aGoodName(el) => {
if (el.isCar()) { return true; }
if (el.isBike()) {
if (el.storage.filter(el => el.id === STORAGE_SHED ).length >= 1) { return true; }
if (el.storage.filter(el => el.id === SOTRAGE_GARAGE).length > 1) { return true; }
}
else { return false };
}
Step 1: To have exactly same logic as you had - you can start with extracting the conditions/function and trying to avoid return true and return false statements. You can easily return condition itself.
const isNews = el.type === 'News' && el.label;
const isBikes = el.type === 'Bikes' && el.storage;
const storageItemsCount = (el, id) => el.storage.filter(el.id === id).length;
return isNews
|| (isBikes && (storageItemsCount(el, 1) >= 1 || storageItemsCount(el, 2) > 1)
Step 2: further I would remove "magic" id 1 and 2 values and explicitly specify what they are about, like
const BIKE1_ID = 1;
const BIKE2_ID = 2;
no we can generalise the counts check with specifying
const minBikeCounts = {
[BIKE1_ID]: 1,
[BIKE1_ID]: 2
}
const bikeCountsAreValid = el => {
return Object.entries(k)
.every(
([id, minCount]) => el.storage.filter(el => el.id === key).length >= minCount)
}
so the main flow simplified to
const isNews = el.type === 'News' && el.label;
const isBikes = el.type === 'Bikes' && el.storage;
return isNews || (isBikes && bikeCountsAreValid(el))
Step 3: We can see a pattern of "Check if type is supported with some extra check". We could extract the knowledge of supported types to separate structure. Now if we want to add new supported type we don't need to remember all if/else statements across the codebase, and just add new one here:
const supportedTypes = {
News: el => !!el.label,
Bikes: el => !!el.storage && bikeCountsAreValid(el)
}
no if statements at all in our main function:
const isSuppotedType =
(el: IHeadlines) => supportedTypes[el.type] && supportedTypes[el.type](el)
Good clean code practices suggest that you have small functions and have a self-describing code.
In your code, I would make each Boolean logic become a variable or a function.
Example 1
func(element: IHeadlines): boolean => {
const isCarWithLabel = element.type === 'Cars' && element.label;
if(isCarWithLabel){
return true;
}
const isBikeWithStorage = element.type === 'Bikes' && element.storage;
if(isBikeWithStorage){
// rest of your logic that I don't know...
}
return false;
}
Example 2
func(element: IHeadlines): boolean => {
return this.isCarWithLabel() || this.isBikeWithStorageAndSomethingElse();
}
I'll suggest you something like this:
func(el: IHeadlines): boolean {
return el.type === 'Cars' && !!el.label ||
el.type === 'Bikes' && (
!!el.storage?.find(el => el.id == 1) || el.storage?.filter(el => el.id == 2).length > 1
);
}
I am trying to make a filter based on checkboxes.
The thing is js ignoring other conditions inside filter when one is active
filterData() {
return this.airlines.filter(x => {
if (this.filters.options.length != 0 || this.filters.airlines.length != 0) {
for (let i = 0; this.filters.options.length > i; i++) {
if (this.filters.options[i] == 0) {
return x.itineraries[0][0].stops == 0;
}
if (this.filters.options[i] == 1) {
return x.itineraries[0][0].segments[0].baggage_options[0].value > 0;
}
}
} else {
return x;
}
})
}
I know that return will stop the current loop, but is there any way to do it correctly?
Update-1: (When to filter record for every case checked OR case)
Replace for loop and all conditions in a single return by && for if and || condition for data:
var chbox = this.filters.options;
return $.inArray(0, chbox) != -1 && x.itineraries[0][0].stops == 0
|| $.inArray(1, chbox) != -1 && x.itineraries[0][0].segments[0].baggage_options[0].value > 0;
Hope this helps !!
$.inArray(value, arr) method will check for each checkboxes and will work for every checked ones .
Update-2 (When to filter record for every case checked AND case)
As per comment below, you are trying to use checkbox on demand so use below code:
var chbox = this.filters.options;
boolean condition = true;
if ($.inArray(0, chbox) != -1) {
conditon = conditon && x.itineraries[0][0].stops == 0;
}
if ($.inArray(1, chbox) != -1) {
conditon = conditon && x.itineraries[0][0].segments[0].baggage_options[0].value > 0;
}
return condition;
Your filter function is returning an object, which ideally should be a boolean value. Please refactor the code as below.
filterData() {
return this.airlines.filter(x => {
let result = false;
if (this.filters.options.length != 0 || this.filters.airlines.length != 0) {
for (let i = 0; this.filters.options.length > i; i++) {
if (this.filters.options[i] == 0) {
result = x.itineraries[0][0].stops == 0;
break;
} else if (this.filters.options[i] == 1) {
result = x.itineraries[0][0].segments[0].baggage_options[0].value > 0;
break;
}
}
}
return result;
})
}
I haven't found a way to change for into a map, so my plan won't use for but want to use maps in each function of this function
variantDefault = (colorId, sizeId) => {
let selected = [];
let i = 0;
if (this.props.index === 0) {
for (i = 0; i < this.props.values.length; i++) {
if (this.props.values[i].id === colorId) {
selected = this.props.values[i];
}
}
}
if (this.props.index === 1) {
for (i = 0; i < this.props.values.length; i++) {
if (this.props.values[i].id === sizeId) {
selected = this.props.values[i];
}
}
}
console.log(selected);
return selected;
};
you can try this way
variantDefault = (colorId, sizeId) => {
let selected = [];
if (this.props.index === 0) {
this.props.values.map((data) => {
if(data.id === colorId){
selected.push(data)
}
})
}
if (this.props.index === 1) {
this.props.values.map((data) => {
if (data.id === sizeId) {
selected.push(data)
}
})
}
console.log(selected);
return selected;
};
You could take Array#find and return the found item.
variantDefault = (colorId, sizeId) => this.props.values.find(
({ id }) => id === [colorId, sizeId][this.props.index]
) || [];
The use of Array.prototype.filter seems more fitting.
variantDefault = (colorId, sizeId) => {
if (this.props.index.isBetween(0, arguments.length, 1)) return [];
var compareTo = arguments[this.props.index];
return this.props.values.filter(item => item.id === compareTo);
};
If you only want to return the last element found (which is what you are doing in your example) then add an Array.prototype.pop call to the filtered array.
You can add the isBetween function by doing:
if (typeof Number.prototype.isBetween !== "function") {
Number.prototype.isBetween = function(start, end, inclusive) {
if (typeof start !== "number" || isNaN(start)) throw Error("Start is not a number");
if (typeof end !== "number" || isNaN(end)) throw Error("End is not a number");
return inclusive ? start <= this && this <= end : start < this && this < end;
};
}
I have a section of code where a variable contains a particular string (here it's multiply), and when I check if the variable has that particular string, the condition always equates to false. I cannot find what I'm missing here.
// calculations
$scope.$watch('colInput.' + el.key, function () {
angular.forEach($scope.colInput[el.key], function (item, index) {
angular.forEach($scope.column[el.key], function (item_1, index_1) {
if (item.hasOwnProperty(item_1.key)) {
item[item_1.key].type = item_1.type;
item[item_1.key].id = item_1.id;
item[item_1.key].options = item_1.options;
}
else {
item[item_1.key] = {};
item[item_1.key].type = item_1.type;
item[item_1.key].id = item_1.id;
item[item_1.key].options = item_1.options;
}
})
angular.forEach(item, function (elem, key) { //each column of the row
var operand_1, operator, operand_2;
if (elem.type == 10) {
// analyzing the formula
elem.options.forEach(function (el, index) {
if (isNaN(el) && index == 1) {
operator = el;
} else if (isNaN(el) && index == 0) {
operand_1 = el;
} else if (isNaN(el) && index == 2) {
operand_2 = el;
} else if (!isNaN(el)) {
operand_2 = parseFloat(el);
}
})
console.log(operator, eval(operator === "multiply"), typeof operator);
if (operator == 'multiply') {
console.log("IF---")
elem.value = parseFloat(item[operand_1].value) * operand_2;
}
}
})
})
}, true)
It looks like your operator is an HTML element not a String.
The comparison with multiply will always be false.