How can I create a two-way mapping in JavaScript, or some other way to swap out values?
Asked Answered
E

11

48

I currently have a need to temporarily swap out values in a JavaScript string, and therefore I will need to have a two-way map/hash thing.

For example, let's say I want to change \* to __asterisk__ (this is just an example, it's not what I'm actually trying to do). I'll have to be able to map * to __asterisk__ (to swap out the value in the original string), but then I'll also have to be able to map __asterisk__ back to * (to get the original string back).

Here's some quick pseudo-ish code of the kind of thing that I'm looking for, so you can understand it better:

var myString = 'this is \* a test';

// ???
var twoWayMap = new TwoWayMap('*' <---> '__asterisk__', '%' <---> '__percent__', ...);

var newString = myString.replace(/\\(.)/g, function(m, c) {
    return twoWayMap.getKey(c);
});
// newString is now 'this is __asterisk__ a test'

// ... later in the code ...

var oldString = newString.replace(/__([^_]+)__/g, function(m, c) {
    return twoWayMap.getValue(c);
});
// oldString is now 'this is * a test'

This is what I've thought about and tried so far:

var twoWayMap = {'*': '__asterisk__', '%': '__percent__', ...};

// getKey would be like this:
twoWayMap[c];
// getValue would be like:
var val; for (var x in twoWayMap) { if (twoWayMap[x] === c) { val = x; break } }

The obvious problem with this is that the way to get by value is much too complicated, and I don't want to have to write out the whole thing every single time I have to reverse lookup.

I just wanted to know: Is there any way to solve this problem without resorting to looping through an object? If not, is there any way to make it easier or cleaner?

Englishman answered 12/1, 2014 at 3:7 Comment(4)
You can specify the first map with typing and then use initialization code to build the reverse map so at run-time you have maps for both directions.Argeliaargent
@Doorknob I added an answer, I think it's elegant, please check itMessy
@EdgarVillegasAlvarado I know; I already upvoted it.Englishman
Your original solution is not too bad, for non-time-critical stuff...Demark
M
12

Use two objects. One object contains the * -> _asterisk_ mapping, the other object contains _asterisk_ -> *.

var forwardMap = {'*': '__asterisk__', '%': '__percent__', ...};
var reverseMap = {};
for (var key in forwardMap) {
    if (forwardMap.hasOwnProperty(key)) {
        reverseMap[forwardMap[key]] = key;
    }
}
Madelainemadeleine answered 12/1, 2014 at 3:9 Comment(2)
So then I would have to manually copy down every single key/value pair in reverse to the other object? -_-Englishman
You don't have to do it manually. If you implement a class, the method for adding a mapping can update both objects. Or write one mapping and then use a loop to create the other mapping.Madelainemadeleine
M
56

With an extra internal object for reverse mapping. Best if we add a utility class ;) like the following:

ES6 syntax (scroll down for ES5 syntax)

class TwoWayMap {
    constructor(map) {
       this.map = map;
       this.reverseMap = {};
       for(const key in map) {
          const value = map[key];
          this.reverseMap[value] = key;   
       }
    }
    get(key) { return this.map[key]; }
    revGet(key) { return this.reverseMap[key]; }
}

Then you instantiate like this:

const twoWayMap = new TwoWayMap({
   '*' : '__asterisk__', 
    '%' : '__percent__',
   ....
});

Finally, to use it:

twoWayMap.get('*')   //Returns '__asterisk__'
twoWayMap.revGet('__asterisk__')  //Returns '*'

Bonus: If you also need set/unset methods, you can do it (inside the class) easily like:

set(key, value) { this.map[key] = value; }
unset(key) { delete this.map[key] }
// same for revSet and revUnset, just use this.reverseMap instead

Equivalent with ES5 (old js) syntax:

function TwoWayMap(map) {
   this.map = map;
   this.reverseMap = {};
   for(var key in map) {
      var value = map[key];
      this.reverseMap[value] = key;   
   }
}
TwoWayMap.prototype.get = function(key){ return this.map[key]; };
TwoWayMap.prototype.revGet = function(key){ return this.reverseMap[key]; };

Usage is the same:

var twoWayMap = new TwoWayMap({
   '*' : '__asterisk__', 
    '%' : '__percent__',
   ....
});
twoWayMap.get('*')   //Returns '__asterisk__'
twoWayMap.revGet('__asterisk__')  //Returns '*'

Hope this helps. Cheers

Messy answered 12/1, 2014 at 3:14 Comment(5)
This should be the accepted answer. Edgar, would you mind updating it and adding an ES6 syntax code pls?Wellington
@Wellington Sure, I edited my answer. Let me know if you need add/remove methodsMessy
If you're thinking about also setting the corresponding key in reverseMap in the set() method, make sure you implement a check so you don't end up in a situation where two keys in map point to the same value, since the corresponding value in reverseMap can only point to one key.Rimini
I don't understand how this is not the accepted answer.Auction
You could use the built in Map class instead of object as it's optimized for insertsMeader
M
12

Use two objects. One object contains the * -> _asterisk_ mapping, the other object contains _asterisk_ -> *.

var forwardMap = {'*': '__asterisk__', '%': '__percent__', ...};
var reverseMap = {};
for (var key in forwardMap) {
    if (forwardMap.hasOwnProperty(key)) {
        reverseMap[forwardMap[key]] = key;
    }
}
Madelainemadeleine answered 12/1, 2014 at 3:9 Comment(2)
So then I would have to manually copy down every single key/value pair in reverse to the other object? -_-Englishman
You don't have to do it manually. If you implement a class, the method for adding a mapping can update both objects. Or write one mapping and then use a loop to create the other mapping.Madelainemadeleine
B
10

I'd just use a plain object:

var map = { '*': '__asterisk__', '__asterisk__': '*', .... }

If you don't want to have to write all those out, take a look at the implementation of underscore's _.invert(object) here

Beamy answered 12/1, 2014 at 3:12 Comment(3)
You could at least post valid Javascript syntax.Madelainemadeleine
yeah... I'm not sure what the = and the array-like syntax after that is doing there :PEnglishman
Yeah...been writing too much lua lately. :/Beamy
H
5

Some might prefer a more succinct functional style...

const create2WayMap = (seedMap, mapName, reversemapName) => ({
  [mapName]: { ...seedMap },
  [reversemapName]: Object.keys(seedMap).reduce((map, key) => {
    const value = seedMap[key]
    return { ...map, [value]: key }
  }, {})
})

usage:

const myIDMap = create2WayMap(
  {
    1: 'SomeString',
    2: 'Another String',
    3: 'Another'
  },
  'idStrings',
  'idNumbers'
)

let id = 2
const str = myIDMap.idStrings[id] // yields 'Another String'
id = myIDMap.idNumbers[str] // yields 2
Halloween answered 1/6, 2018 at 22:0 Comment(0)
D
4

similar to @harunurhan's answer (but much less verbose) I created a small class that takes in a Typescript Map and generates a readonly two-way map:

export class TwoWayReadonlyMap<T, K> {
  map: Map<T, K>;
  reverseMap: Map<K, T>;
  constructor(map: Map<T, K>) {
    this.map = map;
    this.reverseMap = new Map<K, T>();
    map.forEach((value, key) => {
      this.reverseMap.set(value, key);
    });
  }
  get(key: T) {
    return this.map.get(key);
  }
  revGet(key: K) {
    return this.reverseMap.get(key);
  }
}
Dday answered 19/3, 2020 at 22:46 Comment(0)
M
3

I needed something similar, and created https://www.npmjs.com/package/bi-directional-map

It is implementing a 2-way map using 2 es6 Map, even though it doesn't have a much functionality, it is tested and since it's written typescript comes with official typings.

Multi answered 19/1, 2018 at 14:16 Comment(0)
L
2

So a little es17 sugar here:

const twoWayObject = (obj) => {
 const objEntries = Object.entries(obj);
 return Object.fromEntries([
 ...objEntries, 
 ...objEntries.map(e=>[...e].reverse())
 ]);
};
console.log(twoWayObject({ a: 1, b: 2 })); // { a: 1, 1: a, b: 2, 2: b }

Object.entries takes an object and returns an array of entries, an entry being a [key, value] pair array. Object.fromEntries does the inverse - it takes an array of entries and returns an object where each key ~> value.

Combining these two methods, it is possible to write functional mappings from one object to another very succinctly.

This snippet entrifies an object, and creates a new entry array where the entries are reversed (immutably). These two arrays are spread (the dot ... operator used for the reversal as well) into another array. So now we have an array of entries, that comprises of the original object entries, and their reversal. So now for every [a, b], we also have [b, a]. Calling object.fromEntries on this array then creates our two way map.

Compat

Keep in mind, this is new syntax (spread and the object methods (especially fromEntries) isn't supported in old browsers/old versions of node (I think fromEntries was added to node v12, for example)). So caution/babel may be needed.

UPDATE - TypeScript

Let's force TypeScript to get to know our two-way-object when using a discreet object type (with compile time certainty of keys and values), using this InvertResult type - our function signature will look like this:

<T extends Record<string, string>>(obj: T): T & InvertResult<T>

Remember to add a const assertion when passing in an object - otherwise TS will widen the keys and values to <string, string>, and the cool type-safety will disappear.

So, using the example above, now TS will correctly identify the exact type of any key and its reverse i.e.:

obj[1] // "a"
Lifegiving answered 4/11, 2020 at 21:41 Comment(1)
Note that if something is both a key and a value, you'll only be able to index it as a value, since the key will be replaced by the value.Accipitrine
A
2

One can also use lodash.invert package to create the inverted version of the original map.

import invert from "lodash.invert";

const map = { one: 1, two: 2, three: 3 };
const reverseMap = invert(map);

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

console.log(reverseMap[1]); // 'one'
console.log(reverseMap[2]); // 'two'
console.log(reverseMap[3]); // 'three'
Aluminate answered 18/2, 2021 at 9:22 Comment(0)
M
1

Another possible solution:


class TwoWayMap extends Map {
  constructor(entries) {
    super(entries);
    if (Array.isArray(entries)) {
      entries.forEach(entry => {
        super.set(entry[1], entry[0]);
      });
    }
  }
  set(key, value) {
    super.set(key, value);
    super.set(value, key);
    return this;
  }
}


const twoWayMap = new TwoWayMap([['a', 1]]);
twoWayMap.set('b', 2);

twoWayMap.get('a'); // 1
twoWayMap.get(1); // a
twoWayMap.get('b'); // 2
twoWayMap.get(2); // b
Memorize answered 18/12, 2020 at 14:16 Comment(1)
This would be problematic if two keys have the same value.Dday
A
1

I think the simplest way to achieve this in modern JS is to create a Map with reversed entries:

function reverse(map) {
  return new Map([...map].map(([k, v]) => [v, k]));
}

const map = new Map([['*', '__asterisk__'], ['%', '__percent__']]);
const reverseMap = reverse(map);

Note that Maps are safer than objects because they allow keys that aren't numbers, strings, or symbols, and they won't try to coerce values.

Accipitrine answered 22/12, 2022 at 16:58 Comment(0)
M
0

Here is an es6 solution that inherits from the Map class and in which each value is unique,and which has methods for values similar to methods for keys:

class TwoWayMap extends Map {
    getByValue(value) {
        const entriesIterator = this.entries();
        for (const [entryKey, entryValue] of entriesIterator) {
            if (entryValue === value) {
                return entryKey;
            }
        }
    }

    hasValue(value) {
        return !!this.getByValue(value);
    }

    deleteByValue(value) {
        const key = this.getByValue(value);
        this.delete(key);
    }

    set(key, value) {
        if(this.hasValue(value)) {
            this.deleteByValue(value);
        }
        Map.prototype.set.call(this, key, value);
    }
}
Mannerheim answered 22/4, 2024 at 15:43 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.