Convert array of observables of observables to array of observables rxjs - javascript

To simplify the problem i have used numbers and strings here. The code:
const numbers$:Observable<number[]> = of([1,2,3]);
const strings: string[] = ["a","b"];
function getStrings(): Observable<string>[]{
return numbers$.pipe(
map((numbers: number[]) => {
const strings$: Observable<string>[] = strings.map(s => of(s));
return strings$;
}),
)
}
getStrings().subscribe(x => console.log(x))
The error i am getting is:
Type 'Observable<Observable<string>[]>' is missing the following properties from type 'Observable<string>[]
How can i get Observable<string>[] from getStrings function? I have tried to use flatMap, switchMap but unable to get the perfect combination.
Stackblitz

You'll need to use mergeMap() and forkJoin():
function getStrings(): Observable<string[]>{
return numbers$.pipe(
mergeMap((numbers: number[]) => {
const strings$: Observable<string>[] = strings.map(s => of(s));
return forkJoin(strings$);
}),
)
}
getStrings().subscribe(x => console.log(x))
https://stackblitz.com/edit/rxjs-dzvsbh?file=index.ts

So as I understand you want to "zip" two lists together, via observable ?
I can offer you this one
const numbers$: Observable<number[]> = of([1, 2, 3]);
const strings$: Observable<string[]> = of(['a', 'b']);
const combined$: Observable<any> = zip(numbers$, strings$)
.pipe(
map(([numbers, strings]) =>
numbers.length > strings.length ?
numbers.map((value, index) => [value, strings[index]]) :
strings.map((value, index) => [numbers[index], value]))
);
combined$.subscribe(value => console.log(value));
This will log:
[
[
1,
"a"
],
[
2,
"b"
],
[
3,
null
]
]

Related

Passing an array to callback

how would you solve this: Update the transform function such that it works with n number elements and also same function works for string element ?
const input = [
[2,3,5],
[2,4],
[7,8,9]
];
/*Edit only the transform */
const transform = (input, callback) => {
return callback([input[0],input[1]]);
}
/*Edit code only above */
const output = transform(input, (elm1, elm2) => {
return elm1.concat(elm2);// should return [7,8,9,2,4,2,3,5]
});
const input2 = ["hello", "welcome", !];
const output2 = transform(input2, (elm) => {
return elm.toUppercase(); // should return HELLO WELCOME !
});
Thank you all
In my opinion you're not writing clear JavaScript code. Functional paradigms are your friend but units of work that aren't meaningful will only work against you. What benefit does your transform function provide, why is it better than calling the cb method directly on your data? Consider looking into es6 array functions like flatMap and reduce.
const input = [
[2,3,5],
[2,4],
[7,8,9]
]
console.log(input.reduceRight((acc,cur) => acc.concat(cur), []))
// [7, 8, 9, 2, 4, 2, 3, 5]
console.log(["hello", "world"].map(str => str.toUpperCase()).join(" "))
//"HELLO WORLD"
You would need to pass the whole array to your transform function.
For the first output => flat the arrays using flat(Infinity)
For the second output => 'merge' all array value using join and then apply toUpperCase to the whole string:
const input = [
[2,3,5],
[2,4],
[7,8,9]
];
const transform = (input, callback) => {
return callback(input);
}
const output = transform(input.reverse(), (elm1, elm2) => {
return elm1.concat(elm2); // should return [7,8,9,2,4,2,3,5]
});
console.log(output.flat(Infinity).filter(Boolean));
const input2 = ["hello", " welcome", " !"];
const output2 = transform(input2, (elm) => {
return elm.join('').toUpperCase(); // should return HELLO WELCOME !
});
console.log(output2);

How to simplify function which returns an object?

