Search code examples
javascriptarraysobjectprototype

Remove/Filter array of objects with another array


Array.prototype.remove = function() {
  var what, a = arguments,
    L = a.length,
    ax;
  while (L && this.length) {
    what = a[--L];
    while ((ax = this.indexOf(what)) !== -1) {
      this.splice(ax, 1);
    }
  }
  return this;
};

var items = [{
  title: 'Bokningsbar',
  start: moment("2018-04-05 06:00"),
  end: moment("2018-04-05 07:00"),
  allDay: false
}, {
  title: 'Bokningsbar',
  start: moment("2018-04-05 06:00"),
  end: moment("2018-04-05 07:00"),
  allDay: false
}, {
  title: 'Bokningsbar',
  start: moment("2018-04-05 06:00"),
  end: moment("2018-04-05 07:00"),
  allDay: false
}, {
  title: 'Bokningsbar',
  start: moment("2018-04-05 06:00"),
  end: moment("2018-04-05 07:00"),
  allDay: false
}]

var datesToRemove = [{
  title: 'Bokningsbar',
  start: moment("2018-04-06 06:00"),
  end: moment("2018-04-06 07:00"),
  allDay: false
}];
console.log("Before: " + items.length)
for (var i = 0; i < datesToRemove.length; i++) {
  items.remove(moment(datesToRemove[i].start).format("YYYY-MM-DD HH:mm"));
}
console.log("After: " + items.length)
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.0/moment.js"></script>

I want to remove the objects from one array using another array as a reference. Both arrays only contain the object below. All the corresponding objects in datesToRemove should be removed in the items array. Properties I was thinking I could use are start. Unfortunately there is no Id

This is my code so far:

The Object looks like this:

   var item = {
                            title: 'Bokningsbar',
                            start: moment(datesValue + " " + hourValue.start),
                            end: moment(datesValue + " " + hourValue.end),
                            allDay: false
                        };

Prototype.remove

       Array.prototype.remove = function () {
        var what, a = arguments,
            L = a.length,
            ax;
        while (L && this.length) {
            what = a[--L];
            while ((ax = this.indexOf(what)) !== -1) {
                this.splice(ax, 1);
            }
        }
        return this;
    };

use case:

  for (var i = 0; i < datesToRemove.length; i++) {
                            items.remove(moment(datesToRemove[i].start).format("YYYY-MM-DD HH:mm"));
                        }

With this setup I get the error Cannot read property 'indexOf' of undefined. And all athe obects are removed. Here is also a not working example. But maybe it illustrates what I want. If anybody got a better suggestion on how to remove the objects feel free to suggest it.

All help is much appreciated.


Solution

  • No need to extend the native Array prototype to get this done. There are a few methods for dealing with Array manipulation that are already available natively to JavaScript and can tackle your problem. More so, following along with using a custom Array method in conjunction with a for loop for a solution could very easily produce unexpected/unintended negative side effects that limit how well this solution could scale for future use within your application. The most obvious potential pitfall is mutating your original data structures, drastically limiting reusability and making it difficult to easily and accurately debug issues caused further down the application stack. These native Array methods will keep your original data structures intact, creating shallow copies of the Array instances during manipulation and returning them as your final result. As an additional bonus, these methods can be chained together in as many ways and as many times as you may need.

    • .map()
    • .filter()
    • .reduce()

    Another Array manipulation method commonly utilized is .forEach(). The .forEach() method removes the hassle of having to write and maintain messy for loops; making the overall codebase much easier to read and much easier to follow logically. However, .forEach(), unlike the 3 previously listed Array manipulation methods, does NOT protect against mutating any original data structures used. It can be included in the chain you may create using the other Array methods mentioned. However, once called, .forEach() breaks the ability to chain your methods any further.

    .map(), .filter(), and .reduce() provide a pure, functional approach to the iteration and manipulation of an Array. While all 3 methods still offer a means by which to loop over an Array, each method offers it's own unique functionality and purpose for manipulation. Understanding the distinct use of each method first is crucial to understanding when and how to chain them together effectively.

    .MAP()

    • .map() : Similar to .forEach(), this method loops over each item in the given Array. As the loop iterates over each item's index, the individual items are run against a provided method for manipulation. The end result is a new Array with every item being the product of the transformation function provided during the loop. The important note to remember using .map() is that the new Array that is returned after the loop completes will ALWAYS be the same length as the original Array used. This makes .map() an excellent choice when you need to return a new Array with item values that reflect a desired transformation when compared against the original Array, and all of it's item values.

    Example:

    const originalArr = [1, 2, 3, 4, 5]
    
    const incrementedArr = 
      originalArr.map(
        (n,i,arr) => {
          return n + 4
        }
      )
    
    console.log(originalArr)     // console output => [1, 2, 3, 4, 5]
    console.log(incrementedArr)  // console output => [5, 6, 7, 8, 9]
    

    In the example above, the incrementedArr constant could be rewritten in a much more concise way. For the purpose of answering your question thoroughly, I wrote it verbosely to illustrate an important note to always keep in mind. .map(), .filter(), and .reduce() each offer three arguments by default. The 3 arguments for .map() and .filter() are the same. They are:

    1. item

      The individual item value found at each index of the loop's iteration

    2. index

      The index value for each item found during the iteration of the loop

    3. array

      Provides a reference to the original array being used by the loop

    That said, here's a more concise illustration of .map() from the example above:

    const originalArr = [1, 2, 3, 4, 5]
    const incrementedArr = originalArr.map(n => n + 4)
    
    console.log(originalArr)     // console output => [1, 2, 3, 4, 5]
    console.log(incrementedArr)  // console output => [5, 6, 7, 8, 9]  
    

    .FILTER()

    • .filter() : Again, like .forEach(), this method loops over each item in the given Array. As it does, it compares the value of each item in the Array against a provided conditional check, returning either true or false for each item at each index within the Array. The final output will be an Array of item's whose values all returned as the same Boolean, either as true or false depending on the conditional check provided. This makes .filter() the perfect choice for when you need to return a new Array that may need to be a different length compared to the original Array provided. In other words, the new Array returned is a direct reflection of the conditional check used to filter out item values no longer needed in the original Array.

    Example:

    const originalArr = [1, 2, 3, 4, 5]
    const noEvenNumbersArr = originalArr.filter(n => n%2)
    
    console.log(originalArr)     // console output => [1, 2, 3, 4, 5]
    console.log(noEvenNumbersArr)  // console output => [1, 3, 5]
    

    .REDUCE()

    The .reduce() method, like the other Array methods mentioned, loops through all of it's items in a given Array. It's argument parameters are different than .map() and .filter(). Also, while .map() and .filter() ALWAYS return an Array, .reduce() offers the ability to return and Object instead. .reduce() takes a bit more time to properly explain than the other two methods discussed. Also, using what we've discussed using .map() and .filter(), a solution can be formed to properly address the problem originally posed.

    THE SOULTION

    // The Array "items" contains values that need to be removed.
    
    const items = [
      { title: 'Bokningsbar',
        start: moment("2018-04-05 06:00"),
        end: moment("2018-04-05 07:00"),
        allDay: false },
    
      { title: 'Bokningsbar',
        start: moment("2018-04-05 06:00"),
        end: moment("2018-04-05 07:00"),
        allDay: false },
    
      { title: 'Bokningsbar',
        start: moment("2018-04-05 06:00"),
        end: moment("2018-04-05 07:00"),
        allDay: false }, 
    
      { title: 'Bokningsbar',
        start: moment("2018-04-05 06:00"),
        end: moment("2018-04-05 07:00"),
        allDay: false }
    ]
    
    
    // The Array "datesToRemove" contains values that match what needs
    // to be removed from the"items" Array
    
    const datesToRemove = [
      { title: 'Bokningsbar',
        start: moment("2018-04-05 06:00"),
        end: moment("2018-04-05 07:00"),
        allDay: false }
    ]
    

    The .filter() method can handle everything needed for a solution to the problem... Almost. The problem now revolves around 2 things specifically, 1. Not filtering an Array of items against another Array of items. Instead, you're trying to filter an Array of OBJECTS against another Array of OBJECTS. If that is the intention here, you can modify the following to check for native Object methods (and iterators) such as Object.keys() and Object.values().

      const mergeUniqueMatches = (catArr) => catArr.filter((elem, pos, arr) => arr.indexOf(elem) == pos)
      const mergedArr = mergeUniqueMatches(items.concat(datesToRemove))  
    

    Logging mergedArr to the console gives shows a new Array. The original data is still perfectly untouched. Again, shallow copies are what we are working with here.

    Now the trick is to "Dedupe" the mergedArr. Lucky for us, ES6/7 makes this a piece of cake. Try:

    const dedupedArr = [...new Set(mergedArr)]  
    

    Moving any further though requires some clarification on your end. You could make this much easier for yourself by providing a key (like generated a uuid) any time and items object is added to the Array. Then after that item moves to the datesToRemove Array, all you need to do is a .filter() off the instance ID key (uuid) to remove the right items safely, and without worrying about mutating any data along the way.

    I'll leave you with these two finally alternatives,

      const destructingOptionWithES6 = {
        ...items,
        ...datesToRemove
      }
    
      const ideallyAllYouShouldHaveToDoIsThisFunction = items.filter(val => !datesToRemove.includes(val))