Search code examples
angulartypescriptrxjsngrx-store

Angular/ngrx - Group and aggregate values from state


I am trying to figure out how to get a grouped and aggregated array of values (that I can use with ngFor) starting from a list of objects, and for the life of me I can't make it work. The data (which is a slice of my state) looks something like this:

[{name: "A", value: 1, desc: 'something irrelevant'}, 
 {name: "A", value: 3, desc: 'also other properties'}, 
 {name: "B", value: 2, desc: 'etc.'}, 
 {name: "B", value: 5, desc: 'etc.'}]

And the result I'm trying to get is something like (note that the type is different):

[{name: "A", value: 4}, {name: "B", value: 7}]

So, basically I want to find the distinct "names" and the sum of "value" for all the objects that have that name, with an output that can be used by ngFor | async.

My almost working solution to get the distinct values, at the moment, is:

       this.aggregates:Observable<any[]> = this.store
        .select(state => state.valuesList)
        .map(valuesList => valuesList.sort((a,b) => {return (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0);} }))
        .flatMap(valuesList => valuesList)
        .map(value => value.name)
        .distinct();

I'd be happy to start with this; the problem is that, if I don't add a toArray(), Typescript complains about "Type string is not assignable to type any[]"; if I add toArray() after distinct(), it doesn't complain anymore but a subscribe() yields no result.

What am I doing wrong? Should I move everything to the reducer (but then I don't know if I can change the type of objects returned by different actions in the same reducer)? Any help is very very much appreciated.

UPDATE: I'd be even happier to have a working groupBy() implementation, since it should be exactly its use case.


Solution

  • You can use groupBy to do what you want, but you have to use the groupBy operator on an observable derived from the list - as groupBy expects an observable of items.

    In the following snippet, slice is equivalent to this.store.select(state => state.valuesList)

    const slice = Rx.Observable.of([
      { name: "A", value: 1, desc: "something irrelevant" },
      { name: "A", value: 3, desc: "also other properties" },
      { name: "B", value: 2, desc: "etc." },
      { name: "B", value: 5, desc: "etc." }
    ]);
    
    const grouped = slice.concatMap(list => Rx.Observable
      .from(list)
      .groupBy(item => item.name)
      .mergeMap(group => group
        .reduce((total, item) => total + item.value, 0)
        .map(total => ({ name: group.key, value: total }))
      )
      .toArray()
    );
    
    grouped.subscribe(value => console.log(value));
    .as-console-wrapper { max-height: 100% !important; top: 0; }
    <script src="https://unpkg.com/rxjs@5/bundles/Rx.min.js"></script>