Jake Choi
Jake Choi

Reputation: 13

Group objects in array by property

I have just started to pick up coding seriously. :) I came across a problem that seems too complicated for me.

How to group the following products by promotions type?

var data = [
    {
      name:'product1', 
      price:'40', 
      promotion:[
        {
          name:'Buy 3 get 30% off', 
          code:'ewq123'
        },
        {
          name:'Free Gift', 
          code:'abc140'
        }
      ]
    },
    {
      name:'product2', 
      price:'40', 
      promotion:[
        {
          name:'Buy 3 get 30% off', 
          code:'ewq123'
        }
      ]
    },
    {
      name:'product3', 
      price:'40', 
      promotion:[
        {
          name:'Buy 3 get 30% off', 
          code:'ewq123'
        }
      ]
    },
    {
      name:'product4', 
      price:'40'
    },
    {
      name:'product5', 
      price:'40', 
      promotion:[
        {name:'30% off', code:'fnj245'}
      ]
    },
    {
      name:'product6', 
      price:'0', 
      promotion:[
        {
          name:'Free Gift', 
          code:'abc140'
        }
      ]
    }
  ];

I would like to get result in the following format

result =[
    {
      name : 'Buy 3 get 30% off',
      code: 'ewq123',
      products: [
          ... array of products
      ]
    },
    {
      name : '30% off',
      code: 'fnj245',
      products: [
          ... array of products
      ]
    },
    {
        ...
    }
  ];

I am able to get a list of products by promotion code, but how can I make it generic?

function productHasPromo(product, promotion){
  if(!product.hasOwnProperty('promotion')) return false;

  var productPromo = product.promotion;

  for(var i=0; i<productPromo.length; i++){
    if(productPromo[i].code === promotion){
      return true;
    }
  }

  return false;
}

function groupProductByPromo(products, promotion){
  var arr = [];

  for(var i=0; i<products.length; i++){
    if(productHasPromo(products[i], promotion)){
      arr.push(products[i]);
    }
  }

  return arr;
}

Upvotes: 0

Views: 154

Answers (4)

Jake Choi
Jake Choi

Reputation: 13

Ok, after a few hours of works, with lots of help online and offline, I finally made it works. Thanks for the people who has helped.

Please do comment if you have a more elegant solution, always love to learn.

For people who ran into similar problem:

Here is my solution

function groupProductsByPromo(data){

  var result = [];
  // filter only product with promotion
  var productsWithPromo = data.filter(function(product){
    return product.hasOwnProperty('promotions');
  });

  // create promotions map
  var mappedProducts = productsWithPromo.map(function(product) {
    var mapping = {};
    product.promotions.forEach(function(promotion) {
      mapping[promotion.code] = {
        promotion: promotion
      };
    });
    return mapping;
  });

  // reduce duplicates in promotion map
  mappedProducts = mappedProducts.reduce(function(flattenObject, mappedProducts) {
    for (var promoCode in mappedProducts) {
      if (flattenObject.hasOwnProperty(promoCode)) {
        continue;
      }
      flattenObject[promoCode] = {
        code: promoCode,
        name: mappedProducts[promoCode].promotion.name
      };
    }
    return flattenObject;
  }, {});

  // add products to promo item
  for(var promoCode in mappedProducts){
    mappedProducts[promoCode].products = productsWithPromo.filter(function(product){
      return product.promotions.some(function(promo){
        return promo.code === promoCode;
      });
    });
    result.push(mappedProducts[promoCode]);
  }

  return result;
}

Upvotes: 0

Romulo
Romulo

Reputation: 5104

Explanation

You could write a function that loops through your array and search for the unique values within a specified property. That is easily done when working with simple data types, but can be done with more complex structures as arrays of objects (like in your example), using a helper grouping function.

Since you also need the output to be in a specific format after the grouping, we will have to work on a transformer also. This transformer will receive the original data and the unique values extracted by the grouping function, and will generate the desired output.

The following functions were used in the example:

Array.prototype.groupBy = function (property, grouping, transformer) {

    var values = [];

    this.forEach(function (item) {

        grouping.call(this, item, property).forEach(function (item) {

            if (!values.contains(property, item[property])) {
                values.push(item);
            }

        });

    });

    return transformer.call(this, values);

};

Array.prototype.contains = function (key, value) {
    return this.find(function (elm) {
        return elm[key] === value;
    });
};

function transformerFunction(values) {

    this.forEach(function (item) {

        if (!item.promotion) return;

        item.promotion.forEach(function (promotion) {

            values.forEach(function (option) {

                if (option.code === promotion.code) {
                    if (option.products) {
                        option.products.push(item);
                    } else {
                        option.products = [item];
                    }
                }

            });

        });

    });

    return values;

}

function groupingFunction(item, property) {

    if (!item.promotion) return [];

    var values = [];

    item.promotion.forEach(function (promotion) {

        if (!values.contains(property, promotion[property])) {
            values.push(promotion);
        }

    });

    return values;

}

Usage as follows:

