Applying a property to each element in several arrays, then returning a flat map with one array - javascript

I have a collection that looks like this
[
{
count: 123,
description: 'some description',
articles: [
{...}
]
},
{
count: 234,
description: 'some description',
articles: [
{...}
]
}
]
Each object in the collection has a collection of articles. What I need is to apply the description to each article object in the respective collection in each element of the primary collection. I also want to end up with a flat array containing only articles. Clearly I'm using mergeMap incorrectly, but I'm not sure how to do it.
I have tried this
json$.pipe(
// Filter out empty arrays
filter(section => section.count > 0),
// Apply the descriptions
map(section => section.articles.map(a => (Object.assign(a, section.sectionName)))),
mergeMap(x => x.articles)
).subscribe(x => console.log(x));
But the articles do not have the description property in them, and it's not a flat array of articles. I've tried a few things but I'm unsure how to proceed

You only need to concatMap the outer observable, after adjusting each article.
const { Observable } = Rx;
const { map, concatMap, filter } = Rx.operators;
const json$ = Observable.from([
{
count: 123,
description: 'some description 123',
articles: [
{id: 1},
{id: 2},
]
},
{
count: 234,
description: 'some description 234',
articles: [
{id: 3},
{id: 4},
]
}
]);
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => Observable.from(articles).map(a => ({...a, description}))
)
);
withDescription$.subscribe(console.log);
<script src="https://unpkg.com/#reactivex/rxjs#^5/dist/global/Rx.min.js"></script>
If you don't need any special behavior on the inner observable, you could simplify to:
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => articles.map(a => ({...a, description}))
),
);

Related

Array of inner subscriptions with rxjs

I'm trying to improve the flattening and reduce the chaining inside my Rxjs code.
A REST call of my service getAllItems() returns the following Items[]:
[{
id: 101,
name: 'Car',
owner: 1
},
{
id: 102,
name: 'Table',
owner: 2
}]
I have another endpoint getOwnerInfo(id:number) which provides information based in the "owner" id, information which I want to combine, so the final answer should look like:
[{
id: 101,
name: 'Car',
owner: {
id: 1,
username: 'Mikelinin',
name: 'Mikel',
},
},
{
id: 102,
name: 'Table',
owner: {
id: 2,
username: 'Thominin',
name: 'Thomas',
},
}]
My current implementation .subscribes once to getAllItems call and then in the subscription body it iterates per element subscribing to getOwnerInfo.
I've been looking through other flattening examples, but basically they "break" the array, and they never put it back together. I need the output to be an array.
I've tried to use from(), concatMap(), and mergeMap() but seems I am unable to combine both requests properly.
Map your array of items into an array of observables that each emit the updated item. Then join the array of observables together.
getAllItems().pipe(
// turn item[] into observable<updatedItem>[]
map(items => items.map(item => getOwnerInfo(item.id).pipe(
map(owner => ({...item, owner}))
),
// join observable<updatedItem>[] into updatedItem[]
switchMap(itemCalls => forkJoin(itemCalls))
).subscribe(console.log);
Or you can do the mapping and joining in one step:
getAllItems().pipe(
// source item[], do stuff, emit updatedItem[]
switchMap(items => forkJoin(
items.map(item => getOwnerInfo(item.id).pipe(
map(owner => ({...item, owner}))
))
))
).subscribe(console.log);
The following will use from to convert a distinct array of owner ids into a stream of owner ids. Then it will use toArray to convert that stream back into an array and map the items array to an array of items with their respective owner object.
this.getAllItems().pipe(
switchMap(items => {
const ownerIds = Array.from(new Set(items.map(x => x.owner)));
return from(ownerIds).pipe(
concatMap(x => this.getOwnerInfo(x))
toArray(),
map(owners => items.map(x => ({ ...x, owner: owners.find(y => y.id === x.owner) })))
)
})
)

How to do projection in TypeScript?

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]];
}
}

Comparing two arrays with field in common then pushing to a new array with corresponding grouped fields

