Home > Net >  Assure all ids are unique with RxJS Observable pipe
Assure all ids are unique with RxJS Observable pipe

Time:07-15

I have an observable that I'd like to modify before it resolves, either using a map pipe or something similar to ensure that all ids within the groups array are unique. If cats is encountered twice, the second occurrence should become cats-1, cats-2 etc. These fields are being used to populate a HTML id attribute so I need to ensure they are always unique.

{ 
  title: 'MyTitle',
  description: 'MyDescription', 
  groups: [
     {
        id: 'cats',
        title: 'SomeTitle'
     },
     {
        id: 'dogs',
        title: 'SomeTitle'
     },
     {
        id: 'octupus',
        title: 'SomeTitle'
     },
     {
        id: 'cats',
        title: 'SomeTitle'
     },
  ]

}

Using an RxJs observable my code looks like the following:

getGroups() { 
  return this.http.get(ENDPOINT_URL)
}

I was able to achieve this using a map operator with a set but part of me feels like this isn't the correct pipe for this as the array is nested.

getGroups() { 
  return this.http.get(ENDPOINT_URL).pipe(
     map(data => {
        const groupIds = new Map();

        data.groups.map(group => {
           if (!groupIds.get(group.id)) {
              groupIds.set(group.id, 1)
           } else {
             const updatedId = (groupIds.get(group.id) || 0)   1;

             groupIds.set(group.id, updatedId);
             group.id = `${group.id}-${updatedId}`
           }

           return group
        }

        return data;
     }
  )
}

Is there a more efficient way to make this operation using a more appropriate pipe? I am worried this can become quite inefficient and significantly delay rendering of content while the observable resolves the conflicts. As of today I am unable to modify the actual content returned from the API so that is not an option unfortunately.

CodePudding user response:

You could try something like this:

import { of, map } from 'rxjs';
import { findLastIndex } from 'lodash';

of({
  title: 'MyTitle',
  description: 'MyDescription',
  groups: [
    {
      id: 'cats',
      title: 'SomeTitle',
    },
    {
      id: 'dogs',
      title: 'SomeTitle',
    },
    {
      id: 'cats',
      title: 'SomeTitle',
    },
    {
      id: 'octupus',
      title: 'SomeTitle',
    },
    {
      id: 'cats',
      title: 'SomeTitle',
    },
  ],
})
  .pipe(
    map((data) => ({
      ...data,
      groups: data.groups.reduce((acc, group) => {
        const lastElementIndex = findLastIndex(acc, (accGroup) => accGroup.id.startsWith(group.id));
        
        if (lastElementIndex === -1) {
          return [...acc, group];
        }

        const lastElement = acc[lastElementIndex];
        const lastNameNumerator = lastElement.id.split('-')[1];

        return [
          ...acc,
          {
            ...group,
            id: `${group.id}-${lastNameNumerator ?  lastNameNumerator   1 : 1}`,
          },
        ];
      }, []),
    }))
  )
  .subscribe(console.log);

Stackblitz: https://stackblitz.com/edit/rxjs-kcxdcw?file=index.ts

CodePudding user response:

If the only requirement is to have the ids be unique, you could ensure uniqueness by appending the array index to each element's id.

getGroups() { 
  return this.http.get(ENDPOINT_URL).pipe(
    map(data => {
      const groups = data.groups.map(
        (g, i) => ({...g, id: `${g.id}-${i}`})
      );
      
      return { ...data, groups };
    })
  );
}

Output of groups:

// groups: Array[5]
//   0: Object
//     id    : "cats-0"
//     title : "SomeTitle"
// 
//   1: Object
//     id    : "dogs-1"
//     title : "SomeTitle"
// 
//   2: Object
//     id    : "cats-2"
//     title : "SomeTitle"
// 
//   3: Object
//     id    : "octupus-3"
//     title : "SomeTitle"
// 
//   4: Object
//     id    : "cats-4"
//     title : "SomeTitle"

Here's a little StackBlitz.

CodePudding user response:

Honestly what you have is probably fine. Here's another method that's slightly simpler. It first uses reduce to create an object literal of groups. If you were open to external dependencies you could use Ramda's groupWith function to produce the same result. Then it uses flatMap to flatten the groups. If there is only one item in the array then it is returned as is, otherwise the elements are mutated with the new ids.

getGroups() { 
  return this.http.get(ENDPOINT_URL).pipe(
    map(data => Object.values(
         data.groups.reduce((acc, cur) => {
           (acc[cur.id] || (acc[cur.id] = [])).push(cur);
           return acc;
         },
         {} as Record<string | number, [] as GroupType[])
       ).flatMap(grp => (grp.length === 1)
         ? grp
         : grp.map((x, i) => ({ ...x, id: `${x.id}-${i   1}`)))
  )
}

CodePudding user response:

Another one

map((data:any) => {

  //create an array in the way [{id:"cats",data:[0,3]}{id:"dogs",data:[1]..]
  const keys=data.groups.reduce((a:any,b:any,i:number)=>{
    const el=a.find(x=>x.id==b.id)
    if (el)
      el.data=[...el.data,i]
    else
      a=[...a,({id:b.id,data:[i]})]
    return a
  },[])

  //loop over groups, if keys.data.length>1 ...
  data.groups.forEach((x,i)=>{
    const el=keys.find(key=>key.id==x.id)
    if (el.data.length>1)
      x.id=x.id '-' (el.data.findIndex(l=>l==i) 1)
  })
  return data;
})

Or

map((data:any) => {

  //create an object keys {cats:[0,3],dogs:[1]....
  const keys=data.groups.reduce((a:any,b:any,i:number)=>{
    if (a[b.id])
      a[b.id]=[...a[b.id],i]
    else
      a[b.id]=[i]
    return a
  },{})

  //loop over groups, if keys[id].length>0 ...
  data.groups.forEach((x,i)=>{
    if (keys[x.id].length>1)
      x.id=x.id '-' (keys[x.id].findIndex(l=>l==i) 1)
  })
  return data;
})
  • Related