1
\$\begingroup\$

This is a function which filters on multiple object properties and their corresponding arrays.

  • I need to match all properties to a pattern and have an array as their property value.
  • Properties that match that pattern and contain an array get filtered on whether they contain string values.
  • The values can be in any one of the arrays related to that object.

  • All objects which have met the criteria and contain the appropriate strings irrespective of the property array they are contained in. With the exception being that all values are in the same object.

  • The object containing all string values do not get filtered out.


If I match all object properties containing 'ev_' and those properties contain arrays whose values include 'value75' and '12+'

This should be the resulting output

{
  "data": {
    "id": 3,
    "date": "2019-02-03",
    "ev_filter_1": [
      "art",
      "foodie"
    ],
    "ev_filter_2": [
      "value1",
      "value75"
    ],
    "ev_filter_3": [
      "value1",
      "value2"
    ],
    "ev_filter_4": [
      "all",
      "12+"
    ]
  }
}

From this array of objects

const posts = [{
    data: {
      id: 1,
      date: "2019-02-03",
      ev_filter_1: ["art", "foodie"],
      ev_filter_2: ["value1", "value2"],
      ev_filter_3: ["value1", "value2"],
      ev_filter_4: ["all", "12+"]
    }
  },
  {
    data: {
      id: 2,
      date: "",
      ev_filter_1: ["arti", "foodie"],
      ev_filter_2: ["value1", "value2"],
      ev_filter_3: ["value1", "value2"],
      ev_filter_4: ["all", "19+"]
    }
  },
  {
    data: {
      id: 3,
      date: "2019-02-03",
      ev_filter_1: ["art", "foodie"],
      ev_filter_2: ["value1", "value75"],
      ev_filter_3: ["value1", "value2"],
      ev_filter_4: ["all", "12+"]
    }
  }
];

This example is currently working but it is ugly and rigid. The "some" is just used for iteration, which I painfully coerced to return true. There is also a nested if statement (horrid).

const posts = [{
    data: {
      id: 1,
      date: "2019-02-03",
      ev_filter_1: ["art", "foodie"],
      ev_filter_2: ["value1", "value2"],
      ev_filter_3: ["value1", "value2"],
      ev_filter_4: ["all", "12+"]
    }
  },
  {
    data: {
      id: 2,
      date: "",
      ev_filter_1: ["arti", "foodie"],
      ev_filter_2: ["value1", "value2"],
      ev_filter_3: ["value1", "value2"],
      ev_filter_4: ["all", "19+"]
    }
  },
  {
    data: {
      id: 3,
      date: "2019-02-03",
      ev_filter_1: ["art", "foodie"],
      ev_filter_2: ["value1", "value75"],
      ev_filter_3: ["value1", "value2"],
      ev_filter_4: ["all", "12+"]
    }
  }
];

function sift(arrObjLit, pattern, argOne, argTwo) {
  return arrObjLit.filter(function(x, y) {
    return (result = Object.entries(x).some((q, i) => {
      for (let [key, v] of Object.entries(q[1])) {
        if (key.startsWith(pattern) && Array.isArray(v) && v.includes(argOne)) {
          for (let an of Object.values([x.data][0])) {
            if (Array.isArray(an) && an.includes(argTwo)) {
              return true;
            }
          }
        }
      }
    }))
  })
}

console.log(...sift(posts, "ev_", "value75", "12+"));

\$\endgroup\$
  • \$\begingroup\$ Please reformat the question to be more bullet point-ish. I am trying to sort through the requirements \$\endgroup\$ – Christian Bongiorno Jul 11 '17 at 0:59
  • \$\begingroup\$ @ChristianBongiorno will do \$\endgroup\$ – Rick Jul 11 '17 at 1:00
  • \$\begingroup\$ Can your posts structure be flattened or does everything have to be stored within a data object? Could there be more/different properties on the object that need to be checked for the filter value? \$\endgroup\$ – Gerrit0 Jul 11 '17 at 1:41
  • \$\begingroup\$ @Gerrit0, in general, the pattern can be anything, I used startsWith, but a regular expression can just as easily take its place. One of the most difficult parts of this question is the odd nesting of the data, as a requirement, everything should be stored in the data object. However, if no answer is forthcoming I am willing to accept some flattening, but that is not the desired solution. \$\endgroup\$ – Rick Jul 11 '17 at 1:52
  • \$\begingroup\$ Just to make sure, should {data: {ev_1: ["value75"] }, somethingElse: {ev_2: ["12+"]} } be matched? \$\endgroup\$ – Gerrit0 Jul 11 '17 at 1:55
2
\$\begingroup\$

