5

I have a list of keys and a value. For example:

keys = ["keyA", "keyB", "keyC"];

value = 100;

I'm trying to create a function to create a map so that:

map["keyA"]["keyB"]["keyC"] = 100;

I assumed this was the best data structure based on the answer given here:

Anyway, the part that challenges me is that I need a function that will create a map for any number of keys. I've tried doing this in a loop but can't get it to work because I don't know how to access different levels of my map, but it also feels sloppy:

for(var i=0; i<keys.length; i++){
    for(var j=0; j<i; j++){
        maps[keys[0]]...[keys[j]] = {};
        if(j+1 === i){
            maps[keys[0]]...[keys[j]][keys[i]] = value;
        }
    }
}

How can I create my map?

Community
  • 1
  • 1
PDN
  • 771
  • 2
  • 13
  • 29

5 Answers5

4

Edit 24/12/2022

I have created an ES module for order agnostic multi map. I will explain here how you can set it up for the OP's use case.
https://github.com/martian17/ds-js

First you will want to clone the repository to your project, or copy the code.

$ git clone https://github.com/martian17/ds-js.git

Here is an example use case

// import it to your project
import {OrderAgnosticMultiMap} from "path_to_ds-js/multimap.mjs";

// Instantiate
const map = new OrderAgnosticMultiMap();

// Register values
map.set("keyA", "keyB", "keyC", "content 1");
map.set("keyA", "keyC", "keyC", "content 2");
map.set("keyA", "keyB", "keyB", "content 3");
// The keys can be any object
map.set(map, OrderAgnosticMultiMap, map, window, document, "content 4");


// Get values (keys can be in different orders)
console.log(map.get("keyB", "keyC", "keyA"));
// log: "content 1"
console.log(map.get("keyB", "keyB", "keyC"));
// log: undefined
map.get(document, map, window, OrderAgnosticMultiMap, map);
// log: "content 4"

// Check if a value exists for some keys
console.log(map.has("keyC", "keyC", "keyA"));
// log: true
console.log(map.has("keyA", "keyC", "keyA"));
// log: false

// Loop through values
for(let [tally,value] of map){
    console.log(tally,value);
}
// log:
// Map(3) {"keyA" => 1, "keyB" => 1, "keyC" => 1} 'content 1'
// Map(3) {"keyA" => 1, "keyC" => 2} 'content 2'
// Map(3) {"keyA" => 1, "keyB" => 2} 'content 3'
// Map(3) {map => 2, OrderAgnosticMultiMap => 1, window => 1, document => 1} 'content 4'

// Delete keys
map.delete("keyC", "keyB", "keyA");
map.delete("keyB", "keyB", "keyA");
map.delete("keyC", "keyC", "keyA");
console.log(map.has("keyC", "keyC", "keyA"));
// log: false

Pre-edit

If there is anyone wondering if there is a solution for multi keyed ES6 map, here's my take.
The order does matter though, so map.get(a,b,c) and map.get(c,a,b) will fetch different values.
And you can of course use this as string to object map, so it satisfies the OP's use case as well.

class MultiMap{
    map = new Map;
    own = Symbol();// unique value that doesn't collide
    set(){
        let lst = [...arguments];
        let val = lst.pop();
        let map = this.map;
        for(let k of lst){
            if(!map.has(k))map.set(k,new Map);
            map = map.get(k);
        }
        map.set(this.own,val);// to avoid collision between the same level
        return val;
    }
    get(...lst){
        let map = this.map;
        for(let k of lst){
            if(!map.has(k))return undefined;
            map = map.get(k);
        }
        return map.get(this.own);
    }
    has(...lst){
        let map = this.map;
        for(let k of lst){
            if(!map.has(k))return false;
            map = map.get(k);
        }
        return map.has(this.own);
    }
    delete(...lst){
        let map = this.map;
        let maps = [[null,map]];
        for(let k of lst){
            if(!map.has(k))return false;
            map = map.get(k);
            maps.push([k,map]);
        }
        let ret = map.delete(this.own);
        for(let i = maps.length-1; i > 0; i--){
            if(maps[i][1].size === 0){
                maps[i-1][1].delete(maps[i][0]);
            }else{
                break;
            }
        }
        return ret;
    }
}

Example use case

let a = {a:"a"};
let b = {b:"b"};
let c = {c:"c"};

let mm = new MultiMap;

//basic operations
console.log(mm.set(a,b,c,"abc"));// "abc"
console.log(mm.get(a,b,c));// "abc"
console.log(mm.has(a,b,c));// true
console.log(mm.delete(a,b,c));// true

// overlapping keys can be handled fine as well
mm.set(a,b,"ab");
mm.set(a,"a");
console.log(mm.get(a,b));// "ab"
console.log(mm.get(a));// "a"

For anyone curious about my use case: I was trying to make an event listener wrapper that maps to multiple events internally (mousedown => mousedown, touchstart etc). I needed to cache the arguments when .on() is called so .off() can find the right set of event listeners to remove.

martian17
  • 418
  • 3
  • 14
3

You can try to store a reference to the last created inner object, and go deeper in a loop, in order to make it in a linear time:

// Input data:
var keys = ["keyA", "keyB", "keyC", "keyD", "keyE"];
var value = 100;

// Algorithm:
var result = {};
var last = result;

for (var i = 0; i < keys.length - 1; i++)
{
  last = (last[keys[i]] = {});

  // can be change to a two-liner:
  // last[keys[i]] = {};
  // last = last[keys[i]];
}
last[keys[keys.length - 1]] = value;