I have a function which returns an object but I don't like that I gotta declare it first and then do forEach method
export default (data) => {
const keysWithDotsObject = {};
Object.keys(data).forEach((keyWithDot) => {
Object.keys(data[keyWithDot]).forEach((key) => {
keysWithDotsObject[`${keyWithDot}.${key}`] = data[keyWithDot][key];
});
});
return keysWithDotsObject;
};
I think there should be something like this
export default (data) => {
const keysWithDotsObject = Object.keys(data).map((keyWithDot) => {
Object.keys(data[keyWithDot]).map((key) => ({
[`${keyWithDot}.${key}`]: data[keyWithDot][key],
}));
});
return keysWithDotsObject;
};
But for some reason, it doesn't work.
PS: In this part --
[`${keyWithDot}.${key}`]
-- I'm trying to create a key with a name separated by a dot (I don't like that, but that's what back-end wants me to)
Input :
Query1 = {
locus_ids: [25, 26],
microorganism_ids: [12],
};
Output :
Query1.locus_ids: [25, 26],
Query1.microorganism_ids: [12]
I also would like any suggestions on how to write more readable code
Did you consider using reduce?
export default (data) => Object.keys(data).reduce((acc, keyWithDot) => (
Object.keys(data[keyWithDot]).forEach((key) => {
acc[`${keyWithDot}.${key}`] = data[keyWithDot][key];
}),
acc
), {});
You can also use Object.fromEntries, map and flatMap should do the job:
export default (data) =>
Object.fromEntries(
Object.keys(data).flatMap((keyWithDot) =>
Object.keys(data[keyWithDot]).map((key) => [`${keyWithDot}.${key}`, data[keyWithDot][key]])
)
);
First, you build an array for each subentry, for each subentry, you flatten the array you got into an array of key/value, then with Object.fromEntries, you make a new object!
What if the backend decides to add one more nesting? I would choose to go with a recursive function that accounts for that:
function flattenObject(data) {
return Object.fromEntries(
Object.entries(data).flatMap(([key, value]) => {
if (Array.isArray(value) || typeof value !== 'object') {
// The condition might need to be changed depending on the expected data types
return [[key, value]];
}
return Object.entries(flattenObject(value))
.map(([suffix, nestedValue]) => [`${key}.${suffix}`, nestedValue]);
})
)
}
This works even for inputs such as:
{
query1: {
nested: {
test: true
}
},
query2: [1, 2, 3]
}
The above example results in:
{
"query1.nested.test": true,
"query2": [1,2,3]
}

Filter an observed array with a observable condition

I have a request, which returns an array of objects. Each object includes an id, with which I send another request. Based on this result I want to filter the array. Simplified example:
function getAllObjects(): Observable<{ id: number }[]> {
return of([
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
]);
}
function checkObject(obj): Observable<boolean> {
return of(obj.id % 2 === 0);
}
getAllObjects().pipe(
// TODO
).subscribe(console.log); // I only want to see objects here which passed the async check
Does that work for you?
getAllObjects().pipe(
flatMap((ar) => ar),
concatMap((obj) => combineLatest([of(obj), checkObject(obj)])),
filter(([_, checkResult]) => checkResult),
map(([obj]) => obj),
toArray(),
).subscribe(console.log);
Edit, I see you already found a solution, mine isn't much simpler, and I thought you wanted a stream of objects rather than return them as an array. So I added toArray in my Edit.
A solution that may not be the simplest but it also works
getAllObjects()
.pipe(
switchMap(array =>
combineLatest(array
.map(obj =>
checkObject(obj)
.pipe(
distinctUntilChanged(),
map(boolean => boolean ? obj : null)
)
)
)
),
map(array => array.filter(obj => obj))
)
.subscribe(console.log);
Taking into account possible changes in real time
function getAllObjects(): Observable<{ id: number }[]> {
return timer(0, 10000)
.pipe(
map(() => [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
])
);
}
function checkObject(obj): Observable<boolean> {
return timer(1000, 5000)
.pipe(
map(() => obj.id % 2 === 0)
);
}
const objects$ = getAllObjects().pipe(concatAll());
const objectValidations$ = objects$.pipe(concatMap(checkObject));
zip(objects$, objectValidations$).pipe(
filter(([, validated]) => validated),
map(([obj,]) => obj),
toArray()
).subscribe(console.log);
UPDATE:
We can improve the above solution performance-wise by parallelizing the "checks":
getAllObjects().pipe(
concatAll(),
mergeMap(obj =>
checkObject(obj).pipe(
map(isValid => isValid? obj : undefined),
filter(Boolean),
)
),
toArray()
)
That's better because if we assume the following implementation of checkObject (added a delay):
function checkObject(obj) {
return of(obj.id % 2 === 0).pipe(delay(1000));
}
Then for n objects, the previous solution takes n seconds, as opposed to 1 second with the updated solution
import { map, filter } from 'rxjs/operators';
map(items => items.filter(item => item.id % 2 === 0)),
filter(items => items && items.length > 0)
First use the map function and filter the array like normal.
Then to make sure you don't get null or empty arrays, use the filter function which won't call the subscription if the map is null or empty.

RXJS - merge two stream results into one sorted array

I have two streams. Let's say:
const firstStream = Rx.of([
{
first: 'first',
}, {
third: 'third',
}
]);
const secondStream = Rx.of([
{
second: 'second'
}, {
fourth: 'fourth'
}
]);
Now I want a stream that combines the result of these two streams and maps the result array sorted as follows:
const resultArr = [
{
first: 'first',
},
{
second: 'second'
},
{
third: 'third',
},
{
fourth: 'fourth'
}
];
I tried to use combineLatest with RxJS flatmap operator, but that did not work out. I provided a stackblitz playground to test around: StackBlitz
I'm sure there are plenty of ways to do this. Maybe someone can help me out :)
const { from, merge } = rxjs;
const { reduce, map, mergeMap } = rxjs.operators
const a = from(['first', 'third']);
const b = from(['second', 'fourth']);
const sortMap = {
first: 0,
second: 1,
third: 2,
fourth: 4,
}
merge(a, b).pipe(
// wait until every observable has completed,
// zip all the values into an array
reduce((res, item) => res.concat(item), []),
// sort the array accordingly to your needs
map(list => list.sort((a, b) => sortMap[a] - sortMap[b])),
// flatten the array into a sequence
mergeMap(list => list),
).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js" integrity="sha256-mNXCdYv896VtdKYTBWgurbyH+p9uDUgWE4sYjRnB5dM=" crossorigin="anonymous"></script>
enter code hereAs you said your streams first complete and after that, you need the sorted value as single output of the stream, so I would recommend the forkJoin operator, which operator will Wait for Observables to complete and then combine last values they emitted.
const { of, forkJoin } = rxjs;
const { map } = rxjs.operators;
let a$ = of([1, 8, 10, 4]);
let b$ = of([3, 5, 43, 0]);
forkJoin(a$, b$)
.pipe(
map(([a, b]) => [...a, ...b]),
map(x => x.sort((a, b) => a - b))
)
.subscribe(x => {
console.log('Sorted =>', x);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js" integrity="sha256-mNXCdYv896VtdKYTBWgurbyH+p9uDUgWE4sYjRnB5dM=" crossorigin="anonymous"></script>

mapping one object to another using some functional programming

I would like to map one array of object into another in a more functional style, I am using typescript.
Basically I am using delete to remove a property on a object, I would like to know if there is a better way to write it.
const data = props.data.map(d => ({
order: d.position,
logs: d.batches.map(b => {
let log= {
amount: b.scrap,
batchNumber: '', // NO GOOD
}
if (!b.batch || b.batch.length === 0) {
delete log.batchNumber // NO GOOD
}
return log
}),
}))
example input data:
const data = [
position: 1,
batches: [
{batchNumber: '', ammount: 3}
]
]
result:
const data = [{
order: 1,
logs:[ {ammount:3}]
}
]
You can do another map on the batches to return a new array of objects, and attach that to your returned object instead:
const out = data.map(({ position: order, batches }) => {
const logs = batches.map(({ batchNumber, ammount }) => {
if (batchNumber) return { batchNumber, ammount };
return { ammount };
});
return { order, logs }
});
DEMO
One approach would be to make a shallow copy of the target omitting keys you want to delete, for example:
let drop = key => obj => Object.keys(obj).reduce((r, k) =>
k === key ? r : {...r, [k]: obj[k]}, {});
let test = [
{foo:11, bar:2, baz: 3},
{foo:22, bar:2, baz: 3},
{foo:33, bar:2, baz: 3},
];
console.log(test.map(drop('bar')));
To add another option to the mix: it is possible to use Object.assign to optionally assign the property:
const data = [{
position: 1,
batches: [{batchNumber: '',ammount: 3}, {batchNumber: 'withNr',ammount: 4}]
}];
const res = data.map(d =>
({
order: d.position,
logs : d.batches.map(({ammount, batchNumber}) => Object.assign({ammount}, batchNumber ? {batchNumber} : null ))
})
);
console.log(res);

Categories

Resources