Wherefore Art Thou

Learn how to solve the freeCodeCamp algorithm 'Wherefore Art Thou' using the Array.filter() and other JavaScript methods.

Wherefore Art Thou

In this freeCodeCamp algorithm we have a quite a few requirements. We need to create a function that 👀 looks through the first argument named collection which is an array of objects and returns a new array with any of the objects within collection that have all of the key/value pairs from the second argument, source.

For example, if the first argument, collection, is [{ "apple": 1, "bat": 2 }, { "apple": 1 }, { "apple": 1, "bat": 2, "cookie": 2 }], and the second argument, source is { "apple": 1, "cookie": 2 }, then you must return [{ "apple": 1, "bat": 2, "cookie": 2 }] because it contains both "apple": 1 and "cookie": 2 - even though source does not contain the key/value pair "bat": 2.

Requirements

  • whatIsInAName([{ first: "Romeo", last: "Montague" }, { first: "Mercutio", last: null }, { first: "Tybalt", last: "Capulet" }], { last: "Capulet" }) should return [{ first: "Tybalt", last: "Capulet" }]
  • whatIsInAName([{ "apple": 1 }, { "apple": 1 }, { "apple": 1, "bat": 2 }], { "apple": 1 }) should return [{ "apple": 1 }, { "apple": 1 }, { "apple": 1, "bat": 2 }]
  • whatIsInAName([{ "apple": 1, "bat": 2 }, { "bat": 2 }, { "apple": 1, "bat": 2, "cookie": 2 }], { "apple": 1, "bat": 2 }) should return [{ "apple": 1, "bat": 2 }, { "apple": 1, "bat": 2, "cookie": 2 }]
  • whatIsInAName([{ "apple": 1, "bat": 2 }, { "apple": 1 }, { "apple": 1, "bat": 2, "cookie": 2 }], { "apple": 1, "cookie": 2 }) should return [{ "apple": 1, "bat": 2, "cookie": 2 }]
  • whatIsInAName([{ "apple": 1, "bat": 2 }, { "apple": 1 }, { "apple": 1, "bat": 2, "cookie": 2 }, { "bat":2 }], { "apple": 1, "bat": 2 }) should return [{ "apple": 1, "bat": 2 }, { "apple": 1, "bat": 2, "cookie":2 }]
  • whatIsInAName([{"a": 1, "b": 2, "c": 3}], {"a": 1, "b": 9999, "c": 3}) should return []

I Struggled With This One 💥🥊

Alright, I'll start by saying that this was personally the most frustrating freeCodeCamp algorithm that I've encountered. For some reason I just couldn't completely wrap my head around this one and create a solid mental model to work through this. I'd come up with a solution that I thought would do the trick, and it turns out that the one of the previously passing requirements would fail ❌.

Through lots of stackoverflow-ing, JavaScript doc reading, trial & error, and just sheer determination I was able to power through this one. Hopefully you have an easier time than I did. With all that said, this is definitely not the prettiest 👹 solution, but it ultimately got the job done ✅.

NOTE: If anyone would like to feature their working solution, please reach out! 🙏

Psuedocode

Okay, again - this is a lengthy solution, but here's how I ultimately approached it:


// create array to store matches
// get source keys
// get source values

// keep track of the number of source keys in each object from collection

// filter collection
	// get object keys
	// get object values
    
// loop through the number of object keys
	// if the object doesn't have the current source key as a property
		// set source key counter to 0
	
	// if object has current source key as a property
    // & the corresponding object value equals the
    // corresponding source value
			//increment source key counter

	// if source key counter is equal to the length of source keys
    // OR if there's only one source key and it's
    // source value is equal to the corresponding object value
		// add object to array
		// reset source key counter back to 0
        
// return the array

Solving the Algorithm

Setting up our function

Here's the function boilerplate that's given to us by freeCodeCamp:

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
 
  
  // Only change code above this line
  return arr;
}

Getting the keys and values of source

We first need to get the keys and values of source. Lucky for us, there's a couple handy JavaScript methods to do just this: Object.keys & Object.values. I encourage you to skim through these to at least get the gist of how to use them.

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  
  // Only change code above this line
  return arr;
}

Keep track of the number of source keys in each object from collection

After bouncing between a passing ✅ and failing ❌ requirement way too many times, I decided to keep track of the number of keys in a particular obj within collection as I iterated through them. My thinking behind this was that if an obj in collection had at least the same number of keys in source, then assuming values matched - we found a complete match. If obj had less keys than source, than it would impossible for it to be a complete match.

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  var srcKeyCount = 0; // All that...just for this?!
  
  // Only change code above this line
  return arr;
}

Filter collection grabbing the keys & values for each object

Okay, here we start getting into the thick of things. We will use Array.filter() to filter collection for the items we want.

The filter() method creates a new array with all elements that pass the test implemented by the provided function.

NOTE: Definitely read up on the Array.filter() method as it's one of the most useful methods there is.

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  var srcKeyCount = 0;
  
  collection.filter((obj, i) => {
  // obj is the element we're iterating over within collection
  // i is the index of the element
      
      let objKeys = Object.keys(obj) // Get the keys of current object
      let objVals = Object.values(obj) // Get the values of current object
  })
  
  // Only change code above this line
  return arr;
}

