0

I can't understand how to pull arrays from one array out of another.

I tried using plain JavaScript (ES6):

let openTiles = [[1, 1], [2, 2], [1, 3]]
let usedTiles = [[1, 1], [2, 2]]
openTiles = openTiles.filter((item) => !usedTiles.includes(item))

I expected the final openTiles to be: [[1, 3]] but it is unchanged. The problem is, the code above uses JavaScript's standard comparison (===) which can't compare one array with another. Lodash has the _.isEqual() function but I can't understand how to implement it.

I tried:

openTiles = openTiles.filter((item) => {
    return usedTiles.every((el) => {
        _.isEqual(item, el)
    })
})

but that gives me an empty array. I would like to see how people incorporate Lodash's _.isEqual() function so that all the arrays in usedTiles can be removed from openTiles.

Tachyon80
  • 147
  • 10
  • 1
    Same thing as last time you asked, check out the first answer to the [canonical](https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript) – CertainPerformance Mar 24 '19 at 00:42
  • I read it carefully many times and I don't understand how that applies. I don't want to .prototype arrays. I don't see how it applies to my question. – Tachyon80 Mar 24 '19 at 00:43
  • 1
    You don't have to define a method on the prototype, but you can use the same general method just fine, just check whether the value at every index is the same, and that the lengths are the same – CertainPerformance Mar 24 '19 at 00:45
  • I can't use the same general method just fine. I have a legitimate question and it is not addressed in the link you keep pushing. Please take the [duplicate] off my question and let me hear from someone who would like to try and help. – Tachyon80 Mar 24 '19 at 01:49
  • @CertainPerformance: I concur with Tachyon80. This is asking a real question not answered by the dup. I'll try an answer. – Scott Sauyet Mar 24 '19 at 01:50
  • If you need to do this kind of thing a lot, you might want to take a look at [immutable.js](https://immutable-js.github.io/immutable-js/) instead of using plain Javascript Arrays. – solarc Mar 24 '19 at 02:53

3 Answers3

3

A simple method would be to stringify the arrays you want to compare against, so that .includes will work properly:

let openTiles = [[1, 1], [2, 2], [1, 3]]
let usedTiles = [[1, 1], [2, 2]]
const usedTilesStringified = usedTiles.map(JSON.stringify);
openTiles = openTiles.filter((item) => !usedTilesStringified.includes(JSON.stringify(item)))
console.log(openTiles);

Or you can compare every value explicitly:

let openTiles = [[1, 1], [2, 2], [1, 3]];
let usedTiles = [[1, 1], [2, 2]];
openTiles = openTiles.filter((item) => {
  const { length } = usedTiles;
  outer:
  for (let i = 0, { length } = usedTiles; i < length; i++) {
    const usedTile = usedTiles[i];
    if (usedTile.length !== item.length) {
      continue;
    }
    for (let j = 0, { length } = usedTile; j < length; j++) {
      if (usedTile[j] !== item[j]) {
        // The current subarrays being compared are not identical:
        continue outer;
      }
    }
    // We've iterated through all indicies while comparing one subarray to another
    // they all match, so the arrays are the same, so this filter fails:
    return false;
  }
  return true;
})
console.log(openTiles);

For a less generic solution that just checks whether index 0 is equal to index 1:

let openTiles = [[1, 1], [2, 2], [1, 3]];
let usedTiles = [[1, 1], [2, 2]];
openTiles = openTiles.filter((item) => {
  for (let i = 0, { length } = usedTiles; i < length; i++) {
    const usedTile = usedTiles[i];
    if (usedTile[0] === item[0] && usedTile[1] === item[1]) {
      return false;
    }
  }
  return true;
})
console.log(openTiles);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • how about simply `.join(',')` them instead of `stringify`-in ? – num8er Mar 24 '19 at 01:55
  • 2
    That method may return false positives if the arrays contain string values which have commas. (or if the arrays contain different types which coerce to string the same way, like `3` and `'3'`) – CertainPerformance Mar 24 '19 at 01:56
  • I understand that, but in current question it's ineffective. In example we have just arrays of 2 or 3 items. As I understand it's about map tiles, and it may cause rendering performance if we stringify every array of 2 numbers. But I have to say Scott's answer more wise solution for map tiles. – num8er Mar 24 '19 at 01:59
  • I had not thought of trying things this way. Thank you. – Tachyon80 Mar 24 '19 at 20:06
3

Rather than trying to write a general-purpose object- or array-equals function, or using the ones from lodash, Ramda, underscore, etc, you can write one specific to your type. If your tiles are just two-element arrays, then simply write an equals function that reflects that. Then you can use it in some:

const tilesEqual = (a) => (b) => a[0] == b[0] && a[1] == b[1]
const removeTiles = (open, used) => open.filter(
  tile => !used.some(tilesEqual(tile))
)

let openTiles = [[1, 1], [2, 2], [1, 3]]
let usedTiles = [[1, 1], [2, 2]]

console.log(removeTiles(openTiles, usedTiles))

As to why your code above didn't work, there are two problems. You don't want to know if every used tile matches your current one. You only want to know is some of them do. Then because those are the ones you want to remove, you need to negate this for filter. So you want something like !usedTiles.some(...).

But there is another problem. You don't return anything in the callback to every/some:

    return usedTiles.every((el) => {
        _.isEqual(item, el)
    })

You need to switch this to either

    return usedTiles.every((el) => _.isEqual(item, el))

or

    return usedTiles.every((el) => {
        return _.isEqual(item, el)
    })

It's an easy mistake to make, and it's quite common. If you're using an arrow function with a {-} delimited block, you need a return statement.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Thank you, Scott. Your solution at top works great and doesn't require additional libraries. Thanks also for the advice on "return" statements. Question: in const tilesEqual = (a) => (b) => a[0] == b[0] && a[1] == b[1] I see 2 ES6 arrows in a row. Does that mean a is the same as b and they both send variables to the a[0] == b[0] code block? – Tachyon80 Mar 24 '19 at 20:08
1

To subtract one array from another lodash contains a series of difference methods.

Since the value in your case is an array, simple equality won't work because [] !== []. Lodash's _.isEqual() performs a deep comparison between two values.

To combine a difference subtraction with an _.isEqual() check use _.differenceWith().

let openTiles = [[1, 1], [2, 2], [1, 3]]
let usedTiles = [[1, 1], [2, 2]]
openTiles = _.differenceWith(openTiles, usedTiles, _.isEqual)

console.log(openTiles)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209