How to do projection in TypeScript? - javascript

I want to populate orders which is an array of type Order. The expected result is orders=[{id:1,qt:4},{id:2, qt:2},{id:3,qt:2}]. How to do so in TypeScript? I am new to it.
export class Product {
constructor(public id: number, public name: string, public price: number) {}
}
export interface Order {
id: number;
qt: number;
}
export const products: Product[] = [
new Product(1, 'Apple', 2.1),
new Product(2, 'Banana', 2.2),
new Product(3, 'Chocolate', 2.3),
new Product(4, 'Dessert', 2.4),
];
export const cart: Product[] = [
products[0],
products[0],
products[2],
products[1],
products[2],
products[0],
products[1],
products[0],
];
export const orders: Order[] = [];
Edit
For those who want to know how
orders=[{id:1,qt:4},{id:2, qt:2},{id:3,qt:2}] is obtained.
In the cart:
the quantity of apples (id:1) is qt:4
the quantity of bananas (id:2) is qt:2
the quantity of chocolates (id:3) is qt:2
So by using cart, I have to obtain orders=[{id:1,qt:4},{id:2, qt:2},{id:3,qt:2}]. It should be clear.

Since you're looking for a "LINQ-like" solution, you probably want to use the higher order functions like map/filter/reduce.
Strictly, your problem cannot be solved purely with LINQ projections. Those merely represent map (Select), concatMap/flatMap (SelectMany), and zip (Zip). Your problem involves counting the occurences of each id throughout the entire array.
Pretty much every data manipulation problem can be solved with higher order folds, i.e reduce in javascript land, Aggregate in C# land. This one is no exception. The first thing to do, is to count the occurrences of each id, and build a counter object.
cart.reduce((acc, { id }) => {
acc[id] = (acc[id] ?? 0) + 1;
return acc;
}, {} as Record<number, number>);
Essentially, you start the fold operation with an empty object, then add each id and its occurrence count. Every time an id is encountered in the cart array, you increment its count in the object. If the id doesn't already exist in the accumulating object, nullish coalescing (acc[id] ?? 0) uses 0 and increments that instead.
This will give you-
{ '1': 4, '2': 2, '3': 2 }
Now, you need to turn this into-
[ { id: 1, qt: 4 }, { id: 2, qt: 2 }, { id: 3, qt: 2 } ]
For that, use Object.entries on the fold result to get-
> Object.entries({ '1': 4, '2': 2, '3': 2 })
[ [ '1', 4 ], [ '2', 2 ], [ '3', 2 ] ]
Finally, a simple map is all you need-
Object.entries(...).map(([id, qt]) => ({ id: Number(id), qt }))
Combining all that, you have-
export const orders: Order[] = Object.entries(
cart.reduce((acc, { id }) => {
acc[id] = (acc[id] ?? 0) + 1;
return acc;
}, {} as Record<number, number>)
).map(([id, qt]) => ({ id: Number(id), qt }));
One thing to note here is that Object.entries is pretty inefficient since it builds up an array instead of an iterator. If you're into efficiency, roll an iterator version of Object.entries and use that instead, using generator functions-
function* objEntries<T>(x: Record<string, T>): IterableIterator<[string, T]> {
for (const k in x) {
yield [k, x[k]];
}
}

Related

How to use 'reduce' function to filter and display products in cart

