Tachyon80
Tachyon80

Reputation: 157

Comparing arrays of arrays with Lodash

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.

Upvotes: 0

Views: 1015

Answers (3)

Ori Drori
Ori Drori

Reputation: 191966

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>

Upvotes: 1

Scott Sauyet
Scott Sauyet

Reputation: 50787

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.

Upvotes: 3

CertainPerformance
CertainPerformance

Reputation: 370679

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);

Upvotes: 3

Related Questions