Skip to content

Instantly share code, notes, and snippets.

@ViaRationis
Last active November 15, 2023 07:12
Show Gist options
  • Save ViaRationis/5ea2b18d524337d8ae44666c2de7a187 to your computer and use it in GitHub Desktop.
Save ViaRationis/5ea2b18d524337d8ae44666c2de7a187 to your computer and use it in GitHub Desktop.
Bidirectional Map. Unique-valued map where you can look up keys by value.

Bidirectional Map

This module provides BidirectionalMap, StrictBidirectionalMap, and SilentBidirectionalMap classes which extend the built-in JavaScript Map to additionally allow accessing keys from their values.

  • BidirectionalMap links keys and values in both directions. Looking up by key returns the value, looking up by value returns the key.
  • Values stay unique - assigning an existing value to a new key will delete the old key.
  • StrictBidirectionalMap instead throws an error if you try to assign an existing value to a new key.
  • SilentBidirectionalMap will instead ignore/no-op if you try to assign an existing value to a new key.

Installation

NPM Version

npm install @viarationis/bidirectional-map

Usage

import BidirectionalMap from '@viarationis/bidirectional-map';

const map = new BidirectionalMap();

map.set('key1', 'value1');
map.has('key1'); // true
map.hasValue('value1'); // true
map.get('key1'); // 'value1' 
map.getKey('value1'); // 'key1'

map.set('key2', 'value1'); // Deletes 'key1' and remaps 'value1' to 'key2'
map.deleteValue('value1'); // Deleted 'key2' and the map is now empty

import { StrictBidirectionalMap, SilentBidirectionalMap } from '@viarationis/bidirectional-map';

const mapStrict = new StrictBidirectionalMap();
mapStrict.set('key1', 'value1');
mapStrict.set('key2', 'value1'); // Throws error: "Cannot set duplicate value on StrictBidirectionalMap: value1"

const mapSilent = new SilentBidirectionalMap();
mapSilent.set('key1', 'value1');
mapSilent.set('key2', 'value1'); // Does nothing, 'value1' is still mapped to 'key1'

API

The maps provide the following methods in addition to the standard Map API:

  • getKey(value) - Get the key for a value
  • hasValue(value) - Check if the map contains a value
  • deleteValue(value) - Delete a value and its key

To help avoid ambiguity, these aliases of standard Map methods are also provided:

  • getValue(key) - Alias of get(key)
  • hasKey(key) - Alias of has(key)
  • deleteKey(key) - Alias of delete(key)

License

Released under the CC0-1.0 License.

