Карта с использованием кортежей или объектов
Я пытаюсь использовать новые объекты (ES6) Map
, чтобы представлять карту между свойствами и значением.
У меня есть объекты в форме, подобной:
{key1:value1_1,key2:value2_1},..... {key1:value1_N,key2:value2_N}
Я хочу сгруппировать их на основе их значений key1 и key2.
Например, я хочу иметь возможность группировать следующие элементы x
и y
:
[{x:3,y:5,z:3},{x:3,y:4,z:4},{x:3,y:4,z:7},{x:3,y:1,z:1},{x:3,y:5,z:4}]
И получите карту, содержащую:
{x:3,y:5} ==> {x:3,y:5,z:3},{x:3,y:5,z:4}
{x:3,y:4} ==> {x:3,y:4,z:4},{x:3,y:4,z:7}
{x:3,y:1} ==> {x:3,y:1,z:1}
В Python я бы использовал кортежи в качестве словарных клавиш. Карта ES6 допускает произвольные объекты как ключи, но использует стандартный алгоритм равенства (===
), поэтому объекты только равны по ссылке из того, что я могу сказать.
Как я могу выполнить подобную группировку с помощью карт ES6? Альтернативно, решение, использующее обычные объекты JS, если есть изящный способ, который я пропускал.
Я бы предпочел не использовать внешнюю коллекционную библиотеку, но если есть лучшее решение, использующее одно, мне тоже интересно узнать об этом.
Ответы
Ответ 1
Хорошо, теперь я поднял вопрос о esdiscuss, и я получил ответ от Mozilla Джейсон Орендорф:
- Это проблема с картами ES6.
- Решение будет представлено в виде ES7 объектов значений для ключей вместо объектов.
- Раньше считалось, что люди указывают
.equals
и .hashCode
, но они были отвергнуты в пользу объектов значений. (по уважительным причинам, на мой взгляд).
- Единственным решением на данный момент является сворачивание собственной коллекции.
Базовая такая коллекция (концепция, не использующая в производственном коде) была предложена Брэдли в потоке ESDiscuss и может выглядеть примерно так:
function HashMap(hash) {
var map = new Map;
var _set = map.set;
var _get = map.get;
var _has = map.has;
var _delete = map.delete;
map.set = function (k,v) {
return _set.call(map, hash(k), v);
}
map.get = function (k) {
return _get.call(map, hash(k));
}
map.has = function (k) {
return _has.call(map, hash(k));
}
map.delete = function (k) {
return _delete.call(map, hash(k));
}
return map;
}
function TupleMap() {
return new HashMap(function (tuple) {
var keys = Object.keys(tuple).sort();
return keys.map(function (tupleKey) { // hash based on JSON stringification
return JSON.stringify(tupleKey) + JSON.stringify(tuple[tupleKey]);
}).join('\n');
return hashed;
});
}
Лучшее решение - использовать что-то вроде MontageJS/Collections, которое позволяет специфицировать функции hash/equals.
Здесь вы можете увидеть документы API .
Ответ 2
Это не представляется возможным. Что ты можешь сделать? Что-то ужасное, как всегда.
let tuple = (function() {
let map = new Map();
function tuple() {
let current = map;
let args = Object.freeze(Array.prototype.slice.call(arguments));
for (let item of args) {
if (current.has(item)) {
current = current.get(item);
} else {
let next = new Map();
current.set(item, next);
current = next;
}
}
if (!current.final) {
current.final = args;
}
return current.final;
}
return tuple;
})();
И вуаля.
let m = new Map();
m.set(tuple(3, 5), [tuple(3, 5, 3), tuple(3, 5, 4)]);
m.get(tuple(3, 5)); // [[3, 5, 3], [3, 5, 4]]
Ответ 3
Ответ Benjamin не работает для всех объектов, поскольку он полагается на JSON.stringify, который не может обрабатывать круглые объекты и может сопоставлять разные объекты с одной и той же строкой. Ответ Minitech может создавать огромные деревья вложенных карт, которые, как я подозреваю, являются как памятью, так и неэффективной CPU, особенно для длинных кортежей, поскольку она должна создавать карту для каждого элемента в кортеже.
Если вы знаете, что ваши кортежи содержат только цифры, лучшим решением является использование [x,y].join(',')
в качестве ключа. Если вы хотите использовать кортежи, содержащие произвольные объекты в качестве ключей, вы все равно можете использовать этот метод, но сначала должны сопоставить объекты с уникальными идентификаторами. В приведенном ниже коде я генерирую эти идентификаторы лениво, используя get_object_id
, который хранит созданные идентификаторы во внутренней карте. Затем я могу генерировать ключи для кортежей, объединяя эти идентификаторы. (См. Код внизу этого ответа.)
Затем метод tuple
можно использовать для хэш-кортежей объектов для строки, которая может использоваться как ключ на карте. Это использует эквивалентность объектов:
x={}; y={};
tuple(x,y) == tuple(x,y) // yields true
tuple(x,x) == tuple(y,y) // yields false
tuple(x,y) == tuple(y,x) // yields false
Если вы уверены, что ваши кортежи будут содержать только объекты (т.е. не нуль, числа или строки), тогда вы можете использовать WeakMap в get_object_id
, чтобы get_object_id
и tuple
не пропустили объекты которые передаются в качестве аргумента для них.
var get_object_id = (function() {
var generated_ids = 1;
var map = new Map();
return get_object_id;
function get_object_id(obj) {
if (map.has(obj)) {
return map.get(obj);
} else {
var r = generated_ids++;
map.set(obj, r);
return r;
}
}
})();
function tuple() {
return Array.prototype.map.call(arguments, get_object_id).join(',');
}
// Test
var data = [{x:3,y:5,z:3},{x:3,y:4,z:4},{x:3,y:4,z:7},
{x:3,y:1,z:1},{x:3,y:5,z:4}];
var map = new Map();
for (var i=0; i<data.length; i++) {
var p = data[i];
var t = tuple(p.x,p.y);
if (!map.has(t)) map.set(t,[]);
map.get(t).push(p);
}
function test(p) {
document.writeln((JSON.stringify(p)+' ==> ' +
JSON.stringify(map.get(tuple(p.x,p.y)))).replace(/"/g,''));
}
document.writeln('<pre>');
test({x:3,y:5});
test({x:3,y:4});
test({x:3,y:1});
document.writeln('</pre>');
Ответ 4
В то время как этот вопрос довольно старый, объекты ценности по-прежнему не являются существующими в JavaScript (поэтому люди все равно могут быть заинтересованы), поэтому я решил написать простую библиотеку, чтобы выполнить аналогичное поведение для массивов в виде ключей на картах (здесь репо: https://github.com/Jamesernator/es6-array-map). Библиотека предназначена в основном идентичной карте в использовании, за исключением того, что массивы сравниваются по элементам, а не по личность.
Использование:
var map = new ArrayMap();
map.set([1,2,3], 12);
map.get([1,2,3]); // 12
map.set(['cats', 'hats'], {potatoes: 20});
map.get(['cats', 'hats']); // {potatoes: 20}
Предупреждение. Однако библиотека обрабатывает ключевые элементы по идентификатору, поэтому следующее не работает:
var map = new ArrayMap();
map.set([{x: 3, y: 5}], {x:3, y:5, z:10});
map.get([{x: 3, y: 5}]); // undefined as objects within the list are
// treated by identity
Но пока вы можете сериализовать данные в массивы примитивов, вы можете использовать ArrayMap следующим образом:
var serialize = function(point) {
return [point.x, point.y];
};
var map = new ArrayMap(null, serialize);
map.set({x: 10, y: 20}, {x: 10, y: 20, z: 30});
map.get({x: 10, y: 20}); // {x: 10, y: 20, z: 30}