I am trying to train myself with Javascript and am a bit confused with reduce and would appreciate some guidance in understanding it. I've done a lot of research and have arrived at a stage where some clarity beyond just googling is needed. So here's my basic sample code..
the ids are the cart ids and the products are the products in a store
const ids = [4, 3, 1];
const products = [
{ id: 1, product: "Product 1" },
{ id: 2, product: "Product 2" },
{ id: 3, product: "Product 3" },
{ id: 4, product: "Product 4" },
{ id: 5, product: "Product 5" },
{ id: 6, product: "Product 6" },
{ id: 7, product: "Product 7" },
];
I need to display the products which are in the cart so I went with 3 options
The for loop, the map and filter and finally the reduce
FOR LOOP
// SOLUTION WITH FOR LOOP
const selectedProducts = [];
for (id of ids) {
const selectedProduct = products.filter((product) => product.id === id);
selectedProducts.push(selectedProduct);
}
console.log("selectedProducts", selectedProducts);
Question 1 : Currently I have just 7 products. but in an actual store there would be thousands of products. So is filtering thousands of products for each id a good idea. Is there a better way to do this?
MAP and FILTER
// SOLUTION WITH MAP
const mappedProducts = ids.map((id) => {
const [obj] = products.filter((product) => product.id === id);
return obj;
});
console.log("mappedProducts", mappedProducts);
Question 2 : As filter creates an array I ended up with an array of arrays and had to destructure the array and return the object. Is there a better way where I could directly destructure/pass the object without explicitly declaring return.
REDUCE
// SOLUTION WITH REDUCE
const initialArray = [];
const reducedProducts = products.reduce(function (acc, product) {
const productId = product.id;
if (ids.includes(product.id)) acc.push(product);
return acc;
}, initialArray);
console.log("reducedProducts", reducedProducts);
console.log("initialArray", initialArray);
Question 3 : What am I doing wrong with reduce?
This is my first time with reduce and I am sure I am doing something wrong here.. as reduce is supposed to be more compact than the for and map-filter combination but in my case it seems to be the opposite. Also I thought that with reduce the initialValue does not get mutated. But in my case it is getting mutated.
Any help and advice would be appreciated.
So is filtering thousands of products for each id a good idea. Is there a better way to do this?
From the products array, allow for easy lookup by restructuring it into an object or map indexed by ID, so you just need to use ordinary bracket notation or .get to get the matching object (O(1)).
const ids = [4, 3, 1];
const products = [
{ id: 1, product: "Product 1" },
{ id: 2, product: "Product 2" },
{ id: 3, product: "Product 3" },
{ id: 4, product: "Product 4" },
{ id: 5, product: "Product 5" },
{ id: 6, product: "Product 6" },
{ id: 7, product: "Product 7" },
];
const productsById = Object.fromEntries(products.map(
obj => [obj.id, obj]
));
const result = ids.map(id => productsById[id]);
console.log(result);
Is there a better way where I could directly destructure/pass the object without explicitly declaring return.
You could .find instead, which returns the matching object instead of returning an array - but that's still an O(n ^ 2) process. Indexing each product by its ID is better.
as reduce is supposed to be more compact than the for and map-filter combination but in my case it seems to be the opposite. Also I thought that with reduce the initialValue does not get mutated. But in my case it is getting mutated.
Not at all - .reduce, when not used in the appropriate circumstance, is indeed more verbose than more standard loops, as you're seeing. See this video.
Also I thought that with reduce the initialValue does not get mutated.
Sure it can, if the initial value is an object (and not a primitive) - objects can be mutated. If you mutate the accumulator parameter and return it, the next iteration (and the next, and the final return value) is the same exact object.
Just for note, a couple of examples with high-order functions:
reduce
products.reduce((acc, product) => {
if (ids.includes(product.id)) acc.push(product);
return acc;
}, []);
filter
products.filter((product) => ids.includes(product.id));
// or with destructuring
products.filter(({ id }) => ids.includes(id));
map and flat
products.map((product) => (ids.includes(product.id)) ? product : []).flat();
flatMap
products.flatMap((product) => (ids.includes(product.id)) ? product : []);

Apply Combo Discount to a Food Order

An app lets users order food from a menu. The menu has three types of selection: main, drink and dessert. A feature needs to be added which will discount the price by 10% for every main+drink combo (10% off every combo). All items ordered by the customer are stored in an array like so:
order = [
{id: 4, count: 1, type: "main", price: 10}
{id: 5, count: 2, type: "drink", price: 9.5}
]
As you can see, each item the customer orders has a count property. How can I apply the discount without mutating the order array or any of the object properties? Ideally I'd like to loop through the array, determine total number of combos (in the example above it would be 1), determine the total discount value and pass that value to another function which computes the order total. If anyone can suggest a better way of doing it, I'm all ears (or eyes in this case).
Also, what is the best way to express this problem from a technical point of view?
const userOrder = [
{ id: 4, count: 1, type: "main", price: 200 },
{ id: 5, count: 1, type: "drink", price: 100 }
];
const orderInfo = userOrder.reduce((acc, cur) => {
console.log('cur', cur)
if (acc[cur.type]) {
return {
...acc,
[cur.type]: cur.count,
totalAmount: (cur.count * acc.totalAmount)
}
} else {
return {
...acc,
[cur.type]: cur.count,
totalAmount: (cur.count * cur.price ) + acc.totalAmount
}
}
}, {
main: 0,
drink: 0,
totalAmount: 0
});
const noOfComobosPresent = Math.min(orderInfo.main, orderInfo.drink);
const totalDiscountValue = noOfComobosPresent * 10;
const finalAmount = orderInfo.totalAmount - ((orderInfo.totalAmount * totalDiscountValue ) / 100) ;
console.log('finalAmount', finalAmount)

office-ui-fabric / fluent-ui Grouped DetailsList

