filterRangeInPlace(arr, a, b) that gets an array arr and removes from it all values except those that are between a and b

Viewed 271

Below function works nice

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // remove if outside of the interval
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

arr=[2,3,4,5,6,7,1,0,2,12,11];
filterRangeInPlace(arr,1,5);
alert(arr);

but I want to do the same function with the forEach

let filterRange = (arr, a, b) => {
  arr.forEach((item, i) => {
    val=item;debugger;
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  })
}
arr=[2,3,4,5,6,7,1,0,2,12,11];
filterRange(arr,1,5);
alert(arr);

I tried it, but I could not get the right answer. It doesn't work as normal iterations. is there any another way of using the same foreach

2 Answers

You cannot do an inplace modification of the array using forEach as you cannot control the index over which the looping occurs inside forEach

A trivial implementation of forEach would be like

Array.prototype.forEach = function(cb) {
   const arr = this;
   for(var i = 0; i < arr.length; i++) {
     cb.call(arr, arr[i], i, arr);
   }
}

If you look at the above people the callback is just called with the arr[i] value and if the array is mutated, you won't be able to mutate the internal iterator

For usecases like yours, you should use Array.prototype.filter

let filterRange = (arr, a, b) => {
  return arr.filter((item, i) => {
    val=item;
    if (val < a || val > b) {
      return false;
    }
    return true;
  })
}
var arr=[2,3,4,5,6,7,1,0,2,12,11];
arr = filterRange(arr,1,5);
alert(arr);

The call to forEach creates the range of elements that will be processed before starting to process them.

So when you access i you're accessing the value at the time of calling forEach, and not at the time that the callback function is run. Also, as an aside, i-- does nothing in the forEach version of your algorithm, because it's not accessible to the other instances of the callback.

Furthermore, if you remove an element then the next element will be skipped as the elements are shifted to line up with the iterator.

So what is processed 2,3,4,5,6,[skip],1,0,[skip],12,[skip]. Which is why you still see 7 and 11 in your output.

To get the desired effect you can go with Shubham Khatri's answer, which doesn't mutate the array, but overwrites it. It is the method I would choose.

However if you really want to mutate the array with forEach you need to keep track of how far you've deviated and course correct as you go:

let filterRange = (arr, a, b) => {
  let offset = 0;
  arr.forEach((item, i, inPlace) => {
    if (item < a || item > b) {
      inPlace.splice(i, 1);
      offset++;
    }
    let temp_offset = offset;
    while(temp_offset > 0)
    {
      let offset_item = inPlace[i-temp_offset];
      if (offset_item < a || offset_item > b) {
        inPlace.splice(i-temp_offset, 1);
        offset++;
      }
      temp_offset--;
    }
  })
}
arr=[2,3,4,5,6,7,1,0,2,12,11,4,99,2];
filterRange(arr,1,5);
console.log('result',arr);

This method is worse than your original for loop.

Related