Home > Blockchain >  Add new record to each category only once
Add new record to each category only once

Time:12-09

I have an array of catalogs where catId could be repeated.

Each catalog has a unique segmentId

const catalogs = [
  { catID: 2, segmentId: '2', segmentName: 'S2' },
  { catID: 1, segmentId: '1', segmentName: 'S1' },
  { catID: 1, segmentId: '2', segmentName: 'S2' }
];

Means against each catId there could be multiple segmentIds but segmentIds can not be duplicated for that specific catId

Now I want to assign an array of segments to the main catalogs array:

e.g here is an example array of segments that I need to assign:

[{id: '3', name: 'S3'}]

I am expecting this response:

[
  { catID: 2, segmentId: '2', segmentName: 'S2' },
  { catID: 2, segmentId: '3', segmentName: 'S3' },
  { catID: 1, segmentId: '1', segmentName: 'S1' },
  { catID: 1, segmentId: '2', segmentName: 'S2' },
  { catID: 1, segmentId: '3', segmentName: 'S3' },
]

I also tried to solve this myself, I am very close but not getting the expecting result:

const catalogs = [
  { catID: 2, segmentId: '2', segmentName: 'S2' },
  { catID: 1, segmentId: '1', segmentName: 'S1' },
  { catID: 1, segmentId: '2', segmentName: 'S2' }
];

function addSegmentsToCatalogs(segments){

    let catalogUid = catalogs[0].catID;
    const lastCatalog = catalogs.slice(-1).pop();
    const insertionArray = [];
    
  catalogs.forEach(async (catalog) => {
    if (catalogUid != catalog.catID) {
      segments.forEach((segment) => {
        insertionArray.push({
          catalogId: catalogUid,
          segmentId: segment.id,
          segmentName: segment.name,
        });
      });

      catalogUid = catalog.catID;
    }

    if (!segments.some((segment) => segment.id === catalog.segmentId)) {

      segments.push({ id: catalog.segmentId, name: catalog.segmentName });

      if (
        catalog.catID == lastCatalog.catID &&
        catalog.segmentId == lastCatalog.segmentId
      ) {
        segments.forEach((segment) => {
          insertionArray.push({
            catalogId: catalogUid,
            segmentId: segment.id,
            segmentName: segment.name,
          });
        });
      }
    }
    });
  
  console.log(insertionArray);
  return insertionArray;
}

addSegmentsToCatalogs([{id: '3', name: 'S3'}])

Can anyone help me find the issue or suggest me better approach to solve this?

CodePudding user response:

You could take nested Maps and group by catID and segmentId.

map looks like this:

2:
    '2': { catID: 2, segmentId: '2', segmentName: 'S2' }
1:
    '1': { catID: 1, segmentId: '1', segmentName: 'S1' }
    '2': { catID: 1, segmentId: '2', segmentName: 'S2' }

After having a map with nested maps, you could take key of the outer map and add the new segements to new keys of the inner maps.

Finally take a flat array of all values.

const
    add = (catalogs, segments) => {
        const
            map = catalogs.reduce((m, o) =>  m.set(
                o.catID,
                (m.get(o.catID) || new Map).set(o.segmentId, o)
            ), new Map);

        segments.forEach(({ id: segmentId, name: segmentName }) => 
            map.forEach((mm, catID) => mm.set(catID, { catID, segmentId, segmentName }))
        );

        return Array
            .from(map.values(), m => Array.from(m.values()))
            .flat();
    },
    catalogs = [
        { catID: 2, segmentId: '2', segmentName: 'S2' },
        { catID: 1, segmentId: '1', segmentName: 'S1' },
        { catID: 1, segmentId: '2', segmentName: 'S2' }
    ],
    segments = [{ id: '3', name: 'S3' }],
    result = add(catalogs, segments);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

CodePudding user response:

const catalogs = [{
    catID: 2,
    segmentId: '2',
    segmentName: 'S2'
  },
  {
    catID: 1,
    segmentId: '1',
    segmentName: 'S1'
  },
  {
    catID: 1,
    segmentId: '2',
    segmentName: 'S2'
  }
];

function addSegmentsToCatalogs(segments) {

    const copyOfCatlogs = [...catalogs];

  const catlogIds = catalogs.map(c => c.catID);
  const uniqCatIds = [...new Set(catlogIds)];
  const segIds = segments.map(s => s.id);
  

  uniqCatIds.forEach(catId => {

    segIds.forEach(segId => {
      const hasCatIdWithSegment = copyOfCatlogs.findIndex(c => c.catID == catId && segId == c.segmentId);
      if (hasCatIdWithSegment == -1) {
        const newSegment = segments.find(s => s.id == segId);
        copyOfCatlogs.push({
          catID: catId,
          segmentId: newSegment.id,
          segmentName: newSegment.name,
        });
      }
    })
  })
  
  console.log(copyOfCatlogs);

  return copyOfCatlogs;
}

