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 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 }]
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 👇👨💻
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 tim@timwheeler.com
with any questions and if you enjoyed this, stay tuned and subscribe below! 👇