Reputation: 6900
I have questions$
observable which returns more than 10
question objects from Firestore
.
this.questions$ = this.moduleCollection$.doc(module.id)
.collection('questions').valueChanges();
Now i want to limit the result with 10
questions randomly.
I can limit the query like this
this.questions$ = this.moduleCollection$.doc(module.id)
.collection('questions',ref => ref.limit(10)).valueChanges();
But i don't know how to get it randomly, is there any Rxjs
operators do so?
What i have tried (Extending @Richard Matsen's answer)
const sampleSize = 2
const randomIndex = (array) => Math.floor(Math.random() * array.length)
const addIndexes = (array) => array.map((item, index) => {
item['id'] = index
return item
})
const removeIndexes = (array) => array.map(item => {
delete item.id
return item
})
this.questions$ =
this.moduleCollection$.doc(module.id).collection('questions').valueChanges()
.map(addIndexes)
.map(r => r[randomIndex(r)])
.repeat()
.scan((a, c) => a.map(a => a.id).indexOf(c.id) === -1 ? a.concat(c) : a, [])
.skipWhile(array => array.length < sampleSize)
.take(1)
.map(array => array.sort((a, b) => a.id - b.id))
.map(removeIndexes)
Upvotes: 1
Views: 421
Reputation: 23503
This sample demos a pure rxjs way to take a random sample from an array, with no duplicates.
const source = Rx.Observable.of([{val: 'a'}, {val: 'b'}, {val: 'c'}, {val: 'd'}, {val: 'e'}])
const sampleSize = 2
const randomIndex = (array) => Math.floor(Math.random() * array.length)
const addIndexes = (array) => array.map((item, index) => {
item['id'] = index
return item
})
const removeIndexes = (array) => array.map(item => {
delete item.id
return item
})
const randomSample = (source, sampleSize) =>
source
.map(addIndexes)
.map(r => r[randomIndex(r)])
.repeat()
.scan((a, c) => a.map(a => a.id).indexOf(c.id) === -1 ? a.concat(c) : a, [])
.skipWhile(array => array.length < sampleSize)
.take(1)
.map(array => array.sort((a, b) => a.id - b.id))
.map(removeIndexes)
randomSample(source, sampleSize).subscribe(console.log)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
With firestore .valueChanges()
The firestore .valueChanges()
method is designed to push new arrays each time the data changes on the cloud, so this observable source will never complete.
This means that repeat()
never fires. To make it work, we need to wrap the sampling code.
const source = Rx.Observable.of([{val: 'a'}, {val: 'b'}, {val: 'c'}, {val: 'd'}, {val: 'e'}])
const sampleSize = 2
const randomIndex = (array) => Math.floor(Math.random() * array.length)
const addIndexes = (array) => array.map((item, index) => {
item['id'] = index
return item
})
const removeIndexes = (array) => array.map(item => {
delete item.id
return item
})
const randomSample = (source, sampleSize) =>
source
.mergeMap(array =>
Rx.Observable.of(array)
.map(addIndexes)
.map(r => r[randomIndex(r)])
.repeat()
.scan((a, c) => a.map(a => a.id).indexOf(c.id) === -1 ? a.concat(c) : a, [])
.skipWhile(array => array.length < sampleSize)
.take(1)
.map(array => array.sort((a, b) => a.id - b.id))
.map(removeIndexes)
)
randomSample(source, sampleSize).subscribe(console.log)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
Using non-Rx sampling
One thing that stands out in the above code is that we are converting an observable of an array to an array, then back to an inner observable of array (all to get a 'complete' event).
So, it is more practical to work on the array directly.
(Has been tested with firestore .valueChanges()
as well)
const source = Rx.Observable.of([{val: 'a'}, {val: 'b'}, {val: 'c'}, {val: 'd'}, {val: 'e'}])
const sampleSize = 2
const getRandomUniqueSample = (arr, sampleSize) => {
let result = [],
taken = {};
while (result.length < Math.min(sampleSize, arr.length)) {
let x = Math.floor(Math.random() * arr.length);
if (!(x in taken)) {
result.push(arr[x])
taken[x] = null;
}
}
return result;
}
const randomSample = (source, sampleSize) =>
source
.mergeMap(array =>
Rx.Observable.of(getRandomUniqueSample(array, sampleSize))
)
randomSample(source, sampleSize).subscribe(console.log)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>
Upvotes: 1