What is best way of finding array element intersection on rxjs - arrays

I know There is includes Operator on JavaScript Array.
So, Finding common elements from two other Arrays is no Problem. (https://stackoverflow.com/a/1885569/11455650)
Then, What is best way for this on rxjs?
const obs1$ = from(["foo", "bar", "baz", "qux"]);
const obs2$ = from(["bar", "garply", "fred", "foo"];
// const commonIntersection$ = functions or operators...
// result must be ["foo", "bar"]
I think there are two way for implementing this.
Which one is computationally efficient and How can I implement this with rxjs Operator?
merge Two Array and emit only Second value (ignore First Value)
filter each of elements

I assume you want a running intersection from each emission? If so you can either use the scan operator, or roll your own specific intersection operator.
The scan operator is like the reduce method on arrays. In this case, for each element (which is an array of strings) the intersection is returned. Each following emission will work on that last result.
merge(from([["foo", "bar", "baz", "qux"], ["bar", "garply", "fred", "foo"]])).pipe(
map(x => x),
scan((acc, cur) => {
return (!acc)
? next
: acc.filter(x => cur.includes(x));
}, undefined as string[] | undefined),
).subscribe(x => console.log(x));
A custom operator will look cleaner, so if you need it multiple times, then go for it! As you can see the logic below is essentially the same as the scan version. It keeps track of the running intersection state in the acc variable, and the current array is used to update it.
merge(from([["foo", "bar", "baz", "qux"], ["bar", "garply", "fred", "foo"]])).pipe(
map(x => x),
intersection(),
).subscribe(x => console.log(x));
/*...*/
function intersection<T>(): OperatorFunction<T[], T[]> {
return (observable: Observable<T[]>) => {
return new Observable<T[]>((subscriber) => {
let acc: T[] | undefined; // keeps track of the state.
const subscription = observable.subscribe({
next: (cur) => {
acc = (!acc)
? cur
: acc.filter(x => cur.includes(x));
subscriber.next(acc);
},
error: (err) => subscriber.error(err),
complete: () => subscriber.complete(),
});
return () => subscription.unsubscribe();
})
}
}
If you only want the last result then change the scan operator to reduce, or only emit the value of acc in the complete callback of the inner subscription in the operator version.

import { forkJoin, from } from 'rxjs';
import { toArray } from 'rxjs/operators';
import { intersection } from 'lodash';
const obs1$ = from(['foo', 'bar', 'baz', 'qux']);
const obs2$ = from(['bar', 'garply', 'fred', 'foo']);
forkJoin([obs1$.pipe(toArray()), obs2$.pipe(toArray())]).subscribe(
([arr1, arr2]) => console.log(intersection(arr1, arr2))
);
Try here: https://stackblitz.com/edit/rxjs-6jdyzy?file=index.ts

This is not rxjs used codes but used JavaScript Native Array methods (filter, includes).
It seems faster than rxjs operator.
const intersectionArray = combineLatest({
firstArray: [["foo", "bar", "baz", "qux"]],
secondArray: [["bar", "garply", "fred", "foo"]],
}).pipe(
map((x) => x.firstArray.filter((value) => x.secondArray.includes(value)))
);
intersectionArray.subscribe(console.log);

Related

Lodash Merge not updating object with correct values

I am trying to merge these two objects using Lodash merge.
Server object:
[
{
"id": "74738",
"customerId": "534430"
},
{
"id": "74742",
"customerId": "534429"
}
]
Local Object
[
{
"customerId": "534429"
"name": "ABC"
},
{
"customerId": "534430",
"name": "XYZ"
},
]
I am using lodash merge to combine these two objects based on attributes, I am using this code below:
merge({}, serverObject, localObject);
// Output: [{"id":"74738","customerId":"534429","name":"ABC"},{"id":"74742","customerId":"534430","name":"XYZ"}]
The object is not being updated based on the Customer Ids but instead by the sequence of local object.
Expected Output:
[{"id":"74738","customerId":"534430","name":"ABC"},{"id":"74742","customerId":"534429","name":"XYZ"}]
With lodash you can combine all arrays to a single one using _.flatten(), group by a predicate, and then map and merge each group to a single object.
Note: since your customerId is an integer, the items would be ordered by the numerical value.
const { map, groupBy, flatten, merge } = _
const fn = (predicate, ...arrs) => map(
groupBy(flatten(arrs), predicate),
group => merge({}, ...group)
)
const arr1 = [{"id":"74738","customerId":"534430"},{"id":"74742","customerId":"534429"}]
const arr2 = [{"customerId":"534429","name":"ABC"},{"customerId":"534430","name":"XYZ"}]
const result = fn('customerId', arr1, arr2)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
If you need to preserve the original order, flatten to a single array, and then reduce the items to a Map using the result of the predicate as the key. Merge each new item with the same key to the existing one in the Map. Convert the Map's values back to an array using Array.from():
const fn = (predicate, ...arrs) => Array.from(arrs
.flat()
.reduce((acc, o) => {
const key = predicate(o)
return acc.set(key, { ...acc.get(key), ...o })
}, new Map()).values()
)
const arr1 = [{"id":"74738","customerId":"534430"},{"id":"74742","customerId":"534429"}]
const arr2 = [{"customerId":"534429","name":"ABC"},{"customerId":"534430","name":"XYZ"}]
const result = fn(o => o.customerId, arr1, arr2)
console.log(result)

how to merge values in a list of map in ruby

say I have data structure like List< MAP< String, List>>, I only want to keep the List in map's value,
Like, I want to convert following example:
x = [{"key1" => ["list1", "list1"]}, {"key2" => ["list2", "list2"]},
{"key3" => ["list3", "list3"]}]
to:
y = [["list1", "list1"], ["list2", "list2"], ["list3", "list3"]]
Is there any quick way to do this? Thanks
The quickest thing that comes to mind is to leverage flat_map.
x = [ { "key1" => ["list1", "list1"] },
{ "key2" => ["list2", "list2"] },
{ "key3" => ["list3", "list3"] }]
y = x.flat_map(&:values)
=> [["list1", "list1"], ["list2", "list2"], ["list3", "list3"]]
flat_map is an instance method on Enumerable (https://ruby-doc.org/core-2.6.3/Enumerable.html#method-i-flat_map)
values is an instance method on Hash (https://ruby-doc.org/core-2.6.3/Hash.html#method-i-values)
If you only have 1 key in those hashes then you can do it like this:
y = x.map { |h| h.values[0] }

Rxjs Scan method copies content of array property of Observable when new items added

In an Angular component I have a property of type BehaviourSubject<IController>:
controllerData$ = new BehaviourSubject<IController | null>(null);
IController {
users: IUser[];
showDetails: boolean;
}
I need to iterate over the users array of type IUser and execute an intermediate transformation over the property data before returning the results as array.
Below the code I use:
flatMap(): to flatten the IUsers[] items
map(): to iterate over the users and apply the intermediate processing
scan(): to pack the results into an array again
Below my code:
controllerData$
.pipe(
filter((input) => input !== null),
flatMap((p) => {
return p.users;
}),
map((m: IUser) => {
const data = {m,
date: this.getWeekDay(m.occurrence),
nextOccurrence: this.service.getNextDate(m.frequency, m.occurrence),
};
return data;
}),
scan((acc: any, cur) => [...acc, cur], [])
)
When the component loads the first time with two IUser items, everything works well. However when I add a new user to the array, the scan method copies also the previous array values, that is, having two initial items and adding a new one, I get 5 items in the array (the first two twice and the new one) instead of just three elements.
As a workaround I achieved it with the solution below, but I was wondering whether I can skip the foreach loop and obtain everything with just Rxjs operators:
.pipe(
filter((input) => input !== null),
map((m) => {
const results: any[] = [];
m.users.forEach((ctlr: IUser) => {
results.push({
user: ctlr.user,
date: this.getWeekDay(ctlr.occurrence),
nextOccurrence: this.service.getNextDateOccurrence(ctlr.frequency, ctlr.occurrence),
});
});
return results;
}),

array.groupBy in TypeScript

The basic array class has .map, .forEach, .filter, and .reduce, but .groupBy i noticably absent, preventing me from doing something like
const MyComponent = (props:any) => {
return (
<div>
{
props.tags
.groupBy((t)=>t.category_name)
.map((group)=>{
[...]
})
}
</div>
)
}
I ended up implementing something myself:
class Group<T> {
key:string;
members:T[] = [];
constructor(key:string) {
this.key = key;
}
}
function groupBy<T>(list:T[], func:(x:T)=>string): Group<T>[] {
let res:Group<T>[] = [];
let group:Group<T> = null;
list.forEach((o)=>{
let groupName = func(o);
if (group === null) {
group = new Group<T>(groupName);
}
if (groupName != group.key) {
res.push(group);
group = new Group<T>(groupName);
}
group.members.push(o)
});
if (group != null) {
res.push(group);
}
return res
}
So now I can do
const MyComponent = (props:any) => {
return (
<div>
{
groupBy(props.tags, (t)=>t.category_name)
.map((group)=>{
return (
<ul key={group.key}>
<li>{group.key}</li>
<ul>
{
group.members.map((tag)=>{
return <li key={tag.id}>{tag.name}</li>
})
}
</ul>
</ul>
)
})
}
</div>
)
}
Works pretty well, but it is too bad that I need to wrap the list rather than just being able to chain method calls.
Is there a better solution?
You can use the following code to group stuff using Typescript.
const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>
list.reduce((previous, currentItem) => {
const group = getKey(currentItem);
if (!previous[group]) previous[group] = [];
previous[group].push(currentItem);
return previous;
}, {} as Record<K, T[]>);
// A little bit simplified version
const groupBy = <T, K extends keyof any>(arr: T[], key: (i: T) => K) =>
arr.reduce((groups, item) => {
(groups[key(item)] ||= []).push(item);
return groups;
}, {} as Record<K, T[]>);
So, if you have the following structure and array:
type Person = {
name: string;
age: number;
};
const people: Person[] = [
{
name: "Kevin R",
age: 25,
},
{
name: "Susan S",
age: 18,
},
{
name: "Julia J",
age: 18,
},
{
name: "Sarah C",
age: 25,
},
];
You can invoke it like:
const results = groupBy(people, i => i.name);
Which in this case, will give you an object with string keys, and Person[] values.
There are a few key concepts here:
1- You can use function to get the key, this way you can use TS infer capabilities to avoid having to type the generic every time you use the function.
2- By using the K extends keyof any type constraint, you're telling TS that the key being used needs to be something that can be a key string | number | symbol, that way you can use the getKey function to convert Date objects into strings for example.
3- Finally, you will be getting an object with keys of the type of the key, and values of the of the array type.
you could add the function to the array prototype in your app (note some don't recomend this: Why is extending native objects a bad practice?):
Array.prototype.groupBy = function(/* params here */) {
let array = this;
let result;
/* do more stuff here*/
return result;
};
Then create an interface in typescript like this:
.d.ts version:
interface Array<T>
{
groupBy<T>(func:(x:T) => string): Group<T>[]
}
OR in a normal ts file:
declare global {
interface Array<T>
{
groupBy<T>(func:(x:T) => string): Group<T>[]
}
}
Then you can use:
props.tags.groupBy((t)=>t.category_name)
.map((group)=>{
[...]
})
A good option might be lodash.
npm install --save lodash
npm install --save-dev #types/lodash
Just import it import * as _ from 'lodash' and use.
Example
_.groupBy(..)
_.map(..)
_.filter(..)
Instead of groupby use reduce. Suppose product is your array
let group = product.reduce((r, a) => {
console.log("a", a);
console.log('r', r);
r[a.organization] = [...r[a.organization] || [], a];
return r;
}, {});
console.log("group", group);
During the TC39 meeting of December 2021, the proposal introducing the new Array.prototype.groupBy and Array.prototype.groupByToMap function has reached stage 3 in the specification process.
https://github.com/tc39/proposal-array-grouping
https://github.com/tc39/proposals/commit/b537605f01df50fd4901be5ce4aa0d02fe6e7193
Here's how both functions are supposed to look like according to the README linked above:
const array = [1, 2, 3, 4, 5];
// groupBy groups items by arbitrary key.
// In this case, we're grouping by even/odd keys
array.groupBy((num, index, array) => {
return num % 2 === 0 ? 'even': 'odd';
});
// => { odd: [1, 3, 5], even: [2, 4] }
// groupByToMap returns items in a Map, and is useful for grouping using
// an object key.
const odd = { odd: true };
const even = { even: true };
array.groupByToMap((num, index, array) => {
return num % 2 === 0 ? even: odd;
});
// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
While not a 100% guaranty that it will really end up in a future version of JavaScript in the form described above (there's always a chance that the proposal can be adjusted or dropped, notably for compatibility reasons), it's nevertheless a strong commitment to have this groupBy feature offered in the standard lib soon.
By ripple effect, it also means that these functions will be also available in TypeScript.

ES6: Merge two arrays into an array of objects

I have two arrays that I want to merge together to one array of objects...
The first array is of dates (strings):
let metrodates = [
"2008-01",
"2008-02",
"2008-03",..ect
];
The second array is of numbers:
let figures = [
0,
0.555,
0.293,..ect
]
I want to merge them to make an object like this (so the array items match up by their similar index):
let metrodata = [
{data: 0, date: "2008-01"},
{data: 0.555, date: "2008-02"},
{data: 0.293, date: "2008-03"},..ect
];
So far I do this like so: I create an empty array and then loop through one of the first two arrays to get the index number (the first two arrays are the same length)... But is there an easier way (in ES6)?
let metrodata = [];
for(let index in metrodates){
metrodata.push({data: figures[index], date: metrodates[index]});
}
The easiest way is probably to use map and the index provided to the callback
let metrodates = [
"2008-01",
"2008-02",
"2008-03"
];
let figures = [
0,
0.555,
0.293
];
let output = metrodates.map((date,i) => ({date, data: figures[i]}));
console.log(output);
Another option is to make a generic zip function which collates your two input arrays into a single array. This is usually called a "zip" because it interlaces the inputs like teeth on a zipper.
const zip = ([x,...xs], [y,...ys]) => {
if (x === undefined || y === undefined)
return [];
else
return [[x,y], ...zip(xs, ys)];
}
let metrodates = [
"2008-01",
"2008-02",
"2008-03"
];
let figures = [
0,
0.555,
0.293
];
let output = zip(metrodates, figures).map(([date, data]) => ({date, data}));
console.log(output);
Another option is to make a generic map function which accepts more than one source array. The mapping function will receive one value from each source list. See Racket's map procedure for more examples of its use.
This answer might seem the most complicated but it is also the most versatile because it accepts any number of source array inputs.
const isEmpty = xs => xs.length === 0;
const head = ([x,...xs]) => x;
const tail = ([x,...xs]) => xs;
const map = (f, ...xxs) => {
let loop = (acc, xxs) => {
if (xxs.some(isEmpty))
return acc;
else
return loop([...acc, f(...xxs.map(head))], xxs.map(tail));
};
return loop([], xxs);
}
let metrodates = [
"2008-01",
"2008-02",
"2008-03"
];
let figures = [
0,
0.555,
0.293
];
let output = map(
(date, data) => ({date, data}),
metrodates,
figures
);
console.log(output);
If you use lodash, you can use zipWith + ES6 shorthand propery names + ES6 Arrow functions for a one-liner, otherwise see #noami's answer.
const metrodata = _.zipWith(figures, metrodates, (data, date)=> ({ data, date }));

Resources