var items = data.groupBy('code', groupFunction, transformFunction);

Example

Check the example i've prepared at jsfiddle

Upvotes: 1

user663031
user663031

Reputation:

Welcome to the coding world. A lot of people start off with a problem by trying to write some code, then they wonder why it doesn't work and scratch their heads, don't know the basics of debugging it, and then post here to SO. They're missing the crucial first step in programming which is to figure out how you are going to do it. This is also called designing the algorithm. Algorithms are often described using something called pseudo-code. It has the advantage that it can be looked at and understood and established to do the right thing, without getting bogged down in all the mundane details of a programming language.

There are some algorithms that are figured out by some very smart people--like the Boyer-Moore algorithm for string matching--and then there are other algorithms that programmers devise every day as part of their job.

The problem with SO is that all too often someone posts a question which essentially about an algorithm, and then all the keyboard-happy code jockeys pounce it and come up with a code fragment, which in many cases is so contorted and obtuse that one cannot even see what the underlying algorithm is.

What is the algorithm you propose for solving your problem? You could post that, and people would probably give you reasonable comments, and/or if you also give an actual implementation that doesn't work for some reason, help you understand where you've gone wrong.

At the risk of robbing you the pleasure of devising your own algorithm for solving this problem, here's an example:

  1. Create an empty array for the results.

  2. Loop through the products in the input.

  3. For each product, loop through its promotions.

  4. Find the promotion in the array of results.

  5. If there is no such promotion in the array of results, create a new one, with an empty list of products.

  6. Add the product to the array of products in the promotion entry in the array.

In pseudo-code:

results = new Array                                       // 1
for each product in products (data)                       // 2
  for each promotion in promotions field of product       // 3
    if results does not contain promotion by that name    // 4
      add promotion to results, with empty products field // 5
    add product to products field of results.promotion    // 6

If we believe this is correct, we can now try writing this in JavaScript.

var result = [];                                          // 1
for (var i = 0; i < data.length; i++) {                   // 2
  var product = data[i];
  var promotions = product.promotion;
  for (var j = 0; j < promotions.length; j++) {           // 3
    var promotion = promotions[i];
    var name = promotion.name;
    var result_promotion = find_promotion_by_name(name);
    if (!result_promotion) {                              // 4
      result_promotion = { name: name, products: [], code: promotion.code };
      result.push(result_promotion);                     // 5
    }
    result_promotion.products.push(name);                 // 6
  }
}

This code is OK, and it should get the job done (untested). However, it is still a bit unreadable. It does not follow the pseudo-code very closely. It somehow still hides the algorithm. It is hard to be sure that it is completely correct. So, we want to rewrite it. Functions like Array#foreach make it easier to do this. the top level can simply be:

var result = [];
data.forEach(processProduct);

In other words, call the processProduct function for each element of data (the list of products). It will be very hard for this code to be wrong, as long as `processProduct is implemented incorrectly.

function processProduct(product) {
  product.promotion.forEach(processPromotion);
}

Again, this logic is provably correct, assuming processPromotion is implemented correctly.

function processPromotion(promotion) {
  var result_promotion = getPromotionInResults(promotion);
  result_promotion.products.push(name);
}

This could hardly be clearer. We obtain the entry for this promotion in the results array, then add the product to its list of products.

Now we need to simply implement getPromotionInResults. This will include the logic to create the promotion element in the results array if it doesn't exist.

function getPromotionInResults(promotion) {
  var promotionInResults = findPromotionInResultsByName(promotion.name);
  if (!promotionInResults) {
    promotionInResults = {name: promotion.name, code: promotion.code, products: []};
    result.push(promotionInResults);
  }
  return promotionInResults;
}

This also seems demonstrably correct. But we still have to implement findPromotionInResultsByName. For that, we can use Array#find, or some equivalent library routine or polyfill:

function findPromotionInResultsByName(name) {
  return result.find(function(promotion) {
    return promotion.name === name;
  });
}

The entire solution is thus

function transform(data) {

  // Given a product, update the result accordingly.
  function processProduct(product) {
    product.promotion.forEach(processPromotion);
  }

  // Given a promotion, update its list of products in results.
  function processPromotion(promotion) {
    var result_promotion = getPromotionInResults(promotion);
    result_promotion.products.push(name);
  }  

  // Find or create the promotion entries in results.
  function getPromotionInResults(promotion) {
    var promotionInResults = findPromotionInResultsByName(promotion.name);
    if (!promotionInResults) {
      promotionInResults = {name: promotion.name, code: promotion.code, products: []};
      result.push(promotionInResults);
    }
    return promotionInResults;
  }

  // Find an existing entry in results, by its name.
  function findPromotionInResultsByName(name) {
    return result.find(function(promotion) {
      return promotion.name === name;
    });
  }

  var result = [];
  data.forEach(processProduct);

  return result;
}

Upvotes: 1

lxe
lxe

Reputation: 7599

Check out lodash - a nifty library for doing all sorts of transforms.

lodash.groupBy is what you're looking for.

Upvotes: -1

Related Questions