Wednesday, July 10, 2019

Day 094: FCC Intermediate Algorithms: Wherefore art thou

Currently, I am only about halfway through the FCC Intermediate Algos section. I had initially skipped Intermediate Algorithm Scripting: Wherefore art thou since it is dealing with objects and objects are not my cup of tea, but then I encountered another problem with objects so might as well get this one done before moving on, yes?

I really like working with challenges that have arrays. I understand it and enjoy it. But when you throw an object into the mix, I start having problems. And then you add objects into arrays or arrays into objects and nest them in some way or another, my eyes get crossed!

It is partially because the object references the location and not really the values itself (or something like that). So when you compare the following "same" objects in the below array, the result is a false. Try to iterate through that mess and you can get a sense as to why it confuses the heck out of me.

const testArr = [
  { key1: "value1", key2: "value2" },
  { key1: "value1", key2: "value2" }
];

console.log(testArr[0] === testArr[1]); // false

But objects is in part what makes Javascript OOP, so I better get used to it.

The FCC challenge requires the following:
Make a function that looks through an array of objects (first argument named "collections") and returns an array of all objects that have matching name and value pairs (second argument named "source"). Each name and value pair of the source object has to be present in the object from the collection if it is to be included in the returned array.
whatIsInAName(
  [
    { first: "Romeo", last: "Montague" }, 
    { first: "Mercutio", last: null }, 
    { first: "Tybalt", last: "Capulet" }
  ], 
  { last: "Capulet" }
);

// should return [{ first: "Tybalt", last: "Capulet" }].

Being aware of the conditions of objects and key value pairs that make it hard for me, how did I initially tackle the problem? I used Object.keys() and Object.values() to convert the array of objects into an array of arrays, combining the keys and the values as "keys+Values" with use of the for.. in loop, something like the below as an example.

const srcKey = Object.keys(source);

  for ( let i = 0 ; i < collection.length ; i++){
    for (let key in collection[i]) {
      console.log(key + ":" + collection[i][key]);
    }
  }

Midway through writing up my answer, I realized that it was probably a very inefficient and um... bad way to work with objects in getting to the solution.

So I peeked at the hints.

I was on the right path to use the Object.keys(), but I wasn't using it correctly. And filter() and Object.hasOwnProperty() also made an appearance.

I didn't quite consider using filter() because it is a method on an array, not object. Since they are objects, I kind of agonized over that and overlooked the fact that it was an array of objects. So filter will work fine in this case. It almost always does with these matching type problems.

In my own words, filter() accepts a condition and returns that which is true or "passes the test implemented by the provided function" (per MDN). You plop in a true or false function and it gives you an array of whatever values are true to the condition.

Okay, it sounds like I have to write function that checks whether any part of the source matches the submitted collections array by looping through the array (of objects). And somewhere along the way, hasOwnProperty comes into play in there. I'm not sure if I ever used hasOwnProperty and how that comes to play. So what does it do?
The hasOwnProperty() method returns a boolean indicating whether the object has the specified property as its own property (as opposed to inheriting it). --MDN
I am going to need to use it to check if the objects in the collection has the properties of the source. Fair enough.

So cue the many searches and hours spent on this problem and I finally gave up and took a look at their code solution.

I came close to their basic solution. Most of the bits and bobs were in the right place, but I could not get the condition statement right. /sigh Always foiled by that ! operator. Here's my completed solution after reviewing theirs:

function whatIsInAName(collection, source) {
  const sourceKey = Object.keys(source);

  function existsInArray(obj) {
    for (var i = 0; i < sourceKey.length; i++) {
      if (obj[sourceKey[i]] !== source[sourceKey[i]]) {
        return false;
      }
    }
    return true;
  }
  return collection.filter(existsInArray);
}

Why does this work? Let's deconstruct this a bit using the first test case.

Step 1. We need to use Object.keys() which will return the object's name for the key value pairs. We need this information because our task is to find the one that matches with the source. In this case, we need to match the last names (key and value).

Step 2. To simplify matters a bit, I created the "sourceKey" which is the "Object.keys(source)" to keep the code a bit cleaner. You can indeed use something like "Object.keys(source)[i]" later in the conditional part of your code, but it gets harder to read. At least for me this is the case. This code produces the following:

console.log(Object.keys(source)) 
// returns "last"

Step 3. In peeking at the hints, I found that filter() needed to be used. So in this case, it is a matter of filtering out the objects in the array that does have the same sourceKey and sourceKey value and returning the objects that match in its entirety, even the parts such as the first names and values that do not match.

I am still wrapping my head around filter(), reduce() and the like, so I rely very heavily on the documentation and the examples therein to continue. I found the Filtering invalid entries from JSON example to be particularly helpful. So I create a function to check if the source key, i.e. the last name exists in the provided collection.

Step 4. I create the "existsInArray()" function that essentially takes in an array of objects as the parameter (which is the collection) which will be run later when used in conjunction with filter(). I'm, largely following the example in MDN where were will need to call the following eventually:

return collection.filter(existsInArray)

This is the same as checking through the below:

console.log(collection[i][sourceKey[j]]); 
// is same as
console.log(collection[i].last)
// returns all the last names

It is similar to collection[i] because remember that it is built in that filter() will iterate through the arrays so there won't be an actual need to iterate through the array. Though of course it is still possible to solve this problem without filter by indeed using nested for loops to iterate through the collection and checking against the source.

Step 5. Now we write the logic so that when we iterate through the collections to check if the values of the sourceKey (the last name) match. Filter accepts a boolean value and normally outputs the items that are true to the logic and removes those that are false. In MDN's example, if the function checks for all the strings in an array greater than a length of 6, it will only return an array with those strings that are true to the function.

So I initially had in the following in my if statement:

if (obj[sourceKey[i]] === source[sourceKey[i]]) {
return true;
}

Success!.. or is it?

It solved the first and second test case but not the remaining.

Why!?


Well, my initial answer also returned any of the whole object in the collection if it matched one single key+value pair from the source, whereas it should only return the object if it matches all key+value pairs in the source. So just because I match 1 key+value pair in the source, it returned the whole object. This is obviously not what I want if there is more than one key+value pair in the source.

This is also where I spent most of the time puzzling through and could not get why it was not working. I tried a few different things that did not work out. So after an hour to trying, I peeked at the answer and saw right away... that it was reversed on their end. I applied it to my answer and of course it solved right away!

But why?

Step 6. Since filter accepts a boolean value and returns the bits that are true,  the logic should identify the objects in the collections array doesn't match all the key+value pairs in the source first and mark them as false (so it is not included in the output) then return all else as true after the for loop.

for (...){
  if (obj[sourceKey[i]] !== source[sourceKey[i]]) {
    return false;
  }
}
return true;

That's it. I have such a hard time even considering this route in using filter() or map() to define items as false first and return all else true as a way to filter more specifically. I am hoping I get better at this with time!


No comments:

Post a Comment