порівняння множин ECMA6 для рівності


102

Як ви порівнюєте два набори javascript? Я спробував з допомогою ==і , ===але як повернути помилкове.

a = new Set([1,2,3]);
b = new Set([1,3,2]);
a == b; //=> false
a === b; //=> false

Ці два набори рівнозначні, тому що за визначенням множини не мають порядку (принаймні, зазвичай). Я переглянув документацію для Set на MDN і не знайшов нічого корисного. Хтось знає, як це зробити?


Два набори - це два різних об’єкти. ===є для ціннісної рівності, а не об'єктної рівності.
elclanrs

3
повторіть і порівняйте значення кожного члена, якщо все те ж саме, встановити "однаково"
dandavis

1
@dandavis У множинах членами є значення.

2
Набори та Карти мають порядок, який є порядком вставки - з будь-якої причини: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
CodeManX

8
Найгірше, навіть new Set([1,2,3]) != new Set([1,2,3]). Це робить Javascript Set непридатним для наборів наборів, оскільки суперсеть буде містити повторювані підмножини. Єдине вирішення, яке спадає на думку, - це перетворення всіх підмножин у масиви, сортування кожного масиву, а потім кодування кожного масиву як рядка (наприклад, JSON).
7vujy0f0hy

Відповіді:


73

Спробуйте це:

var a = new Set([1,2,3]);
var b = new Set([1,3,2]);

alert(eqSet(a, b)); // true

function eqSet(as, bs) {
    if (as.size !== bs.size) return false;
    for (var a of as) if (!bs.has(a)) return false;
    return true;
}

Більш функціональним підходом було б:

var a = new Set([1,2,3]);
var b = new Set([1,3,2]);

alert(eqSet(a, b)); // true

function eqSet(as, bs) {
    return as.size === bs.size && all(isIn(bs), as);
}

function all(pred, as) {
    for (var a of as) if (!pred(a)) return false;
    return true;
}

function isIn(as) {
    return function (a) {
        return as.has(a);
    };
}

allФункція працює для всіх ітерації об'єктів (наприклад , Setі Map).

Якби Array.fromбільш широка підтримка, ми могли б реалізувати allфункцію як:

function all(pred, as) {
    return Array.from(as).every(pred);
}

Сподіваюся, що це допомагає.


2
Я думаю, ви повинні змінити ім'я hasна isPartOfабо isInабоelem
Бергі

@Bergi Я змінив назву функції hasна isIn.
Аадіт М Шах

1
@DavidGiven Так, набори в JavaScript повторюються в порядку вставки: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Aadit M Shah

48
Щодня я впевнений, що JavaScript - це найграніша мова, яку коли-небудь створювали. Кожен повинен вигадати свої основні функції, щоб впоратися з його обмеженням, і це в ES6, і ми в 2017 році! Чому вони не могли додати такі часті функції до специфікації об'єкта Set!
Ghasan

2
@ GhasanAl-Sakkaf, я згоден, можливо, TC39 складається з учених, але ніяких прагматиків ...
Marecky

59

Ви також можете спробувати:

var a = new Set([1,2,3]);
var b = new Set([1,3,2]);

isSetsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));

console.log(isSetsEqual(a,b)) 


Однозначно краще рішення, оскільки воно вписується в умову if
Hugodby

Мені подобається, наскільки ідіоматичне це рішення, і наскільки читабельне! Дякую @Max
daydreamer

29

lodash забезпечує _.isEqual(), що робить глибокі порівняння. Це дуже зручно, якщо ви не хочете писати своє. Станом на lodash 4, _.isEqual()правильно порівнює набори.

const _ = require("lodash");

let s1 = new Set([1,2,3]);
let s2 = new Set([1,2,3]);
let s3 = new Set([2,3,4]);

console.log(_.isEqual(s1, s2)); // true
console.log(_.isEqual(s1, s3)); // false

7

Інша відповідь буде добре працювати; ось ще одна альтернатива.

// Create function to check if an element is in a specified set.
function isIn(s)          { return elt => s.has(elt); }

// Check if one set contains another (all members of s2 are in s1).
function contains(s1, s2) { return [...s2] . every(isIn(s1)); }

// Set equality: a contains b, and b contains a
function eqSet(a, b)      { return contains(a, b) && contains(b, a); }

// Alternative, check size first
function eqSet(a, b)      { return a.size === b.size && contains(a, b); }

Однак майте на увазі, що це не робить глибокого порівняння рівності. Так

eqSet(Set([{ a: 1 }], Set([{ a: 1 }])

повернеться помилковим. Якщо вищевказані два набори вважати рівними, нам потрібно повторити обидва набори, роблячи глибокі порівняння якості кожного елемента. Ми обумовлюємо існування deepEqualрозпорядку. Тоді логіка була б

// Find a member in "s" deeply equal to some value
function findDeepEqual(s, v) { return [...s] . find(m => deepEqual(v, m)); }

// See if sets s1 and s1 are deeply equal. DESTROYS s2.
function eqSetDeep(s1, s2) {
  return [...s1] . every(a1 => {
    var m1 = findDeepEqual(s2, a1);
    if (m1) { s2.delete(m1); return true; }
  }) && !s2.size;
}

Що це робить: для кожного члена s1 шукайте глибоко рівного члена s2. Якщо він знайдеться, видаліть його, щоб він більше не міг використовуватись. Два набори глибоко рівні, якщо всі елементи s1 знайдені в s2, і s2 вичерпано. Неперевірений.

Це може бути вам корисним: http://www.2ality.com/2015/01/es6-set-operations.html .


6

Жодне з цих рішень не повертає очікувану функціональність такій структурі даних, як набір наборів. У своєму нинішньому стані набір Javascript для цієї мети марний, тому що суперсеть буде містити повторювані підмножини, які Javascript неправильно вважає відмінними. Єдине рішення, про яке я можу придумати, - це перетворення кожного підмножини в масив , сортування його та кодування у вигляді String (наприклад, JSON).

Рішення

var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var fromJsonSet = jset => new Set(JSON.parse(jset));

Основне використання

var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var fromJsonSet = jset => new Set(JSON.parse(jset));

var [s1,s2] = [new Set([1,2,3]), new Set([3,2,1])];
var [js1,js2] = [toJsonSet([1,2,3]), toJsonSet([3,2,1])]; // even better

var r = document.querySelectorAll("td:nth-child(2)");
r[0].innerHTML = (toJsonSet(s1) === toJsonSet(s2)); // true
r[1].innerHTML = (toJsonSet(s1) == toJsonSet(s2)); // true, too
r[2].innerHTML = (js1 === js2); // true
r[3].innerHTML = (js1 == js2); // true, too

// Make it normal Set:
console.log(fromJsonSet(js1), fromJsonSet(js2)); // type is Set
<style>td:nth-child(2) {color: red;}</style>

<table>
<tr><td>toJsonSet(s1) === toJsonSet(s2)</td><td>...</td></tr>
<tr><td>toJsonSet(s1) == toJsonSet(s2)</td><td>...</td></tr>
<tr><td>js1 === js2</td><td>...</td></tr>
<tr><td>js1 == js2</td><td>...</td></tr>
</table>

Кінцевий тест: набір наборів

var toSet = arr => new Set(arr);
var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var toJsonSet_WRONG = set => JSON.stringify([...set]); // no sorting!

var output = document.getElementsByTagName("code"); 
var superarray = [[1,2,3],[1,2,3],[3,2,1],[3,6,2],[4,5,6]];
var superset;

Experiment1:
    superset = toSet(superarray.map(toSet));
    output[0].innerHTML = superset.size; // incorrect: 5 unique subsets
Experiment2:
    superset = toSet([...superset].map(toJsonSet_WRONG));
    output[1].innerHTML = superset.size; // incorrect: 4 unique subsets
Experiment3:
    superset = toSet([...superset].map(toJsonSet));
    output[2].innerHTML = superset.size; // 3 unique subsets
Experiment4:
    superset = toSet(superarray.map(toJsonSet));
    output[3].innerHTML = superset.size; // 3 unique subsets
code {border: 1px solid #88f; background-color: #ddf; padding: 0 0.5em;}
<h3>Experiment 1</h3><p>Superset contains 3 unique subsets but Javascript sees <code>...</code>.<br>Let’s fix this... I’ll encode each subset as a string.</p>
<h3>Experiment 2</h3><p>Now Javascript sees <code>...</code> unique subsets.<br>Better! But still not perfect.<br>That’s because we didn’t sort each subset.<br>Let’s sort it out...</p>
<h3>Experiment 3</h3><p>Now Javascript sees <code>...</code> unique subsets. At long last!<br>Let’s try everything again from the beginning.</p>
<h3>Experiment 4</h3><p>Superset contains 3 unique subsets and Javascript sees <code>...</code>.<br><b>Bravo!</b></p>


1
Чудове рішення! А якщо ви знаєте, що у вас щойно набір рядків чи цифр, це просто стає[...set1].sort().toString() === [...set2].sort().toString()

3

Причина, через яку ваш підхід повертається хибною, полягає в тому, що ви порівнюєте два різних об’єкти (навіть якщо вони отримали однаковий вміст), таким чином порівнюючи два різних об'єкти (не посилання, а об’єкти), завжди повертає вас помилково.

Наступний підхід об'єднує два набори в один і просто тупо порівнює розмір. Якщо це те саме, це те саме:

const a1 = [1,2,3];
const a2 = [1,3,2];
const set1 = new Set(a1);
const set2 = new Set(a2);

const compareSet = new Set([...a1, ...a2]);
const isSetEqual = compareSet.size === set2.size && compareSet.size === set1.size;
console.log(isSetEqual);

Вгору : Це дуже просто і коротко. Немає зовнішньої бібліотеки, лише ванільна JS

Знизу : це, мабуть, буде повільніше, ніж просто повторити значення і вам потрібно більше місця.


1

Порівнюючи два об’єкти з ==, ===

Використовуючи ==або ===оператор для порівняння двох об'єктів, ви завжди отримаєте, false якщо ці об'єкти не посилаються на один і той же об'єкт . Наприклад:

var a = b = new Set([1,2,3]); // NOTE: b will become a global variable
a == b; // <-- true: a and b share the same object reference

В іншому випадку == прирівнюється до false, навіть якщо об'єкт містить ті самі значення:

var a = new Set([1,2,3]);
var b = new Set([1,2,3]);
a == b; // <-- false: a and b are not referencing the same object

Можливо, вам доведеться розглянути можливість порівняння вручну

У ECMAScript 6 ви можете заздалегідь перетворити набори в масиви, щоб ви могли помітити різницю між ними:

function setsEqual(a,b){
    if (a.size !== b.size)
        return false;
    let aa = Array.from(a); 
    let bb = Array.from(b);
    return aa.filter(function(i){return bb.indexOf(i)<0}).length==0;
}

ПРИМІТКА: Array.from це одна зі стандартних функцій ECMAScript 6, але вона не підтримується в сучасних браузерах. Перевірте таблицю сумісності тут: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Browser_compatibility


1
Хіба це не вдасться визначити, учасника bякого немає a?

1
@torazaburo Дійсно. Найкращий спосіб пропустити перевірку, чи немає членів організації b, aце перевірити, чи немає a.size === b.size.
Аадіт М Шах

1
Поставте a.size === b.sizeспочатку на коротке замикання порівняння окремих елементів, якщо немає необхідності?

2
Якщо розмір різний, за визначенням набори не рівні, тому краще спочатку перевірити цю умову.

1
Ну, інше питання полягає в тому, що за характером наборів hasоперація над множинами розроблена як дуже ефективна, на відміну від indexOfоперації з масивами. Тому було б доцільно змінити функцію фільтра, щоб вона була return !b.has(i). Це також усуне необхідність перетворення bв масив.

1

Я створив швидкий поліфайл для Set.prototype.isEqual ()

Set.prototype.isEqual = function(otherSet) {
    if(this.size !== otherSet.size) return false;
    for(let item of this) if(!otherSet.has(item)) return false;
    return true;
}

Github Gist - Set.prototype.isEqual


1

Виходячи з прийнятої відповіді, якщо припустити підтримку Array.from, ось однорівневий:

function eqSet(a, b) {
    return a.size === b.size && Array.from(a).every(b.has.bind(b));
}

Або справжній однолінійний припускаючи функції стрілки та оператора розвороту: eqSet = (a,b) => a.size === b.size && [...a].every(b.has.bind(b))
Джон Хоффер

1

Якщо множини містять лише примітивні типи даних або об'єкт всередині множин має еталонну рівність, то існує простіший спосіб

const isEqualSets = (set1, set2) => (set1.size === set2.size) && (set1.size === new Set([...set1, ...set2]).size);


0

Я дотримуюся такого підходу в тестах:

let setA = new Set(arrayA);
let setB = new Set(arrayB);
let diff = new Set([...setA].filter(x => !setB.has(x)));
expect([...diff].length).toBe(0);

5
Почекайте секунду ... це лише перевіряє, чи є в A елементи, яких B немає? Він не перевіряє, чи є в B елементи, яких немає. Якщо ви спробуєте a=[1,2,3]і b=[1,2,3,4], то це говорить , що вони однакові. Тож я думаю, вам потрібна додаткова перевірка на кшталтsetA.size === setB.size

0

Дуже незначна модифікація на основі відповіді @Aadit M Shah:

/**
 * check if two sets are equal in the sense that
 * they have a matching set of values.
 *
 * @param {Set} a 
 * @param {Set} b
 * @returns {Boolean} 
 */
const areSetsEqual = (a, b) => (
        (a.size === b.size) ? 
        [...a].every( value => b.has(value) ) : false
);

Якщо у когось іншого виникає проблема, як у мене, через якусь химерність останньої дівочої, довелося тут додати явний умовний характер.

(Також для множини я думаю are, що це трохи інтуїтивніше читати вголос 🙃)


-1

1) Перевірте, чи рівні розміри. Якщо ні, то вони не рівні.

2) повторіть кожен елемм A і перевірте, чи існує він у B. Якщо повернення не вдалося unequal

3) Якщо вищезгадані 2 умови не відповідають, це означає, що вони рівні.

let isEql = (setA, setB) => {
  if (setA.size !== setB.size)
    return false;
  
  setA.forEach((val) => {
    if (!setB.has(val))
      return false;
  });
  return true;
}

let setA = new Set([1, 2, {
  3: 4
}]);
let setB = new Set([2, {
    3: 4
  },
  1
]);

console.log(isEql(setA, setB));

2) Спосіб 2

let isEql = (A, B) => {
  return JSON.stringify([...A].sort()) == JSON.stringify([...B].sort());
}

let res = isEql(new Set([1, 2, {3:4}]), new Set([{3:4},1, 2]));
console.log(res);


Ця відповідь є абсолютно невірною. Оператор повернення в forEachметоді НЕ змусить повернути батьківську функцію.
xaviert
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.