Wherefore Art Thou
Learn how to solve the freeCodeCamp algorithm 'Wherefore Art Thou' using the Array.filter() and other JavaScript methods.
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 propertysrcKeys[j]
which, in this case would be"apple"
- AND that
objVals[j]
, which in this case is1
- is equal tosrcVals[j]
which is also1
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 }]
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 👇👨💻
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