Create a conditional subset array with selective keys - javascript

I'm trying to create a conditional sub-set of an array.
I have an array allBooks which has properties such as type,author,id etc.
In a particular view I want to show only some of properties based on a condition.
For example; displaying summarized properties of all the books in stock.
Here is what I have tried:
let booksInStock: any[] = [];
this.allBooks.forEach(book => {
// Add only when book is in stock
if (book.isInStock) {
// Get only few keys from all the available keys
let temp: any = {
typeOfBook: book.targetType,
author: book.author,
bookId: book.id,
bookDisplayName: book.value,
bookName: book.bookName
};
// Add to the summarized or filtered list
booksInStock.push(temp);
}
});
Is there a more efficient way of doing it?

Using filter and map would be more semantic, like so:
let booksInStock = this.allBooks
.filter(book => book.isInStock)
.map(book => ({
typeOfBook: book.targetType,
author: book.author,
bookId: book.id,
bookDisplayName: book.value,
bookName: book.bookName
})
);
If efficiency is your priority however, a for loop is faster. See this link for an example: https://jsperf.com/map-vs-for-loop-performance/6

For example:
// This function is pretty generic, you can find one in e.g. underscore.js or Ramda:
const pluck = fields => item =>
Object
.keys(item)
.filter(key => fields.includes(key))
.reduce((result, key) => {
result[key] = item[key]
return result
}, {})
// Create filter+map+pluck -settings for different 'views':
const inStock = books =>
books.filter(b => b.isInStock)
.map(pluck(['targetType', 'author', 'id', 'value', 'name']))
// Invoke them:
const booksInStock = inStock([
{ isInStock: true, author:'harry', otherFIeld:'not-included' },
{ isInStock:false, author:'notharry'}
])

Related

How can we create OBJ property with the same name from array values?

Hello I'm trying to create an object that includes under the same property name a bunch of array values,
This what I'm trying
const quiz = [
{question: 'Who is the main character of DBZ',
options: ['Vegetta','Gohan','Goku']}
]
const newObj = {
options: []
}
quiz.forEach((item)=>{
item.options.forEach((item, index)=>{
newObj.options[`name${index}`] = item
})
})
expected value =
newObj = {
options: [{name: 'Vegetta'},{name:'Gohan'},{name:'Goku'}]
}
actual value received =
newObj = {
{ options: [ name0: 'Vegetta', name1: 'Gohan', name2: 'Goku' ] }}
Thanks in advance!
As you've noticed, newObj.options[`name${index}`] = item creates a new key on your options array, and sets that to item. You instead want to push an object of the form {name: item} into your array. There are a few ways you could go about this, one way is to use .push() like so:
quiz.forEach((item)=>{
item.options.forEach((item)=>{
newObj.options.push({name: item});
})
});
while not as common, you can also use set the current index of options, which is slightly different to the above example, as it will maintain the same index, which can be important if quiz is a sparse array that you want to keep the same indexing of on options:
quiz.forEach((item)=>{
item.options.forEach((item, index)=>{
newObj.options[index] = {name: item};
})
});
Example of the difference:
const arr = [1, 2,,,5]; // sparse array
const pushRes = [];
const indxRes = [];
arr.forEach(n => pushRes.push(n));
arr.forEach((n, i) => indxRes[i] = n);
console.log("Push result", pushRes);
console.log("Index result", indxRes);
For a different approach, you also have the option of using something like .flatMap() and .map() to create your options array, which you can use to create newObj:
const quiz = [
{question: 'Who is the main character of DBZ',
options: ['Vegetta','Gohan','Goku']}
];
const options = quiz.flatMap(({options}) => options.map(name => ({name})));
const newObj = {options};
console.log(newObj);

Remove elements from one array and add them to a new array