// bidirectional-map.js
// BidirectionalMap, StrictBidirectionalMap, SilentBidirectionalMap
// Two-way map. Same API as Map with the addition of getKey, hasValue, and deleteValue.
// In this version, values will stay unique by deleting previous keys.
export class BidirectionalMap extends Map {
#inverseMap = new Map();
constructor(entries) {
super();
if (entries) {
for (const [key, value] of entries) {
this.set(key, value);
}
}
}
set(key, value) {
if (this.#inverseMap.has(value)) {
const oldKey = this.#inverseMap.get(value);
super.delete(oldKey);
}
if (super.has(key)) {
const oldValue = super.get(key);
this.#inverseMap.delete(oldValue);
}
this.#inverseMap.set(value, key);
return super.set(key, value);
}
getKey(value) {
return this.#inverseMap.get(value);
}
getValue(value) { // Alias
return this.get(value);
}
hasKey(key) { // Alias
return this.has(key);
}
hasValue(value) {
return this.#inverseMap.has(value);
}
delete(key) {
if (super.has(key)) {
const value = super.get(key);
this.#inverseMap.delete(value);
return super.delete(key);
}
return false;
}
deleteKey(key) { // Alias
return this.delete(key);
}
deleteValue(value) {
if (this.#inverseMap.has(value)) {
const key = this.#inverseMap.get(value);
this.#inverseMap.delete(value);
return super.delete(key);
}
return false;
}
clear() {
this.#inverseMap.clear();
return super.clear();
}
}
export default BidirectionalMap;
// This version throws an error on any attempt to set an existing value to a new key.
export class StrictBidirectionalMap extends BidirectionalMap {
set(key, value) {
if (this.hasValue(value)) {
throw new Error(`Cannot set duplicate value on StrictBidirectionalMap: ${value}`);
}
return super.set(key, value);
}
}
// This version silently ignores any attempt to set an existing value to a new key.
export class SilentBidirectionalMap extends BidirectionalMap {
set(key, value) {
if (this.hasValue(value)) {
return this;
}
return super.set(key, value);
}
}
// bidirectional-map.test.js
import { expect } from 'chai';
import { BidirectionalMap, StrictBidirectionalMap, SilentBidirectionalMap } from './bidirectional-map.js';
describe('BidirectionalMap', () => {
let map;
it('constructor() initializes with entries passed as an argument', () => {
const map = new BidirectionalMap([['key1', 'value1'], ['key2', 'value2']]);
expect(map.size).to.equal(2);
});
beforeEach(() => {
map = new BidirectionalMap();
});
it('set() links a key and value in both directions', () => {
map.set('key1', 'value1');
expect(map.get('key1')).to.equal('value1');
expect(map.getKey('value1')).to.equal('key1');
});
it('set() deletes the old key if an existing value is assigned to a new key', () => {
map.set('key1', 'value1');
map.set('key2', 'value1');
expect(map.has('key1')).to.be.false;
expect(map.get('key2')).to.equal('value1');
});
it('getKey() returns the key for a value', () => {
map.set('key1', 'value1');
expect(map.getKey('value1')).to.equal('key1');
});
it('hasValue() checks if a value exists', () => {
map.set('key1', 'value1');
expect(map.hasValue('value1')).to.be.true;
expect(map.hasValue('value2')).to.be.false;
});
it('delete() removes a key-value pair', () => {
map.set('key1', 'value1');
map.delete('key1');
expect(map.has('key1')).to.be.false;
expect(map.hasValue('value1')).to.be.false;
});
it('deleteValue() removes a value and its key', () => {
map.set('key1', 'value1');
map.set('key2', 'value1');
map.deleteValue('value1');
expect(map.has('key1')).to.be.false;
expect(map.has('key2')).to.be.false;
expect(map.hasValue('value1')).to.be.false;
});
it('clear() removes all key-value pairs', () => {
map.set('key1', 'value1');
map.set('key2', 'value2');
map.clear();
expect(map.size).to.equal(0);
});
});
describe('StrictBidirectionalMap', () => {
let map;
beforeEach(() => {
map = new StrictBidirectionalMap();
});
it('set() throws an error if an existing value is assigned to a new key', () => {
map.set('key1', 'value1')
expect(() => map.set('key2', 'value1')).to.throw(`Cannot set duplicate value on StrictBidirectionalMap: value1`);
});
});
describe('SilentBidirectionalMap', () => {
let map;
beforeEach(() => {
map = new SilentBidirectionalMap();
});
it('set() ignores an assignment if the value is already assigned to another key', () => {
map.set('key1', 'value1');
map.set('key2', 'value1');
expect(map.get('key1')).to.equal('value1');
expect(map.has('key2')).to.be.false;
});
});
{
"name": "@viarationis/bidirectional-map",
"description": "Map, but extended to be bidirectional. Values will stay unique by deleting previous keys. Also a 'strict' version, which throws on duplicates instead, and a 'silent' version, which ignores assignments of existing values.",
"version": "1.0.1",
"license": "CC0-1.0",
"keywords": [
"bidirectionalmap",
"strictbidirectionalmap",
"silentbidirectionalmap",
"uniquemap",
"unique value map",
"two-way map",
"key-value",
"data structure",
"bimap",
"associative array"
],
"author": "@ViaRationis",
"repository": {
"type": "git",
"url": "gist:5ea2b18d524337d8ae44666c2de7a187"
},
"main": "bidirectional-map.js",
"type": "module",
"scripts": {
"test": "mocha '**/**.test.js'"
},
"devDependencies": {
"chai": "^4.3.10",
"mocha": "^10.2.0"
}
}
@ViaRationis
Copy link
Author

I was considering this for a project I was working on, and went ahead and coded it for fun and good practice. I likely won't be using it on this project though, as it isn't quite necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment