Reactive JS (RxJs) Group by key - rxjs

I have a data stream with e.g. the following values:
Observable.of(
[{time: 1000, a: 100},
{time: 1000, b: 100},
{time: 2000, a: 200}]
);
And need to merge the values based on time to get:
[{time: 1000, a: 100, b: 100},
{time: 2000, a: 200}]
I can use map and reduce but then I end up with a single map that I have to split somehow again. Is there a more straight forward way in RxJs?

You could just do an array reduce inside of a map operator. Might be a bit clearer than the groupBy and flatMap. This is more of a data mapping issue than an rxjs issue.
Rx.Observable.of(
[{time: 1000, a: 100},
{time: 1000, b: 100},
{time: 2000, a: 200}]
).map(data => {
return data.reduce((acc, cur) => {
const index = acc.findIndex(x => x.time === cur.time);
if (index >= 0) {
acc[index] = { ...acc[index], ...cur };
} else {
acc.push(cur);
}
return acc;
}, [])
})
.subscribe(x => { console.log('result', x); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.7/Rx.min.js"></script>

I got this in the end:
Observable.of(
[{time: 1000, channelKey: 'a', value: 100},
{time: 1000, channelKey: 'b',value: 100},
{time: 2000, channelKey: 'a', value: 200}]
)
.flatMap<any[], any>(x => x)
.groupBy(v => Math.floor(v.time.getTime() / 1000), v => {
return {[v.channelKey]: v.value}
})
.flatMap((group$) => group$.reduce((acc, cur) => Object.assign(cur, acc), {time: group$.key}))
.toArray()
.subscribe((v) => {
console.log("Value: ", v)
})

Related

How to call "defaultIfEmpty" when list is empty on RxJS?

I've two lists with two distinct objects that need to be converted into the same type, the "second" list will be used only if the "first" list is empty, I tried to use the method defaultIfEmpty but it never return the second option.
const first = []; // could be [{code: 1}, {code: 2}]
const second = [{id: 1}, {id: 2}]
of(first).pipe(
map((value) => {number: value.code})
).pipe(
defaultIfEmpty(of(second).pipe(map((value) => {number: value.id})))
).subscribe(doSomething);
The desired output is:
[{number: 1}, {number: 2}]
On the example above, the map from defaultIfEmpty is never called;
how can I "switch" to another method source if the given source is empty?
will subscribe method be called after the map is complete, or it will be called for each item on map?
like this ?
const first = []; // could be [{code: 1}, {code: 2}]
const second = [{id: 1}, {id: 2}];
of(first).pipe(
filter(({length}) => length > 0),
defaultIfEmpty(second),
map((arr) => arr.map((x) => ({number: x.code ?? x.id})))
).subscribe(...);
If that's an option just create the right observable at runtime:
const makeObservable =
(arr1, arr2) =>
from(arr1.length ? arr1 : arr2)
.pipe(map(({code, id}) => ({number: code ?? id})));
const obs1$ = makeObservable([], [{id:1},{id:2}]);
const obs2$ = makeObservable([{code:2},{code:3}], []);
obs1$.subscribe(o => console.log(o));
obs2$.subscribe(o => console.log(o));
<script src="https://unpkg.com/rxjs#%5E7/dist/bundles/rxjs.umd.min.js"></script>
<script>
const {from} = rxjs;
const {map} = rxjs.operators;
</script>
const list1: { code: number }[] = [];
const list2 = [{ id: 1 }, { id: 2 }];
of(list1)
.pipe(
map((aList) => aList.map((v) => ({ 'number': v.code }))),
map((listModified) => {
return listModified?.length > 0
? listModified
: list2.map((value) => ({ number: value.id }));
})
)
.subscribe(console.log);
Don't get confused with map, one of them is from rxjs, the other is a function for arrays. In your first map, you are mapping the the whole array, not each element.
subscribe will be called when everything completes within the pipe.

Object3D.lookAt() in instanced meshes not as expected?

I'm building a particle system in which particles have directions they are pointing at and I want to reflect that in an instance mesh I use to present them, but something is not quite working... It seems that the center of rotation and/or the up axis are not quite right, and despite me console logging stuff I could not find the problem. Here's the code:
const Particles = (particleSystem: TparticleSystem) => {
const mesh = useRef<THREE.InstancedMesh>(null);
const light = useRef<THREE.PointLight>(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
const pyramid = useMemo(() => {
return {
vertices: [1, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, -1, 0, 2, 0],
indices: [0, 1, 2, 2, 3, 0, 0, 4, 1, 1, 4, 2, 2, 4, 3, 3, 4, 0],
};
}, []);
useFrame(() => {
if (particleSystem !== undefined) {
particleSystem.update();
particleSystem.move();
particleSystem.particles.forEach((particle: Tparticle, index: number) => {
dummy.position.set(particle.pos.x, particle.pos.y, particle.pos.z);
dummy.lookAt(vec().copy(particle.pos).add(particle.dir)); // rotating weird
dummy.scale.set(1, 2, 1);
dummy.updateMatrix();
// And apply the matrix to the instanced item
if (mesh.current) mesh.current.setMatrixAt(index, dummy.matrix);
});
if (mesh.current) mesh.current.instanceMatrix.needsUpdate = true;
}
});
return (
<>
<pointLight ref={light} distance={40} intensity={3} color="lightblue" />
{particleSystem !== undefined && (
<instancedMesh ref={mesh} args={[, , particleSystem.num]}>
<polyhedronBufferGeometry
args={[pyramid.vertices, pyramid.indices, 1, 0]}
/>
<meshBasicMaterial
color="#2596be"
wireframe={Math.random() > 0.5 ? true : false}
/>
</instancedMesh>
)}
</>
);
};
I feel like maybe this is something to do with local and world coordinates, but I don't quite understand it.
Oh! and this vec stuff is just
const vec = function (x = 0, y = 0, z = 0) {
return new THREE.Vector3(x, y, z);
};

rxjs: extract slice of a collection given start and end matching rules

I'm trying to write a reactive function with rxjs that, given a potentially infinite array:
Rule 1: Skip initial null items
Rule 2: Extract the items between two '*' appearances
Rule 3: If first item after nulls is not an '*', must fail (or return an empty array)
Rule 4: Process no more than N items
Rule 5: If there's no a second '*', must fail (or return an empty array)
So, with N = 10:
Case 1: [null, null, '*', 1, 2, 3, '*', 4, 5] -> [1, 2, 3]
Case 2: [null, null, 1, '*', 2, 3, '*', 4, 5] -> [] // Breaks rule 3
Case 3: [null, null, '*', 1, 2, 3, 4, 5, 6, 7, 8, '*'] -> [] // Breaks rule 5 (second * is at position > N)
For the case 1, there's no problem. But I don't find the set of operator to enforce the rules 3 and 5
This example illustrates the problem:
const { from } = require('rxjs');
const { take, takeWhile, skipWhile, toArray } = require('rxjs/operators');
function *infinite(items) {
for (let i = 0; ; i++) {
yield i < items.length ? items[i] : `fake${i}`
}
}
const extract = ({
source,
limit = 10,
}) => new Promise(resolve => {
source
.pipe(...[
take(limit),
skipWhile(item => item === null),
skipWhile(item => item === '*'),
takeWhile(item => item !== '*'),
toArray(),
])
.subscribe(result => {
resolve(result)
})
})
;(async () => {
console.log(await extract({ source: from(infinite([null, '*', 1, 2, 3, '*', 4, 5, 6])) }))
console.log(await extract({ source: from(infinite([null, 'a', '*', 1, 2, 3, '*', 4, 5, 6])) }))
console.log(await extract({ source: from(infinite([null, '*', 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12])) }))
})()
Edit: I realized the operation:
skipWhile(item => item === '*'),
is not accurate. Should be something like
skipThisSingleItemIfMatchAsteriskOtherwiseFail
A possible solution for your problem is the following. Comments are inline
function extract(c: any[], n: number) {
// first you create an src stream where all the leading nulls are removed
const src = from(c).pipe(
// just take the first n elements
take(n),
// filter to remove the nulls - this can be a problem if you have nulls between the 2 '*'
filter((item) => item !== null),
// share is used to avoid having more than one subscription to this stream
share()
);
const core = src.pipe(
// here we consider all elements until the second '*' is met
// And what about the first '*'? see the rest of the code, there is the explanation
takeWhile((item) => item !== "*", true),
// create an array which cumulates all the values received until the stream completes
toArray(),
// if the array of all elements received is empty or if the last element is not a '*' return []
// else return the elements received a part the last '*'
map((arr) => {
return arr.length === 0
? []
: arr[arr.length - 1] !== "*"
? []
: arr.slice(0, arr.length - 1);
})
);
// this is the stream returned by the extract function
// it starts from the src stream we have created above
return src.pipe(
// the first element is taken
// since the src stream is shared between this stream and the stream we have called "core" and have built above
// then it means that the first element is consumed here and will not be encountered in the "core" stream
first(),
// if the first element is not a '*' an error is thrown
tap((d) => {
if (d !== "*") {
throw new Error("First not null val is not *");
}
}),
// if no error is thrown then we return the stream "core"
concatMap((firstItem) => {
return core;
}),
// if an error is thrown then we return an Observable which emits []
catchError((e) => of([]))
);
}
In order to use this function you can write the following code
const resp = extract(source, 10);
resp.subscribe((d) => {
// do stuff with the result, for instance
console.log(d);
});
Here a stackblitz that reproduces this logic
How about something with custom RxJS operators? Try the following
const { Observable, from } = rxjs;
const { take, filter, reduce } = rxjs.operators;
const onlyIfFirst = predicate => {
let first = true;
return source =>
new Observable(subscriber =>
source.subscribe({
next(value) {
if (first) {
first = false;
if (predicate(value)) {
subscriber.next(value);
} else {
subscriber.next([]);
subscriber.complete();
}
} else {
subscriber.next(value);
}
},
complete() {
subscriber.complete();
}
})
);
};
const toArrayWhen = (predicate, count) => {
let id = 0;
let times = count * 2;
return source =>
source.pipe(
reduce((acc, curr) => {
if (!!id) {
if (predicate(curr)) id++;
if (id < times && !predicate(curr)) {
acc = [...acc, curr];
}
} else {
if (predicate(curr)) id++;
}
return acc;
}, [])
);
};
const input = [null, null, '*', 1, 2, 3, '*', 3, '*', 4, '*'];
from(input)
.pipe(
take(10),
filter(value => value !== null),
onlyIfFirst(value => value === '*'),
toArrayWhen(value => value === '*', 1)
)
.subscribe({
next: value => console.log('Next:', value),
error: error => console.log('Error:', error),
complete: () => console.log('Complete')
});
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>
Note: I'm fairly certain the count variable behavior for > 1 is buggy at the moment. But as long as you only need the first instead of values between two asterisks *, it should be find.

Handsontable cross calculations

I have a handontable demo.
document.addEventListener("DOMContentLoaded", function() {
var
example = document.getElementById('example1'),
hot1;
hot1 = new Handsontable(example, {
data: [
['', '', '', ''],
[1, 2, 3, '=SUM(A2:C2)'],
[1, 2, 3],
],
width: 584,
height: 320,
rowHeaders: true,
formulas: true,
colHeaders: true,
columns: [1, 2, 3, 4],
columnSummary: function () {
var summary = [];
for (var i = 0; i < 4; i++) {
summary.push({
ranges: [[1, 2]],
destinationRow: 0,
destinationColumn: i,
type: 'sum',
forceNumeric: true,
sourceColumn: i
});
}
return summary;
}
});
});
It caclulates:
Sum of column and puts a result in the first raw.
Sum of rows (except first one) and puts it in the column "D"
I need to calculate correct total of the totals, which is the cell D1.
After loading and changing any cell calculation of D1 has to work properly.
Thank you for any ideas.
The option columnSummary should not be applied on the 4th column (the column of SUM results). Try to apply you code of columnSummary option only for the first three columns :
for (var i = 0; i < 3; i++) //Instead of i < 4
And use in the row one what you use to calculate the sum on your other rows :
data: [
['', '', '', '=SUM(A1:C1)'],
[1, 2, 3, '=SUM(A2:C2)'],
[1, 2, 3, '=SUM(A3:C3)'],
],
You will see that it works like a charm : JSFiddle.

Deep Diff Test Failures in Karma with Mocha

I am running the following test:
describe("objects", function () {
it("should equal", function () {
var a = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
x: 3
}
}
};
var b = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
x: 4
}
}
};
a.should.deep.equal(b);
});
});
The test fails as expected, but the error message is not at all helpful.
AssertionError: expected { Object (a, b, ...) } to deeply equal { Object (a, b, ...) }
How would I get it so that it outputs a prettified json comparison instead?
Libraries I am currently using:
karma 0.12.1
karma-mocha 0.1.6
karma-mocha-reporter 0.3.0
karma-chai 0.1.0
You can change where the message gets truncated with the following:
chai.config.truncateThreshold = 0
So for your example:
var chai = require('chai');
var expect = chai.expect;
chai.config.truncateThreshold = 0;
describe.only("objects", function () {
it("should equal", function () {
var a = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
x: 3
}
}
};
var b = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
x: 4
}
}
};
expect(a).to.deep.equal(b);
});
});
Which will result in:
AssertionError: expected { a: 1, b: 2, c: { a: 1, b: 2, c: { a: 1, b: 2, x: 3 } } } to deeply equal { a: 1, b: 2, c: { a: 1, b: 2, c: { a: 1, b: 2, x: 4 } } }
+ expected - actual
"b": 2
"c": {
"a": 1
"b": 2
+ "x": 4
- "x": 3
}
}
}
The mocha reporter has an option for this: showDiff: true
You can stick this in your karma config:
config.set({
frameworks: [ 'mocha', 'chai', ... ],
plugins: [
require('karma-mocha'),
require('karma-chai'),
require('karma-mocha-reporter'),
...
],
reporters: [ 'mocha', ... ],
mochaReporter: {
showDiff: true, // <-- This!
},
...
});
You can use assert-diff library to show only the difference when test fails.
var assert = require('assert-diff');
describe("objects", function () {
it("should equal", function () {
var a = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
x: 3
}
}
};
var b = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
x: 4
}
}
};
assert.deepEqual(a, b);
});
});
this will give you an error with the difference
1) objects should equal:
AssertionError: { a: 1, b: 2, c: { a: 1, b: 2, c: { a: 1, b: 2, x: 3 } } } deepEqual { a: 1, b: 2, c: { a: 1, b: 2, c: { a: 1, b: 2, x: 4 } } }
{
c: {
c: {
- x: 4
+ x: 3
}
}
}

Resources