按 属性 对数组中的对象进行分组

Group objects in array by property

我刚刚开始认真学习编码。 :) 我遇到了一个对我来说似乎太复杂的问题。

如何按促销类型对以下产品进行分组?

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'
        }
      ]
    }
  ];

我想得到以下格式的结果

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

我可以通过促销代码获取产品列表,但如何才能使其通用?

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;
}

查看 lodash - 一个用于执行各种转换的漂亮库。

lodash.groupBy 就是您要找的。

说明

您可以编写一个循环遍历数组的函数,并在指定的 属性 中搜索 唯一值 。这在处理简单数据类型时很容易完成,但可以使用辅助函数 grouping 函数来处理更复杂的结构作为对象数组(如您的示例)。

由于您还需要在分组后以特定格式输出,因此我们还必须处理 transformer。此转换器将接收原始数据和 grouping 函数提取的 唯一值 ,并将生成所需的输出。

示例中使用了以下函数:

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;

}

用法如下:

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

例子

查看我在 jsfiddle

准备的示例

欢迎来到编码世界。很多人从尝试编写一些代码开始遇到问题,然后他们想知道为什么它不起作用并挠头,不知道调试它的基础知识,然后 post 到这里.他们错过了编程中关键的第一步,即弄清楚你将如何去做。这也称为设计算法。通常使用称为 伪代码 的东西来描述算法。它的优点是可以查看、理解并确定它可以做正确的事情,而不会陷入编程语言的所有世俗细节中。

有一些算法是由一些非常聪明的人想出的——比如用于字符串匹配的 Boyer-Moore 算法——还有其他算法是程序员每天在工作中设计的。

SO 的问题在于,经常有人 post 提出一个本质上是关于算法的问题,然后所有喜欢键盘的代码骑师都会扑向它并想出一个代码片段,在许多情况下案例是如此扭曲和迟钝,以至于人们甚至看不到底层算法是什么。

你提出的解决问题的算法是什么?你可以 post 那样,人们可能会给你合理的评论,and/or 如果你还提供了一个由于某种原因不起作用的实际实现,帮助你了解你哪里出错了。

冒着剥夺你设计自己的算法来解决这个问题的乐趣的风险,这里有一个例子:

  1. 为结果创建一个空数组。

  2. 循环输入中的产品。

  3. 对于每个产品,循环查看其促销活动。

  4. 在结果数组中查找促销。

  5. 如果结果数组中没有这样的促销,创建一个新的,产品列表为空。

  6. 将产品添加到数组中促销入口的产品数组中。

在伪代码中:

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

如果我们认为这是正确的,我们现在可以尝试将其写入 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
  }
}

这段代码没问题,应该可以完成工作(未经测试)。但是,它仍然有点不可读。它并没有非常严格地遵循伪代码。它不知何故仍然隐藏了算法。很难确定它是完全正确的。所以,我们想重写它。 Array#foreach 之类的函数可以更容易地做到这一点。顶层可以简单地是:

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

换句话说,为 data(产品列表)的每个元素调用 processProduct 函数。只要 `processProduct 实现不正确,这段代码就很难 出错。

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

同样,假设 processPromotion 已正确实施,此逻辑可证明是正确的。

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

这再清楚不过了。我们在结果数组中获取此促销的条目,然后将产品添加到其产品列表中。

现在我们需要简单地实现getPromotionInResults。如果促销元素不存在,这将包括在结果数组中创建促销元素的逻辑。

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

这似乎也很正确。但是我们还是要实现findPromotionInResultsByName。为此,我们可以使用 Array#find,或一些等效的库例程或 polyfill:

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

整个解决方案就是这样

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;
}

好的,经过几个小时的努力,在网上和线下的大量帮助下,我终于成功了。感谢帮助过的人。

如果有更优雅的解决方案欢迎评论,永远爱学习。

对于 运行 遇到类似问题的人:

这是我的解决方案

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;
}