addSegmentsToCatalogs([{
  id: '3',
  name: 'S3'
}])

CodePudding user response:

Before getting to the answer: Your input data suggests that the pattern of the input segment name string is a capital letter S followed by an integer sequence. If that's the case, a number type would better represent the segmentId value of each object in the output array for your dataset... a number can be stringified using built-in methods with a 100% success rate — meaning that you can easily do that on demand later if you ever need the value as a string, but the reverse is not true: most strings cannot be parsed as numbers.

In context of the above, you can use a transform function to parse each input item into the desired output structure (and validate the values along the way). See the example code below:

TS Playground

class AssertionError extends Error {
  override name = 'AssertionError';
}

function assert (expr: unknown, msg?: string): asserts expr {
  if (!expr) throw new AssertionError(msg);
}

type OutputItem = {
  catId: number;
  segmentId: number;
  segmentName: string;
};

function transform (item: InputItem): OutputItem {
  const segmentName = item.name;

  const catId = Number(item.id);
  assert(Number.isInteger(catId), `Couldn't parse category ID as integer`);

  const segmentId = Number(segmentName.slice(1));
  assert(Number.isInteger(segmentId), `Couldn't parse segment ID as integer`);

  return {
    catId,
    segmentId,
    segmentName,
  };
}

type InputSegmentName = `S${number}`;
type InputId = `${number}`;
type InputItem = { id: InputId; name: InputSegmentName; };

const input = [
  {id: '2', name: 'S2'},
  {id: '2', name: 'S3'},
  {id: '1', name: 'S1'},
  {id: '1', name: 'S2'},
  {id: '1', name: 'S3'},
  {id: '3', name: 'S3'},
] satisfies InputItem[];

const output = input.map(transform);
console.log(output); /* Logs:
[
  {
    "catId": 2,
    "segmentId": 2,
    "segmentName": "S2"
  },
  {
    "catId": 2,
    "segmentId": 3,
    "segmentName": "S3"
  },
  {
    "catId": 1,
    "segmentId": 1,
    "segmentName": "S1"
  },
  {
    "catId": 1,
    "segmentId": 2,
    "segmentName": "S2"
  },
  {
    "catId": 1,
    "segmentId": 3,
    "segmentName": "S3"
  },
  {
    "catId": 3,
    "segmentId": 3,
    "segmentName": "S3"
  }
]
*/

Compiled JS from the TS Playground:

"use strict";
class AssertionError extends Error {
    constructor() {
        super(...arguments);
        this.name = 'AssertionError';
    }
}
function assert(expr, msg) {
    if (!expr)
        throw new AssertionError(msg);
}
function transform(item) {
    const segmentName = item.name;
    const catId = Number(item.id);
    assert(Number.isInteger(catId), `Couldn't parse category ID as integer`);
    const segmentId = Number(segmentName.slice(1));
    assert(Number.isInteger(segmentId), `Couldn't parse segment ID as integer`);
    return {
        catId,
        segmentId,
        segmentName,
    };
}
const input = [
    { id: '2', name: 'S2' },
    { id: '2', name: 'S3' },
    { id: '1', name: 'S1' },
    { id: '1', name: 'S2' },
    { id: '1', name: 'S3' },
    { id: '3', name: 'S3' },
];
const output = input.map(transform);
console.log(output); /* Logs:
[
  {
    "catId": 2,
    "segmentId": 2,
    "segmentName": "S2"
  },
  {
    "catId": 2,
    "segmentId": 3,
    "segmentName": "S3"
  },
  {
    "catId": 1,
    "segmentId": 1,
    "segmentName": "S1"
  },
  {
    "catId": 1,
    "segmentId": 2,
    "segmentName": "S2"
  },
  {
    "catId": 1,
    "segmentId": 3,
    "segmentName": "S3"
  },
  {
    "catId": 3,
    "segmentId": 3,
    "segmentName": "S3"
  }
]
*/


