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?

2
  • stackoverflow.com/questions/14743536/… may help you. Commented Nov 2, 2015 at 3:50
  • var t = maps; for(var i=0; i<keys.length; i++){ t = (t[keys[i]] = t[keys[i]] || {}); } Commented Nov 2, 2015 at 3:57

5 Answers 5

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Using own = Symbol() to keep Maps from being accessed or overwritten was clever.
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"];

7 Comments

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?
@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 :)
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?
@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"].
@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.
|
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>

3 Comments

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 Not sure if you have placed your comment on the answer you intended. No Map used for this answer.
Right, reading again now I see I falsely assumed this was about a JavaScript 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.
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.

1 Comment

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.
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.