// Output:
document.body.innerHTML = JSON.stringify(result);
document.body.innerHTML += "<br/><br/>" + result["keyA"]["keyB"]["keyC"]["keyD"]["keyE"];
Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101
  • Ah, of course, keeping a reference to the current innermost object. Thank you! As a follow up question, if you had to map multiple keys to a single value, what data structure would you choose instead? – PDN Nov 02 '15 at 04:03
  • @PublicDisplayName If the order is important to you, then that's the most suitable method. I mean, it `result["A"]["B"]` is not equal to `result["B"]["A"]`. I just have never came across such problem :) – Yeldar Kurmangaliyev Nov 02 '15 at 04:05
  • Now that you mention it, I don't really care what order the keys are in either. In that case what data structure would you propose? – PDN Nov 02 '15 at 04:12
  • @PublicDisplayName Honestly, I can't imagine one. You can use this approach and sort `keys` before assignment :) So, when `keys = ["B", "A"]`, you will sort it and return `result["A"]["B"]`. – Yeldar Kurmangaliyev Nov 02 '15 at 04:13
  • `I don't really care what order the keys are in either` how are you planning to access the value later? Does accessing it always need all 3 keys? – Paul S. Nov 02 '15 at 04:34
  • @PaulS. Since I will be using this data structure, the order will matter. However, I just meant that the order of my keys didn't have any intrinsic order, so if there was a data structure I could create that could take a set of three keys and map them to a value without regards to order, that would work too, but I think Yeldar's suggestion is simple and fast enough. – PDN Nov 02 '15 at 05:31
  • This doesn't seem to work for my purposes: if after your first "keys" variable you put in a keys like so: [ 'keyA', 'keyB1', 'keyC1', 'keyD1', 'keyE1' ], `result` will then report as having only one key, 'keyA'. Given that keys in JS must always be strings you have to use a boring option: as detailed by the SO answer referenced by the OP. – mike rodent Nov 18 '17 at 16:32
3

If you do not want to maintain a hierarchy of objects, I would suggest you concatenate the keys and store the value with the concatenated string as key.

This assumes you always have the same keys array. If your keys array is supplied externally, you can sort before joining.

See the snippet.

var keys = ["keyA", "keyB", "keyC", "keyD", "keyE"];
var value = 568;

var datastructure = {};

datastructure[keys.join("-")] = value;

document.getElementById("output").innerHTML = datastructure[keys.join("-")];
<span id="output"></span>
santon
  • 1,654
  • 16
  • 26
  • While this is sometimes an option, it kind of defeats the purpose of using a Map, which is to be able to use non string symbols as a key. A map with just string keys is essentially no different than a plain object, both in use and performance. – inwerpsel Jan 27 '23 at 09:57
  • @inwerpsel Not sure if you have placed your comment on the answer you intended. No Map used for this answer. – santon Mar 04 '23 at 00:40
  • Right, reading again now I see I falsely assumed this was about a [JavaScript `Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), but it's really more generally about any map-like data structure. That's what I was looking for at the moment, I guess I had a bit of tunnel vision. So my comment only applies if someone would want this functionality in such a `Map`, as they would likely not have the option to switch to string keys. – inwerpsel Mar 04 '23 at 18:32
1

Assuming this structure is to be a tree where the nesting has arbitrary depth, first you might benefit from a helper function that lets you access possible non-existent paths safely:

function path(obj, str) {
  return str.split('.').reduce(function (acc, key) {
    return acc instanceof Object ? acc[key] : undefined;
  }, obj);
}

And you also want a way to set such paths neatly:

function setPath(obj, str, val) {
  var path = str.split('.');
  var key  = path.pop();

  var target = path.reduce(function(acc, key) {
    return acc[key] = acc[key] instanceof Object ? acc[key] : {};
  }, obj);

  target[key] = val;
}

Then you have a clean interface for storing and retrieving this data.

map = {};

setPath(map, 'keyA.keyB.keyC', 100);
path(map, 'keyA.keyB.keyC') // 100;
path(map, 'keyA.keyX.keyY') // undefined;

If you prefer, you could have it take arrays of keys instead of dot-notation paths as shown here (just omit the split steps).

Note that if you are never interested in accessing nodes in the tree other than the leaves, or wish to be able to have values for both map.a.b and map.a, you can do this much more simply by having a single depth:

map[keys.join('.')] = 100;

And since you've added in a comment that the objective here is actually just to associate a value with a set of keys, and that there is no actual tree structure at all:

function get(map, keys) {
  var key = keys.sort().join('.');
  return map[key];
}

function set(map, keys, val) {
  var key = keys.sort().join('.');
  map[key] = val;
}

If periods are plausible characters in your keys, substitute a different character that you can safely reserve.

Semicolon
  • 6,793
  • 2
  • 30
  • 38
  • Thank you for your detailed explanation. I just wanted to mention that in case one chooses the single depth solution, but cannot manage to reserve a character, for example because he is not in control of the input, a solution could be to escape the character used. This is exactly what we do for reserved characters in C style strings or for URLs on the web. – Jacopo Tedeschi Jan 09 '22 at 20:05
0

There another options:

  1. Use Map only for the first key and brute force the internal ones.
  2. Create Map for the each level (like an column indexes in a database), where the value is an auto incremental id. So, you filter ids by the first Map, then by another ones. This is like in-memory DB.
  3. Use a cryptographic hash function like SHA-3 256 or blake3.
  4. Create your own reversible hash function that will concatenate the keys by a binary shifts as a BigInt in js or i64/v128 in WASM/AssemblyScript (look at the great Fixed-width SIMD support on https://webassembly.org/roadmap/).
  5. Make an AVL-tree or RB-tree, where each key is an path array.

All of them are targeted to:

  1. Avoid working with strings as it is slow.
  2. Avoid excessive number of structures, as this increases memory usage.
Filyus
  • 89
  • 1
  • 4