trying to work with Minimals.cc and I have a problem with the search feature that include only the names and that I need to extends to other key of the objects I'm filtering (filterAll). Here is the code:
function applySortFilter({ tableData, comparator, filterName, filterCircle, filterAll }) {
const stabilizedThis = tableData.map((el, index) => [el, index]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
tableData = stabilizedThis.map((el) => el[0]);
// this is the one i'm trying to make that should includes both item.name and item.circle_name
if (filterAll) {
tableData = tableData.filter((item) => item.name.toLowerCase().indexOf(filterAll.toLowerCase()) !== -1);
}
if (filterName) {
tableData = tableData.filter((item) => item.name.toLowerCase().indexOf(filterName.toLowerCase()) !== -1);
}
if (filterCircle) {
tableData = tableData.filter((item) => item.circle_name.toLowerCase().indexOf(filterCircle.toLowerCase()) !== -1);
}
return tableData;
}
I tried playing with && and || in the filter method to add item.circle_name and item.name but it did not work (at least for the way I did it).
Thanks in advance.
After trying more the answer that would work is:
if (filterAll) {
tableData = tableData.filter((item) => (item.name.toLowerCase().indexOf(filterAll.toLowerCase()) && item.circle_name.toLowerCase().indexOf(filterAll.toLowerCase())) !== -1);
}
Related
I use the following method in order to sort data (array) in a reusable Material datatable:
sortData(sortParameters: Sort) {
const keyName = sortParameters.active;
if (sortParameters.direction === 'asc') {
this.tableData = this.tableData.sort((a, b) => a[keyName].localeCompare(b[keyName]));
}
else if (sortParameters.direction === 'desc') {
this.tableData = this.tableData.sort((a, b) => b[keyName].localeCompare(a[keyName]));
}
else {
this.getStudents();
}
}
However, althought it works for name field, it does not work for id field. Another issue regarding to this method is that I want to ignore whitespaces of the name and want to use trim() method for the sorted values. It is working, but I think I have to find another solution that works for id columns also. Is there any workaround for that?
Easiest way would be to use d3's ascending() and descending().
import {ascending, descending} from 'd3-array';
sortData(sortParameters: Sort) {
const keyName = sortParameters.active;
if (sortParameters.direction === 'asc') {
this.tableData = this.tableData.sort((a, b) =>
a_prop = a[keyName];
b_prop = b[keyName];
try {
a_prop = a_prop.trim();
b_prop = b_prop.trim();
} catch (error) {
//do nothing, must not have been a string
}
ascending(a_prop, b_prop);
}
else if (sortParameters.direction === 'desc') {
descending(a_prop,b_prop);
}
else {
this.getStudents();
}
}
Alternative to importing D3.js, paste the code for the functions directly in your code since they are short. Taken from
https://github.com/d3/d3-array/blob/master/src/ascending.js
ascending (a, b) {
return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}
descending (a, b) {
return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN;
}
localeCompare is a function of Strings, but if your other columns, like id, are not strings then you can't use it to sort them. You need separate sort functions for each type then. Here is one way to do this:
const defaultSort = (a, b) => a < b;
const sortFunctions = {
id: (a, b) => a < b,
name: (a, b) => a.localeCompare(b),
// ....
};
function sortData(sortParameters: Sort) {
const keyName = sortParameters.active;
const sortFunction = sortFunctions[keyName] || defaultSort;
if (sortParameters.direction === 'asc') {
this.tableData = this.tableData.sort((a, b) => sortFunction(a, b));
}
else if (sortParameters.direction === 'desc') {
this.tableData = this.tableData.sort((a, b) => sortFunction(b, a));
} else {
this.getStudents();
}
}
I have an array which contains books data. I have to loop in array and make an service call to fetch details of each book and each book data has id's of attachments associated to book and make a service calls to fetch associated attachments for each book.
Here issue is promise.all not waiting for aAttachmentPromises to get resolved
function ExportbooksData(books) {
return new Promise((resolve, reject) => {
if (books && books.length > 0) {
let aPromises = [];
for (let i = 0; i < books.length; i++) {
const id = books[i].id;
const name = books[i].name;
aPromises.push(this.getBooksData(name, id, null).then(results => {
let aAttachmentPromises = [];
Object.entries(results).forEach(([key, value]) => {
let fieldName = key;
if (value.constructor === Array && value.length > 0) {
aAttachmentPromises.push(this.getAttachments(fieldName).then(fileContent => {
}))
}
});
}));
}
// Resolve when all are done!
Promise.all(aPromises)
.then(results => resolve(results))
.catch(error => reject(error));
}
})
}
I refactored this live in the BrisJS meetup in Brisbane, Australia tonight. Here is the video where I do it and explain the changes: https://youtu.be/rLzljZmdBNM?t=3075.
Here is a repo with both your version and the refactor, with mocked services: GitHub repo
function getAttachmentsForBookWithMetadataArray(bookdata) {
return Object.entries(bookdata)
.filter(([_, value]) => value.constructor === Array && value.length > 0)
.map(([fieldname, _]) => getAttachments(fieldname));
}
function getAttachmentsForBook(book) {
return getBookData(book).then(getAttachmentsForBookWithMetadataArray);
}
function ExportbooksData(books) {
return !books || !books.length > 0
? Promise.reject(new Error("Did not get an array with 1 or more elements"))
: Promise.all(books.map(getAttachmentsForBook));
}
For a discussion on dealing with failures, see this article: Handling Failure and Success in an Array of Asynchronous Tasks
You build up aAttachmentPromises but you don't return it, so your aPromises.push always pushes a Promise that resolves immediately with undefined into the array that you're waiting on. Change your code like this:
aPromises.push(
this.getBooksData(name, id, null).then(results => {
let aAttachmentPromises = [];
Object.entries(results).forEach(([key, value]) => {
let fieldName = key;
if (value.constructor === Array && value.length > 0) {
aAttachmentPromises.push(this.getAttachments(fieldName)
.then(fileContent => {})
.catch(err => {
if (err == "minor") console.log("Ignoring error",err);
else throw err;
})
);
}
});
return Promise.all(aAttachmentPromises); // <--- here!
})
);
But in addition to that you can simplify the function also. You don't need to wrap everything in a new Promise object, and using a variable to hold a value that's used only once is not helpful. This simplified version is (imho) easier to read/maintain:
function ExportbooksData(books) {
let aPromises = [];
for (let i = 0; books && i < books.length; i++) {
aPromises.push(
this.getBooksData(books[i].name, books[i].id, null).then(results => {
let aAttachmentPromises = [];
Object.entries(results).forEach(([key, value]) => {
if (value.constructor === Array && value.length > 0) {
aAttachmentPromises.push(this.getAttachments(key).then(fileContent => {}));
}
});
return Promise.all(aAttachmentPromises);
})
);
}
return Promise.all(aPromises);
}
I'm embarrassed to post this but I could really use a hand here. This code just looks nasty. I have a feeling that I can write a cleaner approach with filter or reduce but can't seem to stab it. Any thoughts, community?
const vin = detections.map(detection => {
return detection.textAnnotations.map(v => {
let n = v.description.replace(/\s+/g, '');
if (n.length === 17) {
return n;
}
});
})[0][0];
Thanks!
Just an attempt to refactor your code:
const getMatchedTextAnnotation = (textAnnotation) => {
const n = textAnnotation.description.replace(/\s+/g, '');
return n.length === 17 ? n : null;
}
const extractAnnotationsFromDetection = (detection) => {
return detection.textAnnotations.reduce((arr, textAnnotation) => {
const n = getMatchedTextAnnotation(textAnnotation);
return !!n ? [ ...arr, n] : arr;
}, [])
}
const vinArr = detections.reduce((arr, detection) => {
const subArr = extractAnnotationsFromDetection(detection);
return !!subArr ? [ ...arr, ...subArr ] : arr;
}, [])
const vin = !!vinArr ? vinArr[0] : null;
Im using React but I think this is just a vanilla JavaScript question. In my data model I have an array of objects. This is a shortened version:
this.props.exercises = [
{
name: 'Push up',
equipment: 'none',
group: 'push'
},
{
name: 'Bench press',
equipment: 'Barbell',
group: 'push'
},
{
name: 'Pull-Up Bar',
equipment: 'Pull-Up Bar',
group: 'pull'
},
{
name: 'Dumbbell / Kettlebell Squat',
equipment: ['Dumbbell', 'Kettlebell'],
group: 'legs'
}
]
I need to do some quite complex filtering on this array. If the item's name matches the chosenExercise string it always needs to be returned. If this condition is not matched then I need the following conditions:
If the user has chosen a group then only exercises from this group should be returned.
If the user has entered search text then the results should also be filtered by this.
The user specifies what equipment they have and then the results should be filtered by this too. Exercises can require no equipment, 1 piece of equipment, or have an array of possible equipment options.
My code below works but I think its quite fragile and will be a nightmare to debug if something goes wrong. What would be a better approach? Can functional programming come to the rescue?
renderExercises() {
const {selectedType} = this.state;
const allExercises = this.props.exercises;
// If there is search text then create a var for it
let searchText = false;
if (this.searchText && this.searchText.value.length > 0) {
searchText = this.searchText.value.toLowerCase();
}
return (
allExercises
// Set the active exercise to have an active property
.map((item) => {
if (this.props.chosenExercise === item.name) {
item.active = true;
} else {
item.active = false;
}
return item;
})
.filter((item) => {
// If the exercise is active then return true
if (item.active === true) {
return true
}
// Filter by the exercise group if one is selected
if (item.group !== selectedType && selectedType !== 'all') {
return false;
}
// If there is search text then filter out non matches
if (searchText && !item.name.toLowerCase().includes(searchText)) {
return false;
}
// EQUIPMENT CONDITIONS
// If the exercise doesn't need any equipment then return true
if (item.equipment === 'none') {
return true;
}
// If the user has all equipment then return true
if (this.props.equipmentSelectionState === 'all') {
return true;
}
// If the item only has 1 piece of equipment then the type will be a string not an array
if (typeof item.equipment == 'string' || item.equipment instanceof String) {
// If the selected equipment array contains the items's required equipment then return true
if (this.props.equipmentSelection.includes(item.equipment)) {
return true;
} else {
return false;
}
} else {
let test = false;
item.equipment.forEach((itemEquipment => {
if (this.props.equipmentSelection.includes(itemEquipment)) {
test = true;
}
}));
if (test === true) {
return true;
}
}
return false;
})
// Sort by name
.sort((a, b) => {
if (a.name < b.name)
return -1;
if (a.name > b.name)
return 1;
return 0;
})
.map((item) => {
return (
<ChooseAnExercises
key={item.name}
name={item.name}
active={item.active}
setNumber={this.props.number}
updateValue={this.props.updateValue}
/>
)
})
)
}
You could shrink the filter part by using only true conditions and sort them by taking general conditions first and then come to the more kind of expensive operations, like iterating arrays.
.filter(item =>
item.active ||
item.equipment === 'none' ||
this.props.equipmentSelectionState === 'all' ||
selectedType === 'all' ||
item.group === selectedType ||
searchText && item.name.toLowerCase().includes(searchText) ||
(Array.isArray(item.equipment)
? item.equipment
: [item.equipment]
).some(itemEquipment => this.props.equipmentSelection.includes(itemEquipment))
)
All of the condions are chained with logical OR. If necessary, a string is converted to an array for iterating with the same check.
You will have to filter multiple times. Try this simple approach.
Demo below
var exercises = [{
name: 'Push up',
equipment: 'none',
group: 'push'
},
{
name: 'Bench press',
equipment: 'Barbell',
group: 'push'
},
{
name: 'Pull-Up Bar',
equipment: 'Pull-Up Bar',
group: 'pull'
},
{
name: 'Dumbbell / Kettlebell Squat',
equipment: ['Dumbbell', 'Kettlebell'],
group: 'legs'
}
];
var searchName = "Push up";
var searchGroup = "push";
var searchText = "Dumbbell";
//as per priority do the name search first.
var directSeachNameMatches = exercises.filter((s) => s.name == searchName);
//Do a group search if direct name search doesn't give anything back
var groupMatches = directSeachNameMatches.length ? directSeachNameMatches : exercises.filter((s) => s.group == searchGroup);
//Do a text search if group name search doesn't give anything back
var searchTextMatches = groupMatches.length ? groupMatches : exercises.filter(function(s) {
if (!Array.isArray(s.equipment)) {
s.equipment = [s.equipment]
};
return s.equipment.indexOf(searchText) != -1;
});
var finalMatch = searchTextMatches ;
console.log(finalMatch);
Start with some basic simplifications:
if (x === true) should be if (x) (unless your variable can hold values of different types, which it really should not in the first place)
if (x) return true; else return false should be return x
test = false; x.forEach(v => { if (p(v)) test = true; }) should be test = x.some(p)
Then, change your data. As already said, you shouldn't have different types of values in the same place, specifically don't have equipment either be a string, a String instance, or an Array. Just make it always an array of strings!
Instead of the value 'none', have an empty array
Instead of a string, have a singleton array with that string as its element
Now we can at least simplify to
return (
allExercises
// Set the active exercise to have an active property
.map((item) => {
item.active = (this.props.chosenExercise === item.name);
return item;
})
.filter((item) => {
// If the exercise is active then return true
if (item.active) {
return true
}
// Filter by the exercise group if one is selected
if (item.group !== selectedType && selectedType !== 'all') {
return false;
}
// If there is search text then filter out non matches
if (searchText && !item.name.toLowerCase().includes(searchText)) {
return false;
}
// If the exercise doesn't need any equipment then return true
if (!item.equipment.length) {
return true;
}
// If the user has all equipment then return true
if (this.props.equipmentSelectionState === 'all') {
return true;
}
return item.equipment.some(itemEquipment =>
this.props.equipmentSelection.includes(itemEquipment)
);
})
Now transform this into a single boolean expression:
.filter(item =>
item.active// If the exercise is active then return true
|| (item.group === selectedType || selectedType == 'all') // Filter by the exercise group if one is selected
&& (!searchText || item.name.toLowerCase().includes(searchText)) // If there is search text then filter out non matches
&& (this.props.equipmentSelectionState === 'all' // If the user has all equipment then return true
|| !item.equipment.length // If the exercise doesn't need any equipment then return true
|| item.equipment.some(itemEquipment =>
this.props.equipmentSelection.includes(itemEquipment)
)
)
)
I realised that trying to not filter the active item was preventing me from chaining filters, which I think is the easiest and more readable solution. So instead I remove the active item and store it in a separate object and add it back after I'm done filtering.
return (
allExercises
// Set the active exercise to have an active property
.filter((item) => {
if (this.props.chosenExercise === item.name) {
item.active = true;
Object.assign(activeItem, item);
return false; // Remove the active element so we can add it later independent of filtering
} else {
item.active = false;
return true;
}
})
.filter(item => {
const {selectedType} = this.state;
if (selectedType === 'all') return true; // If 'all' exercise group is selected then let everything pass
if (item.group === selectedType) return true; // If the selected exercise group matches the exercise's group then pass
})
.filter(item => {
if (!searchText) {
return true; // If no search text let everything pass
} else if (item.name.toLowerCase().includes(searchText)) {
return true; // If there is search text then match against this
}
})
.filter(item => {
if (item.equipment === 'none') return true; // If exercise doesn't need equipment then always pass
if (this.props.equipmentSelectionState === 'all') return true; // If user has all equipment selected then always pass
// item.equipment is a string if there is only 1 piece of equipment required by the exercise
if (typeof item.equipment === 'string') {
// Check the users selected equipment for a match
if (this.props.equipmentSelection.includes(item.equipment)) {
return true;
}
} else {
// item.equipment is an array
let test = false;
this.props.equipmentSelection.forEach((equipmentSelection) => {
item.equipment.forEach(itemEquipment => {
if (itemEquipment === equipmentSelection) {
test = true;
}
});
});
return test;
}
})
// Sort by name
.concat(activeItem) // Add the active item we removed earlier
.filter((item) => {
// Filter out the empty result which is pushed when no exercise is active
if (Object.keys(item).length > 0) return true;
})
.sort((a, b) => {
// Sort alphabetically
if (a.name < b.name)
return -1;
if (a.name > b.name)
return 1;
return 0;
})
.map((item) => {
return (
<ChooseAnExercises
key={item.name}
name={item.name}
active={item.active}
setNumber={this.props.number}
updateValue={this.props.updateValue}
/>
)
})
)
I have a list of work history objects that have start and end date fields. I need to sort by start and then by end. If the dates overlap between work history objects when I use back-to-back sort() functions, the order can potentially be off. I need a way to sort an ImmutableJS List by multiple keys where the second sort key is only processed when first key values are equal. I would have assumed that ImmutableJS would have a simpler way to handle this type of sorting. Here is what I came up with, but this feels horrible to me (using momentJS for date comparison):
const sortedWorkHistory = workHistory.sort((b, a) => {
const aTo = moment(a.get('to') === null ? undefined : a.get('to'));
const bTo = moment(b.get('to') === null ? undefined : b.get('to'));
if (aTo.isBefore(bTo)) {
return -1;
}
if (bTo.isBefore(aTo)) {
return 1;
}
return 0;
})
.sort((b, a) => {
const aTo = moment(a.get('to') === null ? undefined : a.get('to'));
const bTo = moment(b.get('to') === null ? undefined : b.get('to'));
if (aTo === bTo) {
const aFrom = moment(a.get('from'));
const bFrom = moment(b.get('from'));
if (aFrom.isBefore(bFrom)) {
return -1;
}
if (bFrom.isBefore(aFrom)) {
return 1;
}
return 0;
}
});
You could put that into a single compare function like this:
const sortedWorkHistory = workHistory.sort((b, a) => {
const aTo = moment(a.get('to'));
const bTo = moment(b.get('to'));
const aFrom = moment(a.get('from'));
const bFrom = moment(b.get('from'));
if (aTo.isBefore(bTo)) {
return -1;
} else if (bTo.isBefore(aTo)) {
return 1;
} else if (aFrom.isBefore(bFrom)) {
return -1;
} else if (bFrom.isBefore(aFrom)) {
return 1;
}
return 0;
});
Or maybe even
const sortedWorkHistory = workHistory.sort((b, a) => {
// aTo = ...
if (aTo.isSame(bTo)) {
return aFrom.diff(bFrom);
}
return aTo.diff(bTo);
});
aTo === bTo is never going to be equal the way you are defining it. You're comparing references to objects which are always different. You'll need to use moments .isSame method. You can verify that by trying this:
const a = moment().startOf('day');
const b = moment().startOf('day');
console.log(a === b); // false
console.log(a.isSame(b)); // true
Here is what I'd do:
// Helper function to create moments
function parseMoment(date) {
return moment(date === null ? undefined : date);
}
const sortedWorkHistory = workHistory
.sort((a, b) => parseMoment(a.get('to')).diff(parseMoment(b.get('to'))))
.sort((a, b) => {
if(parseMoment(a.get('to')).isSame(parseMoment(b.get('to')))) {
return parseMoment(a.get('from')).diff(parseMoment(b.get('from')))
}
return 0;
});