general programming problem here.
I have this array called SPACES
[
{
_id: 5e1c4689429a8a0decf16f69,
challengers: [
5dfa24dce9cbc0180fb60226,
5dfa26f46719311869ac1756,
5dfa270c6719311869ac1757
]
},
{
_id: 5e1c4eb9c9461510407d5e81,
challengers: [ 5dfa24dce9cbc0180fb60226, 5dfa26f46719311869ac1756 ],
}
]
And this array called USERS
[
{
_id: 5dfa24dce9cbc0180fb60226,
name: 'Account 1',
email: 'account1#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69, 5e1c4eb9c9461510407d5e81 ],
},
{
_id: 5dfa26f46719311869ac1756,
name: 'Account 2',
email: 'account2#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69, 5e1c4eb9c9461510407d5e81 ]
},
{
_id: 5dfa270c6719311869ac1757,
name: 'Account 3',
email: 'account3#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69 ]
}
]
What I want to do, is go through both, and instead of having the SPACES.challengers array be just IDS, I would like the array to contain each USER object.
So for example, if the USER has an ID that is inside the SPACES.challengers array, then push the user into that array (which will then be the entire object).
SO FAR I have tried this (I am not very good yet):
users.map( ( user ) => {
spaces.map( ( space ) => {
if ( user.spaces.includes( space._id ) ) {
space.challengers.push(user)
}
} );
} );
However, I am not getting inside the IF block. (Even if I did, not sure if it would work OR if this is even how to do it). It feels Odd doing double maps, as I get so many iterations, and it duplicates my push (cause I have no logic to see if it just has been pushed).
Assuming every entry in the Users array has a unique ID, we can build a Hashmap to store (id, index) pairs in order to search efficiently for an ID from Users array while looping through Spaces array.
let spaces = [{_id: '5e1c4689429a8a0decf16f69',challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756', '5dfa270c6719311869ac1757']},{_id: '5e1c4eb9c9461510407d5e81',challengers: [ '5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756' ],}]
let users = [{_id: '5dfa24dce9cbc0180fb60226',name: 'Account 1',email: 'account1#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81' ],},{_id: '5dfa26f46719311869ac1756',name: 'Account 2',email: 'account2#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81' ]},{_id: '5dfa270c6719311869ac1757',name: 'Account 3',email: 'account3#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69' ]}]
let IDIndexMapping = {} // To store (_id, index) pairs, in order to improve search efficiency
for(let index in users) // Iterate through Users array using index
IDIndexMapping[users[index]._id] = index; // store (_id, index) pair in IDIndexMapping
// I'm avoiding using `map` and using vanilla `for` loop for space efficiency
// as map returns a new array but with `for` loop, we can perform changes in-place
for(let outerIndex in spaces){ // Iterate through `spaces` array using index
let challengers = spaces[outerIndex].challengers; // Get challengers array
for(let innerIndex in challengers){ // Iterate through challengers array using index
let ID = challengers[innerIndex]; // Get ID
if(ID in IDIndexMapping) // If ID exists in IDIndexMapping
spaces[outerIndex].challengers[innerIndex] = users[IDIndexMapping[ID]]; // Change ID to actual User object
}
}
console.log(spaces)
Output
[ { _id: '5e1c4689429a8a0decf16f69',
challengers: [ [Object], [Object], [Object] ] },
{ _id: '5e1c4eb9c9461510407d5e81',
challengers: [ [Object], [Object] ] } ]
.map and .find should work here. keep it simple.
var spaces = [
{
_id: "5e1c4689429a8a0decf16f69",
challengers: [
"5dfa24dce9cbc0180fb60226",
"5dfa26f46719311869ac1756",
"5dfa270c6719311869ac1757"
]
},
{
_id: "5e1c4eb9c9461510407d5e81",
challengers: ["5dfa24dce9cbc0180fb60226", "5dfa26f46719311869ac1756", "some non existent"]
}
],
users = [
{
_id: "5dfa24dce9cbc0180fb60226",
name: "Account 1",
email: "account1#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69", "5e1c4eb9c9461510407d5e81"]
},
{
_id: "5dfa26f46719311869ac1756",
name: "Account 2",
email: "account2#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69", "5e1c4eb9c9461510407d5e81"]
},
{
_id: "5dfa270c6719311869ac1757",
name: "Account 3",
email: "account3#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69"]
}
],
result = spaces.map(({ _id, challengers }) => ({
_id,
challengers: challengers.map(challenger =>
users.find(user => user._id === challenger)
).filter(row => row)
}));
console.log(JSON.stringify(result, null, 2));
You can create a map of challengers for look-up and then put them in spaces.
//create user map for look-up
userMap = users.reduce((res, val) => ({
...res,
[val._id]: val
}), {});
//change challenger id with user object
inflatedSpaces = spaces.map(s => ({ ...s, challengers: s.challengers.map(c => userMap[c]) }));
You could map the users with a Map.
Beside the destructuring of the object for mapping this answer uses for this part
challengers: challengers.map(
Map.prototype.get, // cb with a prototype and using `this`
new Map(users.map(o => [o._id, o])) // thisArg
)
the above mentioned Map in two parts.
The lower part generates an instance of Map where _id of the users items is used as key and the whole object as value. This instance is uses as thisArg of Array#map, the second parameter.
The upper part is a prototype of Map, used as callback. And while an this object is supplied, a binding (Function#bind) is not necessary.
var spaces = [{ _id: '5e1c4689429a8a0decf16f69', challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756', '5dfa270c6719311869ac1757'] }, { _id: '5e1c4eb9c9461510407d5e81', challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756'] }],
users = [{ _id: '5dfa24dce9cbc0180fb60226', name: 'Account 1', email: 'account1#gmail.com', spaces: ['5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81'] }, { _id: '5dfa26f46719311869ac1756', name: 'Account 2', email: 'account2#gmail.com', spaces: ['5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81'] },{ _id: '5dfa270c6719311869ac1757', name: 'Account 3', email: 'account3#gmail.com', spaces: ['5e1c4689429a8a0decf16f69'] }],
result = spaces.map(({ _id, challengers }) => ({
_id,
challengers: challengers.map(
Map.prototype.get,
new Map(users.map(o => [o._id, o]))
)
}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

rxjs: recursive Observable emission inside map

I am pulling my hair out a little with attempting to group data recursively in rxjs. There seems to be alot of good examples around with different use cases but I cant seem to refactor the code around to fit my requirements.
The central problem that I can infer is that in my map operator I have a conditional return which is either a non-observable or an observable. Is there a way I can refactor to account for this discrepency?
Ideally this function would group an original "flat" array by an arbitrary amount of columns that are passed in as the cols argument.
Each time the criteria is satisfied it would
append to an array in the format
{ key: col_name, elements : [...col_elements] } where elements would be another array of elements in the same format, or a straight list of elements.
The below function works when only grouping the columns once ( and thus never requiring the observable within map to emit ).
//group data n times based on passed string[] of column attributes
group_data(elements: Observable<any>, cols: string[], index=0) : Observable<any> {
let col = cols[index]
return elements.pipe(
//groupby column value
RxOp.groupBy((el:any) => this.get_groupingValue(el, col)),
//place key inside array
RxOp.mergeMap((group) => group.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + group.key]))
),
// map to key:group
RxOp.map((arr:any) =>
cols.length <= index + 1?
// no more grouping req
{ 'key': arr[0], 'elements': arr.slice(1) } :
//group recursively, returns Observable instead of array:(
{ 'key': arr[0], 'elements':
this.group_data(from(arr.slice(1)), cols, index + 1)
.pipe(
RxOp.tap(t=> console.log(t)), // not logged
RxOp.reduce((acc, val) => [...acc, val], [])
)
}),
RxOp.toArray()
)
}
//simplified data example:
data = [{id: 'idA', type: 'project', parents: null },
{id: 'idB', type: 'project', parents: null },
{id: 'idC', type: 'episode', parents: ['idA'] },
{id: 'idD', type: 'episode', parents: ['idB'] },
{id: 'idE', type: 'scene', parents: ['idA', 'idC'] },
{id: 'idF', type: 'scene', parents: ['idB', 'idD'] }]
// 1 column passed works correctly as below
group_data(elements: from(data), ['project'])
/* outputted data:
[{key: 'idA',
elements: [ {id: 'idC', type: 'episode', parents: ['idA'] },
{id: 'idE', type: 'scene', parents: ['idA', 'idC'] }]},
{key: 'idB',
elements: [ {id: 'idD', type: 'episode', parents: ['idA'] },
{id: 'idF', type: 'scene', parents: ['idA', 'idC'] }]},
{key: null,
elements: [ {id: 'idA', type: 'project', parents: [] }
{id: 'idB', type: 'project', parents: [] }]}
]
*/
// 2 columns not working correctly
group_data(elements: from(data), ['project', 'episode'])
/*[{key: 'idA',
elements: Observable},
{key: 'idB',
elements: Observable},
{key: null,
elements: Observable}
]*/
The approach i needed to take was to restructure so that I could use mergeMap instead of map - which meant i needed two observables at the condition instead of one. From there i just needed to refactor so that the mapping to key, elements was done after the mergeMap.
This is still my first foray into rxjs, so my explanation isn't great and my summary of the problem also wasnt great. Importantly, at least its behaving as expected now.
//group data n times based on passed string[] of column attributes
group_data(elements: Observable<any>, cols: string[], index=0) : Observable<any> {
let col = cols[index]
let grouping = elements.pipe(
//groupby column value
RxOp.groupBy((el:any) => this.get_groupingValue(el, col)),
//place key inside array
RxOp.mergeMap((group) => group.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + group.key]))
)
)
return grouping.pipe(
RxOp.mergeMap((arr) =>
(
cols.length <= (index +1) ?
//no more grouping required
of(arr.slice(1)) :
//group again
this.group_data(from(arr.slice(1)), cols, index + 1))
// reduce result and put the key back in
.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + arr[0]])
)
),
// map to key:group
RxOp.map(arr => ({
key: arr[0],
elements: arr.slice(1)
})
),
RxOp.toArray()
)

