Skip to content
Advertisement

Recursively filter an array of infinitely nested objects by mutliple matching conditions but only return parent that has an instance of both matches

I have the following array of objects; however this could be any unknown key/value and be infinitely nested, for now this is a testing sample:

[
  {
    "reference_id": "R123",
    "customer": "Person 1",
    "customer_email": "person1@email.com",
    "location": "UK",
    "bookings": [
      {
        "product": "Product 1",
        "provider": "Company 1",
        "cancellable": true
      },
      {
        "product": "Product 2",
        "provider": "Company 2",
        "cancellable": true
      },
      {
        "product": "Product 3",
        "provider": "Company 1",
        "cancellable": true
      }
    ]
  },
  {
    "reference_id": "R1234",
    "customer": "Person 2",
    "customer_email": "person2@email.com",
    "location": "USA",
    "bookings": [
      {
        "product": "Product 1",
        "provider": "Company 1",
        "cancellable": true
      },
      {
        "product": "Product 3",
        "provider": "Company 1",
        "cancellable": true
      }
    ]
  },
  {
    "reference_id": "R12345",
    "customer": "Person 3",
    "customer_email": "person3@email.com",
    "location": "UK",
    "bookings": [
      {
        "product": "Product 2",
        "provider": "Company 2",
        "cancellable": true
      },
      {
        "product": "Product 3",
        "provider": "Company 1",
        "cancellable": true
      }
    ]
  }
]

My current implementation is as follows:

const selected = [
  {
    term: 'Company 1',
    column: 'provider',
  },
  {
    term: 'Person 1',
    column: 'customer',
  },
];

const recursivelyFilterByValue = () => (value) => selected.every((item) => {
  if (!value) return false;

  if (typeof value === 'string') {
    // console.log('value', value === item.term);
    return value === item.term;
  }

  if (Array.isArray(value)) {
    return value.some(this.recursivelyFilterByValue());
  }

  if (typeof value === 'object') {
    return Object.values(value).some(this.recursivelyFilterByValue());
  }

  return false;
});

const results = data.filter(recursivelyFilterByValue());

Basically I am adding to the “selected” array then using that to filter the data array by. I do want to ensure the key matches the “column” also however I haven’t added that yet.

For the input above I would expect to output the below:

[
  {
    "reference_id": "R123",
    "customer": "Person 1",
    "customer_email": "person1@email.com",
    "location": "UK",
    "bookings": [
      {
        "product": "Product 1",
        "provider": "Company 1",
        "cancellable": true
      },
      {
        "product": "Product 2",
        "provider": "Company 2",
        "cancellable": true
      },
      {
        "product": "Product 3",
        "provider": "Company 1",
        "cancellable": true
      }
    ]
  },
]

However the output array is empty. If I only search for one term (remove all but one term from selected array) the output is correct for that term however any subsequent terms bring back a blank array.

I’m wondering if my use of .some() is the problem however changing this causes too much recursion errors.

Essentially, I want to return the original parent object so long as there is a key:value match for all my conditions in the selected array, at any level of its children.

Any guidance would be much appreciated, thank you.

Advertisement

Answer

I’m not quite sure if this is what you’re looking for. It makes the assumption that my guess in the comments was correct:

Do I have this right? You have one (presumably dynamic) condition that says that an object either has a provider property with value "Customer 1" or has a (recursively) descendant object that does. And you have a second condition regarding customer and "Person 1", and you’re looking for objects that meet both (or all) such conditions. Does that describe what you’re trying to do?

Here we have two fairly simple helper functions, testRecursive and makePredicates as well as the main function, recursivelyFilterByValue:

const testRecursive = (pred) => (obj) => 
  pred (obj) || Object (obj) === obj && Object .values (obj) .some (testRecursive (pred))

const makePredicates = (criteria) => 
  criteria .map (({term, column}) => (v) => v [column] == term)

const recursivelyFilterByValue = (criteria, preds = makePredicates (criteria)) => (xs) =>
  xs .filter (obj => preds .every (pred => testRecursive (pred) (obj)))


const selected = [{term: 'Company 1', column: 'provider'}, {term: 'Person 1', column: 'customer'}]

const input = [{reference_id: "R123", customer: "Person 1", customer_email: "person1@email.com", location: "UK", bookings: [{product: "Product 1", provider: "Company 1", cancellable: true}, {product: "Product 2", provider: "Company 2", cancellable: true}, {product: "Product 3", provider: "Company 1", cancellable: true}]}, {reference_id: "R1234", customer: "Person 2", customer_email: "person2@email.com", location: "USA", bookings: [{product: "Product 1", provider: "Company 1", cancellable: true}, {product: "Product 3", provider: "Company 1", cancellable: true}]}, {reference_id: "R12345", customer: "Person 3", customer_email: "person3@email.com", location: "UK", bookings: [{product: "Product 2", provider: "Company 2", cancellable: true}, {product: "Product 3", provider: "Company 1", cancellable: true}]}]

console .log (recursivelyFilterByValue (selected) (input))
.as-console-wrapper {max-height: 100% !important; top: 0}
  • testRecursive checks whether a predicate is true for an object or for any objects nested inside it.

  • makePredicates turns an array of {term, column}-objects into predicate functions that test if an object has the proper term in the property named by the column.

  • recursivelyFilterByValue combines these, calling makePredicates to turn the selected items into predicate functions, then filtering the input by testing if each of the predicates is true.

This is not the most efficient code imaginable. It rescans the hierarchy for each predicate. I’m sure we could figure out a version to do the scan only once, but I think it would make for much more complex code. So you might want to test in your production-sized data whether it’s fast enough for your needs.

User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement