How can I get a key in a JavaScript 'Map' by its value?
Asked Answered
J

13

89

I have a JavaScript 'Map' like this one

let people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');

I want some method to return a key by its value.

let jhonKey = people.getKey('jhon'); // jhonKey should be '1'
Jonette answered 6/11, 2017 at 11:13 Comment(5)
Then why did you store it the wrong way round? :-PGaudy
Why do you need to question a requirement?Mestas
@Gaudy Probably because he also needs to access it the other way around.Stead
I would suggest something different than the top answers posted here. Rather than iterating which costs CPU, store another map which is an inversion. You could create a wrapper class with convenience methods and use it for read/write ops on the 2 maps. Memory is cheaper than CPU.Despain
I landed here with the exact same issue. Questioning the requirement made me think of my implementation. Turns out storing the other way around was the simplest solution since it won't require extra processing (loops).Fulguration
B
54

You could convert it to an array of entries (using [...people.entries()]) and search for it within that array.

let people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');
    
let jhonKeys = [...people.entries()]
        .filter(({ 1: v }) => v === 'jhon')
        .map(([k]) => k);

console.log(jhonKeys); // if empty, no key found otherwise all found keys.
Blade answered 6/11, 2017 at 11:17 Comment(11)
@Rajesh, actually, we need the key, not the index.Blade
Actually we need the key not the index or value.. :)Hsining
@NinaScholz I am curious, are you really using this kind of one-liners in your job, and whether you are debugging your code in any way?Mincemeat
@Engineer, in this case, i would use a different data structure, not a map. but the code is pretty straight forward.Blade
@NinaScholz You don't need the array access and || logic if you destructure the map values directly: [...people.values()]Barbusse
@philraj, i changed the answer. anothher solution could be the use of Array.from with a mapping of the value.Blade
@NinaScholz weird, I didn't know about that trick. edit What's the { 1: v } syntax? You're destructuring the key value pair as an object? Why not [k, v] insteadBarbusse
@philraj, it's an array for destructuring. and if you have an element at a certain index, you could use the index for destructuring with an new variable.Blade
This should not be the answer, it returns the value by searching for the .... valueDearden
@JohnB, right. but now it searches all values and return an array of keys. thanks for pointing out.Blade
This shouldn't be the answer because it unnecessarily creates an array. See my answer for a better solution.Irrelievable
C
81

You can use a for..of loop to loop directly over the map.entries and get the keys.

function getByValue(map, searchValue) {
  for (let [key, value] of map.entries()) {
    if (value === searchValue)
      return key;
  }
}

let people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');

console.log(getByValue(people, 'jhon'))
console.log(getByValue(people, 'abdo'))
Consuetudinary answered 6/11, 2017 at 11:33 Comment(4)
One advantage to this solution is possibly speed, as there is no array transform. To really get into the spirit of ES6 though, might be nice if you change the var to a const..Hsining
Nice one,. just a heads up const works here too in for of. It's doesn't work like a normal for in this regard.Hsining
Just my personal preference to use let over const.Consuetudinary
You don't need the map.entries(), the map itself acts as an iterator. I submitted an edit.Barbusse
B
54

You could convert it to an array of entries (using [...people.entries()]) and search for it within that array.

let people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');
    
let jhonKeys = [...people.entries()]
        .filter(({ 1: v }) => v === 'jhon')
        .map(([k]) => k);

console.log(jhonKeys); // if empty, no key found otherwise all found keys.
Blade answered 6/11, 2017 at 11:17 Comment(11)
@Rajesh, actually, we need the key, not the index.Blade
Actually we need the key not the index or value.. :)Hsining
@NinaScholz I am curious, are you really using this kind of one-liners in your job, and whether you are debugging your code in any way?Mincemeat
@Engineer, in this case, i would use a different data structure, not a map. but the code is pretty straight forward.Blade
@NinaScholz You don't need the array access and || logic if you destructure the map values directly: [...people.values()]Barbusse
@philraj, i changed the answer. anothher solution could be the use of Array.from with a mapping of the value.Blade
@NinaScholz weird, I didn't know about that trick. edit What's the { 1: v } syntax? You're destructuring the key value pair as an object? Why not [k, v] insteadBarbusse
@philraj, it's an array for destructuring. and if you have an element at a certain index, you could use the index for destructuring with an new variable.Blade
This should not be the answer, it returns the value by searching for the .... valueDearden
@JohnB, right. but now it searches all values and return an array of keys. thanks for pointing out.Blade
This shouldn't be the answer because it unnecessarily creates an array. See my answer for a better solution.Irrelievable
M
19

JavaScript Map and Object

Given a JavaScript Map, I like Nitish's answer:

// JavaScript Map

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

function getKey(val) {
  return [...map].find(([key, value]) => val === value)[0];
}

console.log(getKey('one'));   // 1
console.log(getKey('two'));   // 2
console.log(getKey('three')); // 3

For a JavaScript object, you could do something like this:

// JavaScript Object

const map = {
  1: 'one',
  2: 'two',
  3: 'three',
};

function getKey(val) {
  return Object.keys(map).find(key => map[key] === val);
}

console.log(getKey('one'));   // 1
console.log(getKey('two'));   // 2
console.log(getKey('three')); // 3
Metallo answered 16/12, 2019 at 16:11 Comment(2)
nice, for the Map one in order to not error if the value match is never found and just return undefined instead, the function can be [...map].find(([key, value]) => val === value)?.[0]Clareta
Which one is performance optimized one ([...map].find or map.foreach) ?Burkitt
B
18

Though late and other great answers already exist, still you can give the below "..." and Array.find a try:

let people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');

function getKey(value) {
  return [...people].find(([key, val]) => val == value)[0]
}

console.log('Jasmein - ', getKey('jasmein'))
console.log('Jhon - ', getKey('jhon')) 
Bindle answered 15/11, 2018 at 6:39 Comment(1)
but what if you want O(1) lookup in both directionsRemonstrant
D
10

There isn't any direct method for picking out information in this direction, so if all you have is the map you need to loop through the set as suggested by others.

If the map/array/other is large enough that such a loop would be a performance issue and the requirement for a reverse lookup is common within the project, you could implement your own structure using a pair of maps/arrays/other with one as per the current object and the other with the key and value reversed.

That way, the reverse lookup is as efficient as the normal one. Of course, you have more work to do as you need to implement each method that you need as a pass-through to one or both of the underlying objects so if the map is small and/or the reverse lookup is not needed often the scan-via-loop option is likely to be preferable due to being simpler to maintain and possible simpler for the JiT compiler to optimise.

In any case, one thing to be wary of is the possibility that multiple keys could have the same value. If this is possible then when looping through your map you need to decide if you are fine to return one of the possible keys arbitrarily (probably the first one) or if you want to return an array of keys, and if implementing a reverse index for data that could have duplicate values the same issue also needs to be accounted for.

Designed answered 6/11, 2017 at 15:36 Comment(0)
I
4

Here is a properly typed Typescript solution that doesn't unnecessarily create an array.

function find_map_value<K, V>(m: Map<K, V>, predicate: (v: V) => boolean): [K, V] | undefined {
  for (const [k, v] of m) {
    if (predicate(v)) {
        return [k, v];
    }
  }
  return undefined;
}

If you want all values you can use a generator:

function* find_all_map_values<K, V>(m: Map<K, V>, predicate: (v: V) => boolean): Generator<[K, V]> {
  for (const [k, v] of m) {
    if (predicate(v)) {
        yield [k, v];
    }
  }
}
Irrelievable answered 18/5, 2021 at 12:11 Comment(2)
This only returns the first matching entry. If more than one key maps to the same target value, those other matching keys will be ignored.Raiment
Yes that's correct. If you want them all just change return to yield and update the return type appropriately.Irrelievable
W
3

One could invert the Map so that the keys are the values and the values are the keys and then lookup the original value as a key. Here's an example:

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

let invertedMap = new Map([...myMap.entries()].map(
  ([key, value]) => ([value, key]))
);

console.log(invertedMap.get('one'))
// => 1
Weider answered 24/2, 2020 at 21:45 Comment(2)
but what happens when the value is a duplicate?Valaree
@Valaree Using duplicate values as keys is problematic given keys require uniqueness. However, even in this case the invertedMap will reflect the value of the last of the duplicate values.Weider
F
3

Why not simply make use of map's built in iterator prototype/instance reference looking for the target value? Injection into the prototype chain/polyfill inspired solution of sorts makes it universal to ones code:

Map.prototype.getKey = function(targetValue){
  let iterator = this[Symbol.iterator]()
  for (const [key, value] of iterator) {
    if(value === targetValue)
      return key;
  }
}

const people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');

const jhonKey = people.getKey('jhon');
console.log(`The key for 'jhon' is: ${jhonKey}`);

For anyone curious why I added yet another answer. Most of these answers (exception, I like Rajesh's answer, but I added to the prototype chain) are doing a lot of data duplication in the name of finding a value by using the spread operator or even straight up crafting Arrays. Object.keys() mind you is also terribly nonperformant.

Note, I use for..of which iterates on iterables. One could do short hand simply with for(const [key, value] of this){...} if desired.

Fillander answered 19/4, 2021 at 2:4 Comment(2)
Having result variable outside the forEach iteration doesn't feel great to me - the result should be const, the key that is found is const. Answer by @Irrelievable seems like a better version.Elder
@Elder -- I updated per your feedback, please note the answer you referenced is typescript and the question is tagged for javasript.Fillander
A
1

Tailing off what Maciej Krawczyk suggested here is a general circular map implementation for that.

class ReferenceMap {
  #left = new Map();
  #right = new Map();

  constructor(iterable = []) {
    this.#left = new Map(iterable);
    this.#right = new Map(ReferenceMap.swapKeyValues(iterable));
  }

  has(key) {
    return this.#left.has(key) || this.#right.has(key);
  }

  get(key) {
    return this.#left.has(key) ? this.#left.get(key) : this.#right.get(key);
  }

  set(key, value) {
    this.#left.set(key, value);
    this.#right.set(value, key);
  }

  delete(key) {
    if (this.#left.has(key)) {
      let ref = this.#left.get(key);
      this.#left.delete(key);
      this.#right.delete(ref);
    } else if (this.#right.has(key)) {
      let ref = this.#right.get(key);
      this.#right.delete(key);
      this.#left.delete(ref);
    }
  }

  entries() {
    return this.#left.entries();
  }

  keys() {
    return this.#left.keys();
  }

  values() {
    return this.#left.values();
  }

  [Symbol.iterator]() {
    return this.entries();
  }

  get size() {
    return this.#left.size;
  }

  static * swapKeyValues(entries) {
    for (let [key, value] of entries) yield [value, key];
  }
}
Aloe answered 25/1, 2023 at 17:58 Comment(0)
A
0

My TypeScript version:

const getByValue = <A, B>(m: Map<A,B>, searchValue: B):[A, B] | undefined => {
  const l:IterableIterator<[A, B]> = m.entries();
  const a:[A, B][] = Array.from(l);
  return a.find(([_k,v]) => v === searchValue);
}
Alek answered 23/4, 2020 at 16:35 Comment(0)
W
0

Cache

The question is a bit wrong because one value can be assigned to many keys. Therefore, the result for a given value should be an array of keys (not a single key). If you want to oftet make such search you can use following cache generator for reverse map

let genRevMapCache = map => [...map.entries()].reduce((a,[k,v]) => {
  if(!a.get(v)) a.set(v,[]);
  a.get(v).push(k);
  return a;
}, new Map() );

let genRevMapCache = map => [...map.entries()].reduce((a,[k,v]) => {
  if(!a.get(v)) a.set(v,[]);
  a.get(v).push(k);
  return a;
}, new Map() );


// TEST

let people = new Map();
people.set('1', 'jhon');
people.set('2', 'jasmein');
people.set('3', 'abdo');
people.set('4', 'jhon');

let cache = genRevMapCache(people);

console.log('jasmein', cache.get('jasmein'));
console.log('jhon', cache.get('jhon'));
Whiney answered 20/2, 2023 at 22:58 Comment(0)
H
0

what I ended up doing to make it O(1) in all cases

class ClientService {
  private readonly clientMap = new Map<number, Client[]>();
  private readonly userMap = new Map<Client, number>();

  addClient(userId: number, client: Client) {
    const existingClients = this.clientMap.get(userId) ?? [];
    existingClients.push(client);
    this.clientMap.set(userId, existingClients);
    this.userMap.set(client, userId);
  }

  removeClient(client: Client) {
    const userId = this.userMap.get(client);

    if (!userId) {
      return;
    }

    this.userMap.delete(client);

    const existingClients = this.clientMap.get(userId);

    if (!existingClients) {
      return;
    }

    if (existingClients.length === 1) {
      this.clientMap.delete(userId);
    } else {
      // I know filter isn't O(1), but in my case I know it will be usually 1 and in general less than 5, you can user a better data structure here if in other case it can grow
      const newClients = existingClients.filter(
        (c) => client !== c,
      );
      this.clientMap.set(userId, newClients);
    }
  }

  getClients(userId: number): Client[] {
    const clients = this.clientMap.get(notification.userId) ?? [];
  }
}
Hultin answered 21/6 at 17:10 Comment(0)
P
-1

JS:

// Returns keys for all instances
function findAll(obj) {
  return Array.from(items.keys()).map(k => items.get(k) === obj ? k : undefined).filter(k => k);
}

// Returns keys for the first instances
function findFirst(obj) {
  return Array.from(items.keys()).find(k => items.get(k) === obj);
}

Typescript:

protected items = new Map<TKey, TObject>();

public findAll(obj: TObject): Array<TKey> {
  return Array.from(this.items.keys()).map(k => this.items.get(k) === obj ? k : undefined).filter(k => !!k);
}

public findFirst(obj: TObject): TKey | undefined {
  return Array.from(this.items.keys()).find(k => this.items.get(k) === obj);
}

Explanation:

// Gets the keys as an array
Array.from(this.items.keys())

// Map the keys whose object matches the instance of `obj` to the key itself, undefined otherwise
.map(k => this.items.get(k) === obj ? k : undefined)

// Filter out array elements that are undefined
// (!! is for strict type-checking/readability practices, you can simply use k => k)
.filter(k => !!k)

// Finds the first occurrence of the key for the given object, undefined if not found
.find(k => this.items.get(k) === obj)
Pahari answered 30/11, 2020 at 14:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.