简体   繁体   中英

Lodash: filter a nested object by multiple properties

Consider the following example:

var products = {
    "Products": [{
        "Title": "A",
        "Categories": [{
            "Name": "Type",
            "Properties": ["Type 1", "Type 2", "Type 3"]
        }, {
            "Name": "Market",
            "Properties": ["Market 1", "Market 2", "Market 3", "Market 4"]
        }, {
            "Name": "Technology",
            "Properties": ["Tech 1", "Tech 2"]
        }]
    }, {
        "Title": "B",
        "Categories": [{
            "Name": "Type",
            "Properties": ["Type 1", "Type 3"]
        }, {
            "Name": "Market",
            "Properties": "Market 1"
        }, {
            "Name": "Technology",
            "Properties": ["Tech 1", "Tech 3"]
        }]
    }, {
        "Title": "C",
        "Categories": [{
            "Name": "Type",
            "Properties": ["Type 1", "Type 2", "Type 3"]
        }, {
            "Name": "Market",
            "Properties": ["Market 2", "Market 3"]
        }, {
            "Name": "Technology",
            "Properties": ["Tech 2", "Tech 3"]
        }]
    }]
}

I'm trying to filter products by their properties so consider I'm using an array to keep track of my selected filters:

var filters = ['Type 3', 'Tech 1'];

With these filters I would like to return product A and product B.

I currently have this:

var flattenedArray = _.chain(products).map('Categories').flatten().value();
var result= _.some(flattenedArray , ['Properties', 'Tech 1']);

But I'm stuck on how to combine the properties for a combined search.

Use _.filter() to iterate the products. For each product combine the list of properties using _.flatMap() , and use _.intersection() and _.size() to find the amount of filters that exist in the categories. Compare that to the original number of filters, and return comparison's response.

 var products = {"Products":[{"Title":"A","Categories":[{"Name":"Type","Properties":["Type 1","Type 2","Type 3"]},{"Name":"Market","Properties":["Market 1","Market 2","Market 3","Market 4"]},{"Name":"Technology","Properties":["Tech 1","Tech 2"]}]},{"Title":"B","Categories":[{"Name":"Type","Properties":["Type 1","Type 3"]},{"Name":"Market","Properties":"Market 1"},{"Name":"Technology","Properties":["Tech 1","Tech 3"]}]},{"Title":"C","Categories":[{"Name":"Type","Properties":["Type 1","Type 2","Type 3"]},{"Name":"Market","Properties":["Market 2","Market 3"]},{"Name":"Technology","Properties":["Tech 2","Tech 3"]}]}]}; var filters = ['Type 3', 'Tech 1']; var result = _.filter(products.Products, function(product) { return filters.length === _(product.Categories) .flatMap('Properties') .intersection(filters) .size(); }); console.log(result); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script> 

If I understand you question correctly, this code may help:

_.filter(
    products.Products,
    product => _.difference(
        filters,
        _.chain(product.Categories).map(category => category.Properties).flatten().value()
    ).length === 0
);

It calculates a union of all properties for each product:

_.chain(product.Categories).map(category => category.Properties).flatten().value()

And then checks that it contains all filters array elements, using _.difference method.

Hope it helps.

another fancy way through _.conforms

var res = _.filter(
    products.Products, 
    _.conforms({'Categories': function(categories) {
        return _.chain(categories)
            .flatMap('Properties') // flat arrays 
            .uniq() // remove dublicates
            .keyBy() // transform to objects with Properties keys 
            .at(filters) // get objects values by filters
            .compact() // remove undefineds
            .size() // get size
            .eq(filters.length) // compare to filters size
            .value();
    }
}))

This will work for a list of items where the givenProperty you want to filter on is either a string like 'doorColour' or an array of strings representing the path to the givenProperty like ['town', 'street', 'doorColour'] for a value nested on an item as town.street.doorColour.

It also can filter on more than one value so you could you just need pass in an array of substrings representing the string values you want to keep and it will retain items that have a string value which contains any substring in the substrings array.

The final parameter 'includes' ensures you retain these values if you set it to false it will exclude these values and retain the ones that do not have any of the values you specified in the substrings array

import { flatMap, path } from 'lodash/fp';

const filteredListForItemsIncludingSubstringsOnAGivenProperty = (items, givenProperty, substrings, including=true) => flatMap((item) =>
substrings.find((substring) => path(givenProperty)(item) && path(givenProperty)(item).includes(substring))
  ? including
    ? [item]
    : []
  : including
  ? []
  : [item])(items);

Eg fLFIISOAGP (contacts, ['person','name'], ['Joh','Pau',Pet']); with items of structure {contact, business:null, personal:{name:'John'}}.

For the original question - this will also work - I would use this repeatedly on a list of items to filter with different keys to filter on more than one property.

const firstFilteredResult = filteredListForItemsIncludingSubstringsOnAGivenProperty( products.Products, ["Categories", "0", "Properties"], ["Type 3"]);

const secondFilteredResult = filteredListForItemsIncludingSubstringsOnAGivenProperty( firstFilteredResult, ["Categories", "2", "Properties"], ["Tech 1"]);

expect(secondFilteredResult[0]['Title']).to.equal( "A"); expect(secondFilteredResult[1]['Title']).to.equal( "B"); expect(secondFilteredResult.length).to.equal(2);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM