Philipp Claßen
Philipp Claßen

Reputation: 43970

How to filter a JavaScript Map?

Given an ES6 Map and predicate function, how do I safely delete all non-matching elements for the map?


I could not find an official API function, but I can think of two implementations. The first does not attempt to delete in-place, but instead creates a copy:

// version 1:
function filter(map, pred) {
  const result = new Map();
  for (let [k, v] of map) {
    if (pred(k,v)) {
      result.set(k, v);
    }
  }
  return result;
}

const map = new Map().set(1,"one").set(2,"two").set(3,"three");
const even = filter(map, (k,v) => k % 2 === 0);
console.log([...even]); // Output: "[ [ 2, 'two' ] ]"

The other deletes in-place. In my tests, it works but I did not find a guarantee that modifying a map does not break the iterator (of the for-of loop):

// version 2:
function deleteIfNot(map, pred) {
  for (let [k, v] of map) {
    if (!pred(k,v)) {
      map.delete(k);
    }
  }
  return map;
}

const map = new Map().set(1,"one").set(2,"two").set(3,"three");
deleteIfNot(map, (k,v) => k % 2 === 0);
console.log([...map]); // Output: "[ [ 2, 'two' ] ]"

Question:

Upvotes: 105

Views: 182277

Answers (6)

double-beep
double-beep

Reputation: 5504

A proposal adding support for Map.prototype.filter is now on Stage 1.

Once Map.prototype.filter is implemented, you will be able to write:

const myMap = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three']
]);

// value goes first!
const filtered = myMap.filter((value, key) => key % 2 === 0);

Until then, you can use this core-js polyfill:

const myMap = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three']
]);

// value goes first!
const filtered = myMap.filter((value, key) => key % 2 === 0); // Map(1) {2 => 'two'}
console.log([...filtered]);
<script src="https://unpkg.com/[email protected]/minified.js"></script>

Upvotes: 1

smr
smr

Reputation: 984

If the goal is to create a new Map with filtered data, there is a solution by using the iterator.filter function.

NOTE: This feature is currently in an experimental stage.

In this case, the code runs twice as fast as using the spread operator or converting the map to an Array.
I used perf.link for benchmarking, so there may be some errors in the results.

const map = new Map().set(1,"one").set(2,"two").set(3,"three");

// Keys that are included in [1, 2]
iter1 = map.entries().filter(([key, value]) => [1,2].includes(key)); 
// Values equal to two
iter2 = map.entries().filter(([key, value]) => value == "two");
// 100 First elements
iter3 = map.entries().filter((_, index) => index < 100); 

// The resulted iterators can be converted into a new Map or an Array.
Array.from(iter1); // or new Map(iter1);

Upvotes: 1

technik
technik

Reputation: 1076

In case you want to use the immutable Map (immutable-js.com) library your code will even be one line in combination with the lodash. Basically, as it is written in the official technical specifications of the immutable Map, the filter() function does NOT return a mutated Map, but it mutates the source. That is why the source should be cloned first.

import { Map } from 'immutable';

_.cloneDeep(Map()).filter(predicate);

Here is working example:

import _ from 'lodash';
import { describe, it } from 'mocha';
import { Map } from 'immutable';

const mixedImmutableMap = Map([[1, 'Some string'], [2, 777], [3, { name: 'User' }], [4, 1234], [5, [1, 2, 3, 4]]]);

describe('Test immutable Map filtering', () => {
  describe('Test filtering map with mixed value types to remove the numbers', () => {
    const result = _.cloneDeep(mixedImmutableMap).filter((value) => !_.isNumber(value));
    it('Should have defined result', () => {
      expect(result).to.be.not.undefined;
    });
    it('Should have result of type immutable Map', () => {
      expect(result instanceof Map).to.be.true;
    });
    it('Should the result not mutate the source', () => {
      expect(result).to.be.not.equal(mixedImmutableMap);
    });
    it('Should actually filter out the map number values', () => {
      expect(result.size).to.be.equal(3);
    });
    console.log(result.toArray());
  });
});

Results output:

result --> [ 'Some string', { name: 'User' }, [ 1, 2, 3, 4 ] ]


  Test immutable Map filtering
    Test filtering map with mixed value types to remove the numbers
      √ Should have defined result
      √ Should have result of type immutable Map
      √ Should the result not mutate the source
      √ Should actually filter out the map number values


  4 passing (14ms)

Upvotes: 0

Roman
Roman

Reputation: 21765

If we want to use .filter() iterator for a map, we can apply a simple trick, because there is no .filter operator for ES6 Maps. The approach from Dr. Axel Rauschmayer is:

  • Convert the map into an array of [key, value] pairs.
  • Map or filter the array.
  • Convert the result back to a map.

Example:

const map0 = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3]
]);

const map1 = new Map(
  [...map0]
  .filter(([k, v]) => v < 3 )
);

console.info([...map1]); 
//[0: ["a", 1], 1: ["b", 2]]

Upvotes: 109

C B
C B

Reputation: 13314

// Given a map with keys a, b

const m = new Map()
m.set('a', 1)
m.set('b', 2)

// Return a new map that do not match key a

new Map([...m].filter(([k, v])=>k!=='a'))

// Return a new map only matching value 2

new Map([...m].filter(([k, v])=>v===2))

// Filter in place, removing all values not 2

[...m].map((i)=>i[1]!==2 && m.delete(i[0]))

Upvotes: 3

Estus Flask
Estus Flask

Reputation: 222369

ES6 iterables have no problems when an entry is deleted inside a loop.

There is no special API that would allow to efficiently filter ES6 map entries without iterating over them.

If a map doesn't have to be immutable and should be modified in-place, creating a new map on filtering provides overhead.

There is also Map forEach, but it presumes that value will be used, too.

Since the map is being filtered only by its key, there's no use for entry object. It can be improved by iterating over map keys:

for (let k of map.keys()) {
  if (!(k % 2))
    map.delete(k);
}

Upvotes: 44

Related Questions