Dynamic array filtering by object property

I have a react live search dropdown component that filters through an array of objects by a search term. It filters my objects by title and then returns a list of all the related objects. This works fine.
Current:
Data Structure
data: [
{ id: 1, title: 'Some title here' },
{ id: 2, title: 'Another title' },
{ id: 3, title: 'last title' },
]
Component
<LiveSearch
term={term}
data={data} />
Inside Live search component
Filter data by term and render list
return data
.filter(item => item.title.toLowerCase().includes(term.toLowerCase())
.map((item, idx) => <li key={idx}>{item.title}</li>
My objects to search by are getting more advanced and what I would like to be able to do is pass into my component an array of property names I would like to compare to the search term.
My thinking process behind it is to loop through the object properties and if on of the properties matches the term the loop breaks and returns true adding that object to the list of items to be displayed.
Goal
Data Structure
data: [
{ id: 1, country: 'Canada', title: 'Some title here' },
{ id: 2, country: 'Australia', title: 'Another title' },
{ id: 3, country: 'Netherlands', title: 'last title' },
]
Component
<LiveSearch
searchFields={['country', 'title']}
term={term}
data={data} />
Inside Component filtering
return data
.filter(item => {
// Dynamic filtering of terms here
})
.map((item, idx) => <li key={idx}>{item.title}</li>
Inside the filter I'm trying to get a loop through the array and dynamically produce logic similar to this
item.searchFields[0].toLowerCase().includes(term.toLowerCase()) ||
item.searchFields[1].toLowerCase().includes(term.toLowerCase())
But obviously could loop over an infinite number of searchfields/properties
Use Array#some()
Something like
term = term.toLowerCase()
return data
.filter(item => {
return searchFields.some(field => item[field].toLowerCase().includes(term))
}).map(...
Check if some of the searchFields match:
// Checks wether a value matches a term
const matches = (value, term) => value.toLowerCase().includes(term.toLowerCase());
// Checks wether one of the fields in the item matcues the term
const itemMatches = (fields, term) => item => fields.some(field => matches(item[field], term);
// Filter the data to only contain items where on of the searchFields matches the term
const result = props.data.filter( itemMatches(props.searchFields, props.term) );
return result.map(item => <li key={idx}>{item.title}</li>);
You can use Array .some combined with .filter
let result = data.filter(obj =>
searchFields.some(s =>
obj[s] != undefined && obj[s].toLowerCase() === term
));
let data = [
{ id: 1, country: 'Canada', title: 'Some title here' },
{ id: 2, country: 'Australia', title: 'Another title' },
{ id: 3, country: 'Netherlands', title: 'last title' },
], searchFields = ["country", "title"], term = "canada";
let result = data.filter(obj =>
searchFields.some(s =>
obj[s] != undefined && obj[s].toLowerCase() === term
));
console.log(result);

Categories

Resources