Search code examples
javascripttypescriptalgorithmfunctional-programming

TypeScript: group items according to shared attributes and assign them a groupID


I want to implement a function that does the following:

  • Takes an array of products as an argument
  • returns a new array of products, each of which has a new groupId attribute.
  • Different products will share the same groupId if they share the same common attributes, the attributes being determined by groupIfIdentical

See function skeleton here:

    function groupProductsBySharedAttributes(
      products: Product[],
      groupIfIdentical: (keyof Product)[],
      initialGroupId: string,
    ) {
      
    }

Products containing identical attributes should be grouped like this:

export function productsAreIdentical(
  prod1: Product,
  prod2: Product,
  groupIfIdentical: (keyof Product)[],
) {
  for (const key of groupIfIdentical) {
    if (prod1[key] !== prod2[key]) {
      return false
    }
    return true
  }
}

Example:

const prods = [{
  country: 'china',
  material: 'steel',
  sku: 1453
},
{
  country: 'china',
  material: 'steel',
  sku: 4874
},
{
  country: 'japan',
  material: 'steel',
  sku: 4874
},
]


const result = groupProductsBySharedAttributes(prods, ['country', 'material'], 1001)

result = [
  {
  country: 'china',
  material: 'steel',
  sku: 1453,
  groupId: 1001
},
{
  country: 'china',
  material: 'steel',
  sku: 4874,
  groupId: 1001
},
{
  country: 'japan',
  material: 'steel',
  sku: 4874,
  groupId: 1002
}
]

Solution

  • In the groupProductsBySharedAttributes function First you will initialize a Map object called groupIdMap where you will store the Stringify version of the object of the groupIfIdentical keys of the product as a key and groupId as a value, also you will initialize a new array called groupedProducts of the products but after inserting the groupId, then you will loop through the products array check if the key exist in the groupIdMap , if yes, they are identical, if not save the new key and groupId ( after increment by one ), and in both cases push in the groupedProducts

    interface Product {
        [key: string]: any;
    }
    
    function productsAreIdentical(
        prod1: Product,
        prod2: Product,
        groupIfIdentical: (keyof Product)[]
    ): boolean {
        for (const key of groupIfIdentical) {
            if (prod1[key] !== prod2[key]) {
                return false;
            }
        }
        return true;
    }
    
    function groupProductsBySharedAttributes(
        products: Product[],
        groupIfIdentical: (keyof Product)[],
        initialGroupId: string,
    ): Product[] {
        let groupId = parseInt(initialGroupId); // Convert initialGroupId to an integer for easy incrementation
        const groupedProducts: Product[] = [];
        const groupIdMap = new Map<string, number>(); // Maps a stringified key to a groupId
    
        products.forEach(product => {
            let foundGroup = false;
            for (const [key, id] of groupIdMap) {
                const keyAttributes = JSON.parse(key);
                if (productsAreIdentical(product, keyAttributes, groupIfIdentical)) {
                    product.groupId = id;
                    foundGroup = true;
                    break;
                }
            }
    
            if (!foundGroup) {
                const key = JSON.stringify(
                    groupIfIdentical.reduce((acc, attr) => {
                        acc[attr] = product[attr];
                        return acc;
                    }, {} as Partial<Product>)
                );
    
                if (!groupIdMap.has(key)) {
                    groupIdMap.set(key, groupId);
                    product.groupId = groupId;
                    groupId++;
                }
            }
    
            groupedProducts.push(product);
        });
    
        return groupedProducts;
    }
    
    // Example usage
    const prods = [
        { country: 'china', material: 'steel', sku: 1453 },
        { country: 'china', material: 'steel', sku: 4874 },
        { country: 'japan', material: 'steel', sku: 4874 }
    ];
    
    const result = groupProductsBySharedAttributes(prods, ['country', 'material'], '1001');
    console.log(result);
    

    PLAYGROUND