First, and I can't emphasize this enough, use meaningful variable names. What is argOne? argTwo? The same goes for all of your single letter variable names. They make it harder to look at your code and figure out what's going on.

To help you see where the code you provided might be difficult to understand, I've copied it and commented what came to mind when reading each line, hopefully this can help with writing code that self-documents.

// It looks like this will filter arrObjLit based on pattern.
// But what is argOne, argTwo? Do I need both? What if I include argThree?
function sift(arrObjLit, pattern, argOne, argTwo) {
  // Filtering as expected, but why is the index y needed? 
  return arrObjLit.filter(function(x, y) {
    // Where did result come from? Right now it declares a global variable.
    // What is q? Is the index of it, i, needed?
    // Does this need to be wrapped in parenthesis?
    // The above line used function, here there's an arrow function. Why?
    return (result = Object.entries(x).some((q, i) => {
      // Okay, q[1] is the value, but q[0] isn't used, maybe 
      // Object.values() should be used above?
      // What happens if q isn't an object?
      for (let [key, v] of Object.entries(q[1])) {
        // This is clear, but what is special about argOne? 
        // Why not check argTwo here too?
        if (key.startsWith(pattern) && Array.isArray(v) && v.includes(argOne)) {
          // Why is [x.data][0] used instead of just x.data?
          // x.data == q[1]? What happens if q[1] != x.data?
          for (let an of Object.values([x.data][0])) {
            // Why don't we check that the key for the an array starts 
            // with key like we did for argOne?
            if (Array.isArray(an) && an.includes(argTwo)) {
              return true;
            }
          }
        }
      }
    }))
  })
}

In chat, you mentioned that ideally, every array contained within the object should be searched, if it matches the pattern. With this in mind, here is how I would solve your problem. (It might be a good idea to rename pattern to prefix, with pattern I would expect that I can pass in a regular expression)

function sift(entries, pattern, ...filters) {
  return entries.filter(entry => {
    // Copy the filters array into a set
    let toMatch = new Set(filters);

    let searchForMatches = root => {
      for (let [key, value] of Object.entries(root)) {
        if (Array.isArray(value) && key.startsWith(pattern)) {
          // The key for the array matches the pattern, so remove all of these
          // items from the required items to match, if they are found
          value.forEach(item => toMatch.delete(item));
        } else if (typeof value == 'object' && value) {
          // This item isn't null, and is an object.
          // It may contain an array, recurse
          searchForMatches(value);
        }
      }
    };

    searchForMatches(entry);

    // If the set is empty, all matches were found
    // and this object should be included in the result.
    return toMatch.size == 0;
  });
}
\$\endgroup\$
  • \$\begingroup\$ I should have said the same thing about your variables. \$\endgroup\$ – Christian Bongiorno Jul 11 '17 at 4:34
  • \$\begingroup\$ @ChristianBongiorno , I'd love to hear any suggestions you have to increase the clarity - or feel free to submit an edit :) \$\endgroup\$ – Gerrit0 Jul 11 '17 at 4:57
  • \$\begingroup\$ In general: extra variables should be toss. The ones that are around need to be clearly named. It would help to declare intermediate functions instead of nesting returns \$\endgroup\$ – Christian Bongiorno Jul 11 '17 at 5:28
  • \$\begingroup\$ @ChristianBongiorno, I originally did split the function up into two parts, with the recursive function placed on its own and returning an array of values to be checked, however this increased the complexity as it required searchForMatches to receivec 3 parameters (root, pattern, array) and also reduced the speed since it resulted in each item being checked twice (granted, speed could still be improved - but would make the code a bit messier). As far as I can tell, there are no extra variables. As for names, I'd still like to hear any suggestions, I'm not sure how to improve them. \$\endgroup\$ – Gerrit0 Jul 11 '17 at 11:52
  • \$\begingroup\$ @Gerrit0 why don't you use strict equality? \$\endgroup\$ – Rick Jul 11 '17 at 19:15
1
\$\begingroup\$

Try this script:

function sift2(arrObjLit, pattern, ...values) {
  const toMatch = new Set(values)
  return arrObjLit.filter(o =>
    Object.entries(o.data)
    .some(([k, v]) => {
      return k.startsWith(pattern)
          && Array.isArray(v)
          && v.some(x => toMatch.has(x));
    })
  );
}

The idea is to decompose it, apply the filtering and then reconstruct it.

part of the problem is this code mixes development paradigms: it uses function and => and for

\$\endgroup\$
  • \$\begingroup\$ @ChristianBogiorno In your update, you're third conditional on the second return 'some'. It will produce false positives because it will run through all arrays returning true and false several times. \$\endgroup\$ – Rick Jul 12 '17 at 1:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.