I have written this code to filter a "books" array, depending on the author or genre have been given as a parameter:-
//This code works perfectly fine as is
allBooks: (root, args) => {
if (args.author === undefined && args.genre === undefined) {
return books
} else if (args.author === undefined) {
return books.filter((book) => book.genres.includes(args.genre))
} else if (args.genre === undefined) {
return books.filter((book) => book.author === args.author)
} else {
const booksFilter = books.filter((book) => book.author === args.author)
return booksFilter.filter((book) => book.genres.includes(args.genre))
}
}
I believe there must be some way to write this more "professionally" without using all these if-else. So if anyone knows a better way, I'll appreciate it.
[Edited]
Thanks to all, I decided to go with ghostkraviz solution, code looks like this now:
allBooks: (root, args) => {
return books.filter((book) => {
const filteredGenres = book.genres.filter((genre) =>
genre.includes(args.genre || "")
);
return book.author.includes(args.author || "") && filteredGenres.length > 0;
})
}
You could take an array for filtering with key/value pairs, like
filters = [
['author', 'eliot'],
['genre', 'fiction']
]
and an object for storing special type of searching, like
methods = {
genre: 'includes'
}
Together, you get the following function
result = books.filter(book => filter.every(([key, value]) => key in methods
? book[key][methods[key]](value)
: book[key] === value
));
Because of Array#every's return value of true for empty arrays, you need no further action to get all books.
since String.prototype.includes actually checks if a string maybe found within another string (the parameter) MDN String.prototype.includes. that means, for undefined args you could default it to empty string. Empty strings will return true if checked with .includes from any string.
you only check for 2 args which are the author & genre.
here's the example:
const books = [
{author: "A", genres: ["Horror", "romance"]},
{author: "B", genres: ["romance"]},
{author: "X", genres: ["science"]},
{author: "C", genres: ["science", "Horror"]}
];
const allBooks = (root, args) => {
return books.filter(book => {
const filteredGenres = book.genres.filter(genre =>
genre.includes(args.genre || "")
);
return book.author.includes(args.author || "") && filteredGenres.length > 0;
});
};
console.log('filtered Horror = ', allBooks({}, {genre: 'Horror'}));
console.log('filtered A and Horror = ', allBooks({}, {author: 'A', genre: 'Horror'}));
console.log('filtered romance = ', allBooks({}, {genre: 'romance'}));
// for all books result
console.log('filtered romance = ', allBooks({}, {}));
// for an author result
console.log('filtered author(A) = ', allBooks({}, {author:"A"}));
I don't know if the version below is written more "professionally" as it looks messy, but it is a single line, and does not use if-else.
books = [{
author: "a",
genres: ["a"]},
{author: "a",
genres: ["a", "b"]},
{author: "b",
genres: ["c", "b"]
}];
allBooks = (root, args) => {
return (!args.author && !args.genre) ? books : (!args.author) ?
books.filter((book) => book.genres.includes(args.genre)):(!args.genre)?
books.filter((book) => book.author === args.author) :
(books.filter((book) => book.author === args.author)).filter((book) =>
book.genres.includes(args.genre));
}
console.log(allBooks("",{genre: "b"}));
//console:
//0: {author: "a", genres: Array(2)}
//1: {author: "b", genres: Array(2)}
I like to use if-else, as it's more readable. We can also eliminate the else-ifs since each of the if statement has a return.
This is just a simplified version of your existing codes without the else-ifs and curly brackets
allBooks: (root, args) => {
const { author, genre } = args;
if (!author && !genre) return books;
if (!author) return books.filter((book) => book.genres.includes(genre))
if (!genre) return books.filter((book) => book.author === author);
return books.filter((book) => book.author === author).filter((book) => book.genres.includes(genre))
}
Sometimes it's not about writing codes professionally.
It's about writing codes that you can easily understand.
Related
I have the following object:
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
In result I want an array of string as the following:
[
"celebrity/actor/male",
"celebrity/actor/female",
"celebrity/director",
"movie",
"user"
]
I create the following function:
function getPathsList(node, path = '') {
let pathsList = [];
const childs = Object.entries(node);
for (const child of childs) {
if (typeof child[1] === 'string') {
path += `/${child[1]}`
pathsList.push(path)
} else {
path += `/${child[0]}`
pathsList = [...pathsList, ...getPathsList(child[1], path, pathsList)]
}
}
return pathsList;
}
But I got:
[
"/celebrity/actor/male",
"/celebrity/actor/male/female",
"/celebrity/actor/director",
"/celebrity/movie",
"/celebrity/movie/user"
]
I know that the path variable should be initialized somewhere, but I can't figure it out.
You could use an appraoch which works without a path, but assembles the path by iterating the nested part object.
function getPathsList(node) {
const pathsList = [];
for (const [key, value] of Object.entries(node)) {
if (value && typeof value === 'object') {
pathsList.push(...getPathsList(value).map(p => `${key}/${p}`))
} else {
pathsList.push(key);
}
}
return pathsList;
}
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
console.log(getPathsList(modules));
Another way, using reduce:
const modules = {celebrity:{actor:{male:"male",female:"female"},director:"director"},movie:"movie",user:"user"};
function getPathsList(node, path = '') {
return Object.entries(node)
.reduce(
(res, [k, v]) => res.concat(
typeof v === "string" ? `${path}${v}` : getPathsList(v, `${path}${k}/`
)
), []);
}
console.log(getPathsList(modules));
You may consider a "dfs" like algorithm where you explore every path from root to leaf.
You then join your path with '/'.
Subtlety: don't put the leaf itself into the path (e.g: otherwise you would get movie/movie)
Below an example using flatMap
const modules = {"celebrity":{"actor":{"male":"male","female":"female"},"director":"director"},"movie":"movie","user":"user"}
const flatten = x => {
if (typeof(x) === 'string') { return [[]] }
// each node has for children its entries
// each node returns an array of path
return Object.entries(x).flatMap(([k, v]) => {
return flatten(v).map(path => [k , ...path])
})
}
console.log(flatten(modules).map(path => path.join('/')))
Where is the difficulty ?
const
modules =
{ celebrity:
{ actor: { male: 'male', female: 'female' }
, director: 'director'
}
, movie: 'movie'
, user: 'user'
}
, pathsList = []
;
function getPathsList( obj, path='' )
{
for (let key in obj )
{
if (typeof(obj[key]) === 'object') getPathsList( obj[key], path+'/'+key )
else pathsList.push( (path+'/'+key).substring(1) )
}
}
getPathsList( modules )
console.log( pathsList )
A very simple recursive solution using Object .entries returns an array containing only an empty string for non-objects, and otherwise, for every key-value, combines the key with the results of a recursive call to the value. The only slightly tricky part is to not insert the slash (/) before an empty string. It looks like this:
const getPathsList = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPathsList (v) .map (p => p ? k + '/' + p : k)
)
: ['']
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
But I would prefer to do this a slightly different way, building our function atop one that gathers the result into arrays of values (e.g. [['celebrity', 'actor', 'male'], ['celebrity', 'actor', 'female'], ... ['user']]), then simply joining those new arrays together with slashes. It's quite similar:
const getPaths = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [k, ...p])
)
: [[]]
const getPathsList = (obj) =>
getPaths (obj) .map (xs => xs .join ('/'))
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
I find that intermediate array format much more helpful.
This is a slightly less sophisticated version of getPaths than I generally write. Usually I distinguish between numeric array indices and string object keys, but that's not relevant here, since we're folding them back into strings, so this version is simplified.
The idea was to query a dataset with querystring params. I only want the "records" to match only what was queried.
Dataset
{
1111:
{
Category: "Education"
Role: "Analyst"
}
2222:
{
Category: "Communications and Media"
Role: "Analyst"
}
3333:
{
Category: "Public Sector"
Role: "Something else"
}
4444:
{
Category: "Public Sector"
Role: "Something else"
}
...
}
[[Prototype]]: Object
I'm sending in qString
Category: (2) ['Communications and Media', 'Education']
Role: ['Analyst']
length: 0
[[Prototype]]: Array(0)
I'd like to loop over that and filter/reduce so I only have records that match. Sort of an and instead of an or.
dataSet is an Object of objects. Thoughts? Thanks in advance.
export const Filtered = (qStrings, dataSet) => {
const filtered = [];
Object.entries(qStrings).forEach(([field]) => {
qStrings[field].forEach((value) => {
filtered.push(
..._.filter(dataSet, (sess) => {
if (sess[field] && sess[field].toString() === value.toString()) {
return sess;
}
})
);
});
});
return _.uniq(filtered);
};
geez, I figured it out with a colleague who's way smarter than me wink Jess!
export const Filtered = (qStrings, dataSet) => {
let filtered = [];
Object.entries(qStrings).forEach(([field], idx) => {
let source = filtered;
if (idx === 0) {
source = dataSet;
}
filtered = _.filter(source, (sess) => {
return sess[field] && sess[field].includes(qStrings[field]);
});
});
return _.uniq(filtered);
};
Now to clean this up.
Not sure if this solves your problem exactly, but you can apply this logic without mutation for a much cleaner function.
export const matches = (qStrings, dataSet) =>
Object.entries(dataSet).reduce((acc, [key, value]) =>
Object.entries(value).every(([rKey, rValue]) => qStrings[rKey]?.includes(rValue))
? { ...acc, [key]: value }
: acc,
{});
This will return records 1111 and 2222 because they match one of the categories and the role in qStrings.
I have filter function which is working just fine when I use static column name in it like:
this.listOfData = this.listOfData.filter((item: DataItem) =>
item.name.toLowerCase().indexOf(newValue.toLowerCase()) !== -1
);
PS: item.name
But I need to search in every column of the item, how can I do that?
PS: name should be dynamic.
My ListofData has this columns:
listOfData({
id:
ticket_number:
status_name:
name: // currently my function is set to this value only.
created_by_full_name:
receive_time:
response_time:
resolution_time:
})
Update
based on Allabakash answer I have final code below which returning lots of typescript error:
ngOnInit(): void {
// this listens to the input value from the service and does something on change.
this.globalSearchService.searchTerm.subscribe((newValue: string) => {
// this is where you would apply your existing filtering.
this.searchTerm = newValue;
if(newValue != null) {
this.visible = false
this.listOfData = this.listOfData.filter((item: DataItem) =>
let keys = Object.keys(item);
for (let key of keys) {
if (typeof item[key] === 'string' &&
item[key].toLowerCase().indexOf(newValue.toLowerCase()) !== -1) {
return true;
}
}
return false;
);
}
});
}
If you wanted to search on all properties dynamically, you can try something like this.
this.listOfData = this.listOfData.filter((item: DataItem) => {
let keys = Object.keys(item);
for (let key of keys) {
if (typeof item[key] === 'string' &&
item[key].toLowerCase().indexOf(newValue.toLowerCase()) !== -1) {
return true;
}
}
return false;
}
);
You can use ES6 for a one liner solution. Below is a sample using vanilla Javascript
const initialListOfData = [
{ name: 'Peter', surname: 'John'},
{ name: 'Judas', surname: 'James'},
{ name: 'Paul', surname: 'Peter'},
{ name: 'Petrover', surname: 'Junior'}
]
const searchItem = 'pet'
const listOfData = initialListOfData.filter(
item => Object.keys(item).some(prop =>
(new RegExp(searchItem.toLowerCase())).test(item[prop].toLowerCase())
))
console.log(listOfData)
For your Problem using typescript this will be
this.listOfData = initialListOfData.filter(
(item: any) => Object.keys(item).some((prop: any) =>
(new RegExp(newValue.toLowerCase())).test(item[prop].toLowerCase())
))
Console output for function with multiple arguments
I have this class method:
searchForProduct({productName, manufacturer, seller}, itemsPerPage = 20, onlyAvailable = true) {
console.log(Searching for...) // Here's what is my question about
//do stuff
}
How can I print out all arguments that I'm passing to the method?
What a want to achieve is:
searchForProduct({productName: laptop});
// Output:
"Searching for productName: 'laptop'"
// or
searchForProduct({productName: "laptop", manufacturer: "Dell"});
// Output:
"Searching for productName: 'laptop', manufacturer: 'Dell'"
And so on...
Also (if it's possible with any approach that will be proposed) I don't want to print out default itemsPerPage and onlyAvailable even if it will be passed to the method.
UPD:
Wow, I was not expecting this amount of such neat approaches in the answers.
However I should admit that I'm not allowed to change this function. Basically I just need to add this output form my personal needs since changing this method will "break everything".
I apologize for time that you took to propose passing object instead of destructed arguments. I'll upvote your answers anyway
UPD2:
I tried a couple of suggestion but still didn't achieved the perfect result:
searchForProduct({productName, manufacturer, seller}, itemsPerPage = 20, onlyAvailable = true) {
function buildString({firstArgument, secondArgument, thirdArgument}) {
return {
productName: (firstArgument !== undefined) ? firstArgument : "",
manufacturer: (secondArgument !== undefined) ? secondArgument : "",
seller: (thirdArgument !== undefined) ? thirdArgument : ""
}
}
const searchString = buildString({productName, manufacturer, seller})
const displayStr = Object.entries(searchString)
.map(([key, val]) => key + ': ' + val)
.join(', ');
console.log('Searching for', displayStr);
//do stuff
}
The buildString function is super ugly but it works without changing the original function.
However I have faced an issue when not all arguments are passed
It will look like:
"Searching for productName: "laptop", manufacturer: "Dell", seller: undefined
I tried:
function buildString({firstArgument, secondArgument, thirdArgument}) {
return {
productName: (firstArgument !== undefined) ? firstArgument : delete productName,
manufacturer: (secondArgument !== undefined) ? secondArgument : delete manufacturer,
seller: (thirdArgument !== undefined) ? thirdArgument : delete seller
}
}
But this gives me:
"Searching for productName: "laptop", manufacturer: "Dell", seller: true
According to MDN:
When trying to delete a property that does not exist, true is
returned
However property does exist - it's value does not. So what I tried to above is to delete the object key if it does not have a value.
Currently no success
I wouldn't destructure the first argument - instead, stringify it or iterate over its entries to extract its keys and values:
const searchForProduct = (obj) => {
console.log('Searching for', JSON.stringify(obj));
};
searchForProduct({productName: 'laptop'});
searchForProduct({productName: "laptop", manufacturer: "Dell"});
const searchForProduct = (obj) => {
const displayStr = Object.entries(obj)
.map(([key, val]) => key + ': ' + val)
.join(', ');
console.log('Searching for', displayStr);
};
searchForProduct({productName: 'laptop'});
searchForProduct({productName: "laptop", manufacturer: "Dell"});
You can also use a wrapper function that does the same thing:
const searchForProductWrapper = (obj, itemsPerPage = 20, onlyAvailable = true) => {
const displayStr = Object.entries(obj)
.map(([key, val]) => key + ': ' + val)
.join(', ');
console.log('Searching for', displayStr);
searchForProduct(obj, itemsPerPage, onlyAvailable);
};
const searchForProduct = (obj, itemsPerPage, onlyAvailable) => {
console.log('true searchForProduct', obj, itemsPerPage, onlyAvailable);
};
searchForProductWrapper({productName: 'laptop'}, 10);
searchForProductWrapper({productName: "laptop", manufacturer: "Dell"});
How can I print out all arguments that I'm passing to the method?
Its more like you would like to see object properties of first argument
function searchForProduct(data, itemsPerPage = 20, onlyAvailable = true) {
console.log(`Searching for ${JSON.stringify(Object.entries(data))}`)
}
criteria = {
productName: '',
manufacturer: ''
}
searchForProduct(criteria)
Here is my initial code that works flawlessly.
const objNotes = [
{},
{
title: "Be better",
body: "Do better"
},
{
title: "You are what you eat",
body: "Eat well and responsibly"
},
{
title: "Look good",
body: "Work good"
}
];
const findNote = (notes, noteTitle) => {
const index = notes.findIndex((note, index) => {
return note.title === noteTitle;
});
return notes[index];
};
const action = findNote(objNotes, "Look good");
console.log(action);
When I attach the method .toLowerCase like down below I get:
TypeError: Cannot read property 'toLowerCase' of undefined
and I don't understand why.
const findNote = (notes, noteTitle) => {
const index = notes.findIndex((note, index) => {
return note.title.toLowerCase() === noteTitle.toLowerCase();
});
return notes[index];
};
Your first object does not have the property title, trying to toLowerCase() that is throwing the error.
You can check if the property in object exists or not before using toLowerCase():
const objNotes = [
{},
{
title: "Be better",
body: "Do better"
},
{
title: "You are what you eat",
body: "Eat well and responsibly"
},
{
title: "Look good",
body: "Work good"
}
];
const findNote = (notes, noteTitle) => {
const index = notes.findIndex((note, index) => {
return note.title == undefined? '' : note.title.toLowerCase() === noteTitle.toLowerCase();
});
return notes[index];
};
const action = findNote(objNotes, "Look good");
console.log(action);
Use Array.find() when you want the item and not the index of the item.
To prevent the error when you call a string method on an undefined value, you can use short-circuit evaluation note.title !== undefined && .... Assuming the note.title is always a string if not undefined, an undefined value would return false immediately, and if it's not undefined the rest of the expression (the comparison) would be evaluated:
const objNotes = [{},{"title":"Be better","body":"Do better"},{"title":"You are what you eat","body":"Eat well and responsibly"},{"title":"Look good","body":"Work good"}];
const findNote = (notes, noteTitle) =>
notes.find((note, index) => // use Array.find()
note.title !== undefined && // the title is not undefined
note.title.toLowerCase() === noteTitle.toLowerCase() // compare the strings
);
const action = findNote(objNotes, "Look good");
console.log(action);