I have an array of objects containing world countries with some additional information e.g.
countries = [
{
flag: 'assets/flags/angola.svg',
code: 'AGO',
name: 'Angola',
regions: [{
name: 'Luanda'
}]
},
{
flag: 'assets/flags/albania.svg',
code: 'ALB',
name: 'Albania',
regions: [{
name: 'Korça'
}, {
name: 'Tirana'
}, {
name: 'Gjirokastër'
}]
}...
I want to extract three of my favorite countries into a new array while removing them from the original array so I end up with two arrays one for my favorite countries and one for the rest of the countries.
I managed to achieve this the following way:
public createCountriesList(allCountries: Country[]) {
let topCountries: Country[] = [];
let restOfCountries: Country[];
allCountries.forEach((element) => {
switch (element.code) {
case 'HRV':
topCountries.push(element);
break;
case 'AT':
topCountries.push(element);
break;
case 'GER':
topCountries.push(element);
break;
}
});
restOfCountries = allCountries.filter((c) => {
return !topCountries.includes(c);
});}
It works, but I was wondering if there is a more elegant way to do this?
Everything seems fine according to me...
Obviously you need two arrays one for the extracted ones and second for rest of countries.
One thing we can work on is the switch case.
Instead of switch case you can use .includes function.
Store the name of countries you want to extract in an array.
const arr = ['HRV','AT','GR']
now you can do,
if(arr.includes(element.code)){
//push into new array
} else{
//push into another
}
One more thing you can do is save restOfTheCountries using .filter function.
Just return true for the countries which fails your above if case.
You can just use regular filter to split the array:
const isTop = ({code}) => ['HRV','AT','GR'].includes(code);
const topCountries = allCountries.filter(isTop);
const restOfCountries = allCountries.filter((contry) => !isTop(contry));
Another way, you can add a property that shows whether this country is top or not, and filter by this key
const withTop = countries.map((e) => ({...e, top: ['AGO','AT','GR'].includes(e.code)}));
// {
// code: "AGO"
// flag: "assets/flags/angola.svg"
// name: "Angola"
// regions: [{…}]
// top: true
// }
I would probably create a separate generic function for splitting array based on the criteria (using ts since you are)
const splitArray = <T>(array: Array<T>, matchFunction: (el: T) => boolean) => {
const matching: T[] = [], nonMatching: T[] = []
array.forEach(el => matchFunction(el) ? matching.push(el) : nonMatching.push(el))
return [matching, nonMatching]
}
then you can call it with the array and a function
const [topCountries, restOfCountries] = splitArray(countries, c => ["HRV", "AT", "GER"].includes(c.code))
that would be a bit more readable. a more elegant solution is to extend Array with that functionality (Array.prototype.split) then using countries.split(c => ["HRV", "AT", "GER"].includes(c.code))
All suggestions so far seem valid, I ended up using #Alexandr Belan answer as it was the most straightforward to implement in my case, I don't know why I used switch case instead of filter like for the topCountries 🤷‍♂️. Final code (added alphabetical sorting of the countries as well)
const suggestedCountriesList = ['Brazil', 'France', 'Germany'];
const suggested = ({ name }) => suggestedCountriesList.includes(name);
const suggestedCountries = allCountries.filter(suggested);
const otherCountries = allCountries.filter((country) => !suggested(country));
// sort alphabetically
otherCountries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
suggestedCountries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
return [suggestedCountries, otherCountries];

How to solve "Expected to return a value in arrow function" error in eslint

I am using eslint and getting this error.
Expected to return a value in arrow function
The error is showing on the third line of the code.
useEffect(() => {
let initialPrices = {};
data.map(({ category, options }) => {
initialPrices = {
...initialPrices,
[category]: options[0].price,
};
});
setSelectedPrice(initialPrices);
}, []);
The map function must return a value. If you want to create a new object based on an array you should use the reduce function instead.
const reducer = (accumulator, { category, options }) => (
{...accumulator, [category]:options[0].price}
)
const modifiedData = data.reduce(reducer)
More information https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
The map function is intended to be used when you want to apply some function over every element of the calling array. I think here it's better to use a forEach:
useEffect(() => {
let initialPrices = {};
data.forEach(({ category, options }) => {
initialPrices = {
...initialPrices,
[category]: options[0].price,
};
});
setSelectedPrice(initialPrices);
}, []);
Your map function should return something. Here it's not the case so the error happens. Maybe a reduce function will be more appropriate than map?
From what I can see in your case, is that you want to populate initialPrices, and after that to pass it setSelectedPrice. The map method is not a solution, for you in this case, because this method returns an array.
A safe bet in your case would a for in loop, a forEach, or a reduce function.
const data = [
{
category: "ball",
options: [
{
price: "120.45"
}
]
},
{
category: "t-shirt",
options: [
{
price: "12.45"
}
]
}
];
The forEach example:
let initialPrices = {};
// category and options are destructured from the first parameter of the method
data.forEach(({ category, options}) => {
initialPrices[category] = options[0].price;
});
// in this process I'm using the Clojure concept to add dynamically the properties
setSelectedPrice(initialPrices);
The reduce example:
const initialPrices = Object.values(data).reduce((accumulatorObj, { category, options}) => {
accumulatorObj[category] = options[0].price
return accumulatorObj;
}, {});
setSelectedPrice(initialPrices);

filtering object and modify properties

I am having a newbie question and I have tried to read the manuals over and over and cannot figure it out.
so I have this code:
export function editSerier(data, products) {
return (dispatch) => {
const filteredProducts = Object.assign(
...Object.keys(products)
.filter(key => products[key].Artikelgrupp === data.Artikelgrupp)
.map(k => ({
[k]: products[k]:{
Beskrivning: data.Beskrivning,
kategori: data.kategori,
status: data.status,
synas: data.synas,
tillverkare: data.tillverkare,
titel: data.titel}
})
})
console.log(filteredProducts)
}
}
Where I want to filter the incoming object products by "Artikelgrupp" and then modify the existent properties of the remaining products with properties from "data".
However this code does not let me run it.
Does someone have any idea?
UPDATE:
just solved it by merging both objects
const filteredProducts = Object.assign(
...Object.keys(products)
.filter(key => products[key].Artikelgrupp === data.Artikelgrupp)
.map(k => ({
[k]: {...products[k], ...data}
}))
)
You have invalid JavaScript. If you want a nested object, you need { something: { } } and if you want to use a computed property name, you need to surround it with [].
So, this will work
export function editSerier(data, products) {
return dispatch => {
const filteredProducts = Object.assign(
...Object.keys(products)
.filter(key => products[key].Artikelgrupp === data.Artikelgrupp)
.map(k => ({
[k]: {
[products[k]]: {
Beskrivning: data.Beskrivning,
kategori: data.kategori,
status: data.status,
synas: data.synas,
tillverkare: data.tillverkare,
titel: data.titel
}
}
}))
);
console.log(filteredProducts);
};
}
If I understand correctly, you are wanting to obtain a single object which:
excludes all value objects of products where a Artikelgrupp field does not match data.Artikelgrupp and,
the specific fields Beskrivning, kategori, etc, from your data object are merged/copied into the product values of the resulting object
One solution to this would be as
/* Extract entry key/value pairs from products object */
Object.entries(products)
/* Reduce entry pairs to required object shape */
.reduce((result, [key, value]) => {
/* Substitute for prior filter step. Avoid overhead of array copy
between prior filter and this reduction. */
if(value.Artikelgrupp !== data.Artikelgrupp) {
return result;
}
/* Update result object, "upserting" fields of data object into
existing product value, for this reduce iteration */
return {
...result,
[ key ] : {
...value,
Beskrivning: data.Beskrivning,
kategori: data.kategori,
status: data.status,
synas: data.synas,
tillverkare: data.tillverkare,
titel: data.titel
}
};
}, {})

Cycle.js - How to get collection length in a collection item

I'm trying to add some behavior exclusively to the last item in a list in Cycle.js. I tried to use cycle-onionify to make a collection like so:
const List = makeCollection({
item: Child,
itemKey: (childState, index) => String(index),
itemScope: key => key,
collectSinks: instances => {
return {
onion: instances.pickMerge('onion'),
DOM: instances.pickCombine('DOM')
.map(itemVNodes => ul(itemVNodes))
}
}
});
I understand that lenses can be used to share state between components, but there doesn't seem to be a way to use lenses with a collection. I'm thinking I could pass the Collection length to the children so I could compare it with an id.
Is there something I am missing?
You can use lenses with makeCollection. Remember it returns a normal Cycle.js component that you can isolate. So if you want to add a boolean isLast you can do this like this:
function omit(obj, key) {
let tmp = { ...obj }; //Copy the object first
delete tmp[key];
return tmp;
}
const listLens = {
get: stateArray => stateArray.slice(0, -1).concat({
...stateArray[stateArray.length - 1],
isLast: true
}),
set: (stateArray, mappedArray) => mappedArray.slice(0, -1)
.concat(omit(mappedArray[mappedArray.length - 1], 'isLast'))
};
const List = isolate(
makeCollection({
item: Child,
itemKey: (childState, index) => String(index),
itemScope: key => key,
collectSinks: instances => ({
onion: instances.pickMerge('onion'),
DOM: instances.pickCombine('DOM')
.map(itemVNodes => ul(itemVNodes))
})
}),
{ onion: listLens, '*': null }
);
As a side note, if you want to apply a lens on each individual item, you can do so too, with the itemScope property. For example
itemScope: key => ({ onion: myLens, '*': key })

Categories