Today I tried to use the Grouped DetailsList of the fluent-ui.
What I expected: I need to declare some groups, let's say red, blue, green and then just add to each item, I want to add to the List, a specific property, that maps the Item to the groups.
e.g.:
groups: [
{ key: 'red', name: 'Color: "red"'},
{ key: 'green', name: 'Color: "green"'},
{ key: 'blue', name: 'Color: "blue"' },
],
items: [...,
{ key: 'red',anyProp1: "abc", anyProp2: "dfg",...},
...,
]
What I found out I have to do: I need to sort the Array, which contains my items in that way, that all Items belonging to the Group red need to be in one block. e.g.: [red, red, red, blue, blue, green, green, green]. Now I needed to provide the information about startIndex and count to map my Array of items to the groups.
This is what a definition of a group could look like:
groups: [
{ key: 'groupred0', name: 'Color: "red"', startIndex: 0, count: 2, level: 0 },
{ key: 'groupgreen2', name: 'Color: "green"', startIndex: 2, count: 0, level: 0 },
{ key: 'groupblue2', name: 'Color: "blue"', startIndex: 2, count: 3, level: 0 },
],
I can't understand why they have done it this way (For me it's very inconvenient this way). So, while I'm more between a beginner and an intermediate in JS. I think the guys who implemented this are professionals. There must be a reason. Maybe it has something todo with performance? I could imagine that when it comes to very large lists, it performs better this way, but I'm not sure.
Does anybody knows some details about this and can explain?
Faced the same issue and got a clue here. Then bult my solution.
Following is the function to generate groups array from the given items list sorted by the grouping column:
function groupsGenerator(itemsList, fieldName) {
// Array of group objects
const groupObjArr =[]
// Get the group names from the items list
const groupNames = [...new Set(itemsList.map(item => item[fieldName]))]
// Iterate through each group name to build proper group object
groupNames.forEach(gn => {
// Count group items
const groupLength = itemsList.filter(item => item[fieldName] === gn).length
// Find the first group index
const groupIndex = itemsList.map(item => item[fieldName]).indexOf(gn)
// Generate a group object
groupObjArr.push({
key: gn, name: gn, level: 0, count: groupLength, startIndex: groupIndex
})
})
// The final groups array returned
return groupObjArr
}
Typed and with empty group name option variant of the Timus's answer
function generateGroups(items: any[], fieldName: string, emptyGroupName: string = '-'): IGroup[] {
const groups: IGroup[] = []
const groupNames = [...new Set<string>(items.map(item => item[fieldName]))]
groupNames.forEach(name => {
const groupItemsCount = items.filter(item => item[fieldName] === name).length
const groupStartIndex = items.map(item => item[fieldName]).indexOf(name)
groups.push({
key: name,
level: 0,
name: name ?? emptyGroupName,
count: groupItemsCount,
startIndex: groupStartIndex
})
})
return groups
}

What is the best approach to clone objects properties when using map()?

I want to add a new property (contactDetails.countryName) and assign a value to a nested object stored in an array called users using the function map().
I've recently learned that I should use the spread operator (...) and then create / assign the new property in order to avoid mutating my original array of objects so I've developed 2 different implementations for this but I'm not really confident I'm following the best practices to accomplish I want to regarding the semantic and performance.
What would be the best approach to accomplish what I want to do in your opinion?
const countries = [
{ id: 3, countryName : "UK" },
{ id: 4, countryName : "Spain" },
{ id: 6, countryName : "Germany"}
];
const users = [
{ id : 1,
name: "Douglas Camp",
dateOfBirth: "23-06-1984",
contactDetails:
{
country: 3,
phone: "7373724997"
}
},
{
id : 2,
name: "Martin Stein",
dateOfBirth: "19-08-1992",
contactDetails:
{
country: 6,
phone: "3334343434"
}
},
];
const usersData = users.map(user=> {
// Version 1 : using spreading operator twice
const newUser = {
...user,
contactDetails: {
...user.contactDetails,
countryName: countries.find(c=> c.id == user.contactDetails.country).countryName
}
};
return newUser;
});
// Version 2: copying the original object property and using spread operator only for cloning the nested object properties
const newUser = {
id: user.id,
name: user.name,
dateOfBirth: user.dateOfBirth,
contactDetails: {
...user.contactDetails,
countryName: countries.find(c=> c.id == user.contactDetails.country).countryName
}
};
console.log(users);
console.log(usersData);
Here is an approach you can consider:
First of all I would Array.reduce the countries to a Map so you can get them via key/value or in this case by countries.get(key) and avoid filtering that array every time.
You can map through the users and for each one create a new object. In this case I call them accounts.
You can also consider using Object.assign
Note that both ... operator and Object.assign are shallow clone approaches. They do not recursively clone the nested objects/children. For that you can use JSON.stringify and JSON.parse etc.
let countries = [
{ id: 3, countryName : "UK" },
{ id: 4, countryName : "Spain" },
{ id: 6, countryName : "Germany"}
].reduce((r,{id, countryName}) => (r.set(id, countryName), r), new Map()) // reduce with Map
let users = [ { id : 1, name: "Douglas Camp", dateOfBirth: "23-06-1984", contactDetails: { country: 3, phone: "7373724997" } }, { id : 2, name: "Martin Stein", dateOfBirth: "19-08-1992", contactDetails: { country: 6, phone: "3334343434" } }, ];
let accounts = users.map(user => Object.assign({}, user, { // <-- map through
contactDetails: {
...user.contactDetails,
countryName: countries.get(user.contactDetails.country) // <-- get by key
}
}))
users[0].id = 2 // <-- modify users
users[0].contactDetails.phone = "00000"
console.log(users, accounts) // <-- no changes to accounts
Notice when we update the users[0].id and users[0].contactDetails.phone the accounts values did not update.
I normally use version 1, the spread operator twice. I also would consider checking out immer which allows you to do mutable updates on a cloned draft and handles merging it back for you.
const newUser = immer(user, draft => {
draft.contactDetails.countryName = countries.find(
c => c.id == user.contactDetails.country).countryName
)
})
Just edit the specific property you want and immer handles copying the rest of it.
Cloning and merging MapsSection
Just like Arrays, Maps can be cloned:
var original = new Map([
[1, 'one']
]);
var clone = new Map(original);
console.log(clone.get(1)); // one
console.log(original === clone); // false. Useful for shallow comparison
I personally like to use Version 1, as it makes your code much less redundant and easier to read. It also passes all the properties of 'user' down to newUser.

Converting Array to Object for selecting objects (Refactoring / Optimizing)

While I was facing slow loading time when it iterate array to render objects, I want to change its data structure. I show table of contents for seasons. When user clicks an item, the item is marked as selected.
Here is current data structure (Array)
const seasons = [{
id: 6,
value: 'All',
}, {
id: 7,
value: 'Spring',
}, {
id: 8,
value: 'Summer',
}, {
id: 9,
value: 'Fall',
}, {
id: 10,
value: 'Winter',
}];
I'm storing selected Season Ids as an Array now
state = {selectedSeasonIds: []}
When selectedSeasonIds has id, I want to remove the id from it. Otherwise, add the id to selectedSeasonIds. (This is current approach)
if(_.includes(this.state.selectedSeasonIds, id)) {
let newSelectedSeasonIds = _.filter(this.state.selectedSeasonIds, (curObject) => {
return curObject !== id;
});
this.setState({selectedSeasonIds : newSelectedSeasonIds});
} else {
let newSelectedSeasonIds = [...this.state.selectedSeasonIds, id];
this.setState({selectedSeasonIds : newSelectedSeasonIds});
}
And here is my pseudo-code for refactoring to convert my arrays to object structure for performance. (I found searching on an object is MUCH faster than searching on the array)
Changing the array to object
const seasons = {
6 :{
id: 6,
value: 'All',
},
7: {
id: 7,
value: 'Spring',
},
8: {
id: 8,
value: 'Summer',
},
9: {
id: 9,
value: 'Fall',
},
10: {
id: 10,
value: 'Winter',
}
};
Changing Selected Seasons <- I want to store only the key(id) of the objects. But I want to use it as an object
state = {selectedSeasonIds : {}} Can I store object type state?
Here is expected logic which can be 50 times faster than array search.
if(selectedSeasonIds[id]) {
//remove
return _.omit(state.selectedSeasonIds, id); < is this right?
} else {
//add
return {...state.selectedSeasonIds, [id]:id} <- Does this look ok?
}
Well if you think this is right, you can copy and paste my code to the answer (I will edit my question too).
Otherwise, Can you provide better suggestion or find the error?
Thank you so much
I guess you have to loop through seasons in order to render them.
My first suggestion is to add selected prop in each one of them so you don't have to check in selectedSeasonsIds on every render.
In case this is not an option, you can still keep the key value approach.
onAdd(id) {
this.setState({
selectedSeasonsIds: {
...this.state.selectedSeasonsIds,
[id]: this.state.selectedSeasonsIds[id] ? false : true
}
})
}
When checking for specific season whether they are selected or not, simply:
render() {
const { seasons, selectedSeasonsIds } = this.state
return (
<div>
...
{Object.keys(seasons).map(key =>
<ItemComponent
{...propsThatYouMightNeed}
selected={selectedSeasonsIds[key]}
/>
)}
</div>
)
}
Maybe something like this? I'd recommend storing arrays and then converting as necessary for lookups.
const seasons = [{
id: 6,
value: 'All',
}, {
id: 7,
value: 'Spring',
}, {
id: 8,
value: 'Summer',
}, {
id: 9,
value: 'Fall',
}, {
id: 10,
value: 'Winter',
}];
const seasonsHash = _.keyBy(seasons, 'id');
// check for existence
const hasId = _.has(seasonsHash, id)
// remove and convert back to array
_.values(_.omit(seasonsHash, id))
// add new id
_.concat(_.values(seasonsHash), id)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>

Categories

Resources