0

Let's say I have this array of observables:

animals$ = from([ 
   { animal: 'rat', color: 'grey', speed: 2 },
   { animal: 'rat', color: 'black', speed: 3 },
   { animal: 'dog', color: 'grey', speed: 2 },
   { animal: 'dog', color: 'black', speed: 1 },
   { animal: 'cat', color: 'grey', speed: 5 },
   { animal: 'cat', color: 'black', speed: 1 },
]);

I want to get an observable array of the following format, where the results have been grouped by animal type, sorted alphabetically by animal, and the color values are transformed to keys for speed values:

[ 
   { animal: 'cat', grey: 5, black: 1 },
   { animal: 'dog', grey: 2, black: 1 },
   { animal: 'rat', grey: 1, black: 3 },
]

Is it possible by using groupBy? Most examples I have found so far are quite different, where in the results are in an array rather than combined to make key/value pairs.

пaean
  • 61
  • 8

2 Answers2

3

IMO better fit for this scenario would be the RxJS reduce operator + Array#findIndex function to group the objects and RxJS map operator + Array#sort function to sort the elements of the array.

Try the following

const { from } = rxjs;
const { map, reduce } = rxjs.operators;

const animals$ = from([ { animal: 'rat', color: 'grey', speed: 2 }, { animal: 'rat', color: 'black', speed: 3 }, { animal: 'dog', color: 'grey', speed: 2 }, { animal: 'dog', color: 'black', speed: 1 }, { animal: 'cat', color: 'grey', speed: 5 }, { animal: 'cat', color: 'black', speed: 1 }, ]);

animals$.pipe(
  reduce((acc, curr) => {
    const idx = acc.findIndex((item) => item.animal === curr.animal);
    if (idx !== -1) {
      acc[idx] = {
        ...acc[idx],
        [curr.color]: curr.speed,
      };
    } else {
      acc = [
        ...acc,
        {
          animal: curr.animal,
          [curr.color]: curr.speed,
        },
      ];
    }
    return acc;
  }, []),
  map((animals) =>
    animals.sort((a1, a2) =>
      a1.animal > a2.animal ? 1 : a1.animal < a2.animal ? -1 : 0
    )
  )
).subscribe(console.log);
.as-console-wrapper { max-height: 100% !important; top: 0px }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>

Working example: Stackblitz

ruth
  • 29,535
  • 4
  • 30
  • 57
  • Yes this solves it! Thanks a lot – пaean Nov 19 '21 at 13:36
  • the only thing I needed to add was `mergeMap((res) => res),` before reduce – пaean Nov 19 '21 at 13:38
  • @mponny: I don't actually see the need for `mergeMap((res) => res)` before `reduce` in the example you've provided. Indeed the Stackblitz example works without it. But if each element of the array is an observable, then you would need it. – ruth Nov 19 '21 at 13:48
  • 1
    I am quite new to observables, did not realize the difference between this example and my actual implementation (I was not able to post this due to customer confidentiality). Thanks for explaining! – пaean Nov 19 '21 at 14:17
0

Another solution would be the following:

of([
  { animal: 'rat', color: 'grey', speed: 2 },
  { animal: 'rat', color: 'black', speed: 3 },
  { animal: 'dog', color: 'grey', speed: 2 },
  { animal: 'dog', color: 'black', speed: 1 },
  { animal: 'cat', color: 'grey', speed: 5 },
  { animal: 'cat', color: 'black', speed: 1 },
])
  .pipe(
    map((result) => {
      let res: { animal: string; grey: number; black: number }[] = [];

      const animals = new Set(result.map((x) => x.animal));

      animals.forEach((animal) => {
        const grey: number = result
          .filter((x) => x.animal === animal && x.color === 'grey')
          .map((x) => x.speed)
          .reduce((x, y) => x + y);

        const black = result
          .filter((x) => x.animal === animal && x.color === 'black')
          .map((x) => x.speed)
          .reduce((x, y) => x + y);

        res.push({
          animal,
          grey,
          black,
        });
      });

      return res;
    })
  )

Working example: Stackblitz

StPaulis
  • 2,844
  • 1
  • 14
  • 24
  • `of` creates a very different flow to `from` which is in the question. The colors in the example are likely to be dynamic, so `grey` and `black` constants wouldn't suffice – Drenai Nov 19 '21 at 23:16
  • Yes, I am aware for both and I agree that the answer above is the best solution. I only let this here because in the question, it doesn't have the requirement to be dynamic so I think that I introduce a more beginner friendly solution. – StPaulis Nov 22 '21 at 11:12