Loop through the keys of each obj checking for matches

Now that our filter is setup, for each obj, let's loop through all of it's keys, objKeys. This way, however many keys that particular obj has, we will get through all of them.

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  var srcKeyCount = 0;
  
  collection.filter((obj, i) => {
  // obj is the element we're iterating over within collection
  // i is the index of the element
      
      let objKeys = Object.keys(obj) // Get the keys of current object
      let objVals = Object.values(obj) // Get the values of current object
      
      for (var j in objKeys) {
          // Insert logic to check for match here
      }
  })
  
  // Only change code above this line
  return arr;
}

Handle Any obj That Has No Match

We need to create an edge case that will account for what happens when the current obj does not have a match. We can do this fairly simply by using the Object.hasOwnProperty() method which will tell us whether or not a key directly exists within an object. We then know that any obj that doesn't contain any of the keys from srcKeys could not possibly be a match, so we can skip past it.

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  var srcKeyCount = 0;
  
  collection.filter((obj, i) => {
      
      let objKeys = Object.keys(obj) // Get the keys of current object
      let objVals = Object.values(obj) // Get the values of current object
      
      for (var j in objKeys) {
          if (!obj.hasOwnProperty(srcKeys[j])) {
            srcKeyCount = 0; // Reset srcKeyCount to zero
          }
      }
  })
  
  // Only change code above this line
  return arr;
}

Handle When objVals Equals a srcVals

Whenever objVals equals srcVals we want to add this to arr right? Well, in my example - not quite. Here's why:

Let's say that within our loop, obj is currently: { "apple": 1, "bat": 2 } and source is: { "apple": 1, "cookie": 2 }.

Our loop iterating through objKeys and checking that:

  • obj indeed has the property srcKeys[j] which, in this case would be "apple"
  • AND that objVals[j], which in this case is 1 - is equal to srcVals[j] which is also 1
Great! We have a match! 🛑 Not so fast...

We did indeed match the first key/value pair between obj and source, but what about the other key/value pair in obj? It doesn't match - "bat": 2 does not equal "cookie": 2, so therefore we cannot add it to arr.

This is where having the counter comes in handy. Once we find a match, like we did with "apple": 1, we will simply increment srcKeyCount, and if the total number of key/value matches is at least equal to the number of srcKeys - assuming all of our other logic checks out - we have a match and we can add it to arr. Otherwise, if within the current obj we find a key/value pair that is not a match - we reset srcKeyCount back to 0 so we can disregard that obj and start iterating through the next one.

Still with me? Here's how I wrote this out:

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  var srcKeyCount = 0;
  
  collection.filter((obj, i) => {
      
      let objKeys = Object.keys(obj) // Get the keys of current object
      let objVals = Object.values(obj) // Get the values of current object
      
      for (var j in objKeys) {
          if (!obj.hasOwnProperty(srcKeys[j])) {
            srcKeyCount = 0; // Reset srcKeyCount to zero
          }
          if (obj.hasOwnProperty(srcKeys[j]) && objVals[j] == srcVals[j]) {
            srcKeyCount++
          }
      }
  })
  
  // Only change code above this line
  return arr;
}

Handle Complete Matches And Single Source Keys

Once we've looped through the current obj, and if the number of matches (srcKeyCount)equals the length of srcKeys, we can than add that obj to arr. Lastly, if we have just a single source key, and the key/value pair is found within obj we can also add that to arr.

function whatIsInAName(collection, source) {
  // What's in a name?
  var arr = [];
  // Only change code below this line
  var srcKeys = Object.keys(source)
  var srcVals = Object.values(source);
  var srcKeyCount = 0;
  
  collection.filter((obj, i) => {
      
      let objKeys = Object.keys(obj) // Get the keys of current object
      let objVals = Object.values(obj) // Get the values of current object
      
      for (var j in objKeys) {
          if (!obj.hasOwnProperty(srcKeys[j])) {
            srcKeyCount = 0; // Reset srcKeyCount to zero
          }
          if (obj.hasOwnProperty(srcKeys[j]) && objVals[j] == srcVals[j]) {
            srcKeyCount++
          }
          if (srcKeyCount === srcKeys.length || srcKeys.length === 1 && objVals[j] == srcVals) {
              arr.push(obj)
              srcKeyCount = 0;  
          }
      }
  })
  
  // Only change code above this line
  return arr;
}
whatIsInAName([{ "apple": 1, "bat": 2 }, { "apple": 1 }, { "apple": 1, "bat": 2, "cookie": 2 }], { "apple": 1, "cookie": 2 })
// Should return [{ "apple": 1, "bat": 2, "cookie": 2 }]

Try this the code snippet in your browser console.

Woah, How About That

That was a lot. If you made it this far then hats off to you! 👏👏👏👏👏
As I've said before, this solution is definitely not the cleanest out there, but it did get the job done. On to the next!

How I felt after completing this algorithm 👇👨‍💻

via GIPHY

Final Thoughts

Hopefully you found this to be a helpful walkthrough on the freeCodeCamp algorithm "Wherefore Art Thou". Definitely take a stab at this or feel free to tighten up my example and send it over if you'd like me to feature it on this post!

Shoot me an email at [email protected] with any questions and if you enjoyed this, stay tuned and subscribe below! 👇

Help us improve our content