Reputation: 43970
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
Reputation: 5504
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
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
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
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:
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
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
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