Update in response to your comment: If your actual dataset is different (e.g. segment names aren't all in the format you showed in your question) such that the above observation doesn't apply, then — of course — you don't have to parse the segment identifier as a number:

Full code in the TS Playground

type OutputItem = {
  catId: number;
  segmentId: string;
  segmentName: string;
};

function transform (item: InputItem): OutputItem {
  const segmentName = item.name;

  const catId = Number(item.id);
  assert(Number.isInteger(catId), `Couldn't parse category ID as integer`);

  const segmentId = segmentName.slice(1);

  return {
    catId,
    segmentId,
    segmentName,
  };
}

Compiled JS from the TS Playground:

"use strict";
class AssertionError extends Error {
    constructor() {
        super(...arguments);
        this.name = 'AssertionError';
    }
}
function assert(expr, msg) {
    if (!expr)
        throw new AssertionError(msg);
}
function transform(item) {
    const segmentName = item.name;
    const catId = Number(item.id);
    assert(Number.isInteger(catId), `Couldn't parse category ID as integer`);
    const segmentId = segmentName.slice(1);
    return {
        catId,
        segmentId,
        segmentName,
    };
}
const input = [
    { id: '2', name: 'S2' },
    { id: '2', name: 'S3' },
    { id: '1', name: 'S1' },
    { id: '1', name: 'S2' },
    { id: '1', name: 'S3' },
    { id: '3', name: 'S3' },
];
const output = input.map(transform);
console.log(output); /* Logs:
[
  {
    "catId": 2,
    "segmentId": "2",
    "segmentName": "S2"
  },
  {
    "catId": 2,
    "segmentId": "3",
    "segmentName": "S3"
  },
  {
    "catId": 1,
    "segmentId": "1",
    "segmentName": "S1"
  },
  {
    "catId": 1,
    "segmentId": "2",
    "segmentName": "S2"
  },
  {
    "catId": 1,
    "segmentId": "3",
    "segmentName": "S3"
  },
  {
    "catId": 3,
    "segmentId": "3",
    "segmentName": "S3"
  }
]
*/

CodePudding user response:

The beneath provided solution implements an approach which uses preprocessed lookup data ...

  • the catIDs set for existing catalog-IDs
  • and the catSegments object for existing combinations of catalog- and segment-IDs.

The intermediate result, an array of newly created and valid catalog items, then gets computed by iterating over an array of existing catIDs via flatMap where for each catID one tries to create an array of new catalog items by reduceing the passed segments array. The validity of each possible catalog item is assured by looking up whether it already exists as a combination of the same catID and segmentId values.

function getUpdatedCatalogsAggregatedBySegments(
  catalogs = [], segments = []
) {
  // create lookup data,
  // - the `catIDs` set for existing catalog-IDs 
  // - and the `catSegments` object for existing
  //   combinations of catalog- and segment-IDs.
  const {

    catIDs,
    catSegments,

  } = catalogs

    .reduce((
      { catIDs, catSegments },
      { catID, segmentId, segmentName }
    ) => {
      catIDs.add(catID);
      catSegments[`${ catID }_${ segmentId }`] ??= segmentName;

      return { catIDs, catSegments };
    }, {
      catIDs: new Set,
      catSegments: {},
    });

  // try to create new/valid catalog items for each existing `catId`.
  const newCatalogs = [...catIDs]

    .flatMap(catID =>
      segments
        .reduce((list, { id: segmentId, name: segmentName }) => {

          const itemKey = `${ catID }_${ segmentId }`;
          if (!catSegments.hasOwnProperty(itemKey)) {

            // update lookup.
            catSegments[itemKey] = segmentName;

            // push new/valid catalog item into the aggregating list.
            list.push({ catID, segmentId, segmentName });
          }
          return list;

        }, [])
    );

  // - return a new array of ...
  //   ... originally passed ...
  return catalogs
    // ... and newly computed data.
    .concat(newCatalogs);
}


const catalogs = [
  { catID: 2, segmentId: '2', segmentName: 'S2' },
  { catID: 1, segmentId: '1', segmentName: 'S1' },
  { catID: 1, segmentId: '2', segmentName: 'S2' },
];
console.log(
  "The OP's use case ... (catalogs, [{ id: '3', name: 'S3' }]) =>",
  getUpdatedCatalogsAggregatedBySegments(
    catalogs, [{ id: '3', name: 'S3' }]
  ),
);

const segments = [
  { id: '3', name: 'S3' },
  { id: '2', name: 'S3' },
  { id: '1', name: 'S3' },
];
const updatedCatalogs =
  getUpdatedCatalogsAggregatedBySegments(catalogs, segments);

console.log(
  "Another, more complex, use case ...", {
    segments,
    catalogs,
    updatedCatalogs,
  },
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

  • Related