How can I create a two-way mapping in JavaScript,

2020-06-08 03:18发布

问题:

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?

回答1:

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


回答2:

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

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]; };

Then you instantiate like this:

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

Then, for using it:

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

EDIT: Equivalent with ES6 syntax

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

Usage is the same

Hope this helps. Cheers



回答3:

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



回答4:

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.



回答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


回答6:

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