【JavaScript】ネイティブで集合演算できるようになった

JavaScriptにはだいぶ前から[Set](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Set)オブジェクトがありましたが、何故か集合演算は全く定義されておらず[自力で実装しなければなりませんでした](https://qiita.com/toshihikoyanase/items/7b07ca6a94eb72164257)。

その後、まあ不便だねってことで[Set Methods for JavaScript](https://github.com/tc39/proposal-set-methods)というproposalが提出されました。

実装は珍しくSafariが最も早く、[2023/09/18のSafari17](https://webkit.org/blog/14445/webkit-features-in-safari-17-0/)から対応しました。
その後[2024/02/21にChrome122](https://developer.chrome.com/blog/chrome-122-beta?hl=ja#set_methods)、そして[2024/06/11にFirefox127](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/127#javascript)で実装されたことにより、[主要全ブラウザで集合演算が使用可能](https://caniuse.com/?search=Set%20intersection)になりました。
複数環境で実装されたことから、無事[ES2025](https://github.com/tc39/proposals/blob/main/finished-proposals.md)としてStage4、つまり上がりになりました。


ということで使い方を紹介するよ。


# [Set.prototype.intersection()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection)

要素と引数の、両方に含まれる値を返します。

```js
new Set([1, 2, 3, 4]).intersection( new Set([1, 3, 5])); // Set{1, 3}
new Set([1, 2, 3, 4]).intersection( new Set([5, 6, 7])); // Set{}
```

いわゆるANDですね。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/fe0b3057-76f1-8620-df87-e7b42604ad0b.png)


# [Set.prototype.union()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/union)

要素と引数の、少なくともどちらか片方に含まれる値を返します。

```js
new Set([1, 2, 3]).union( new Set([1, 3, 5])); // Set{1, 2, 3, 5}
new Set([1, 2, 3]).union( new Set([4, 5, 6])); // Set{1, 2, 3, 4, 5, 6}
```

こちらはORです。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/ccce1e88-d6df-0b57-9aa9-e959768d4c8d.png)

並び順は、最初のSetのあとに後ろのSetの要素が追加されます。
Setなので順番は保証されています。


# [Set.prototype.difference()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/difference)

要素のうち、引数に含まれないものを返します。

```js
new Set([1, 2, 3]).difference( new Set([1, 3, 5])); // Set{2}
new Set([1, 2, 3]).difference( new Set([4, 5, 6])); // Set{1, 2, 3}
```

差集合です。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/14807084-84d0-ced5-c582-704f9d1500fe.png)


# [Set.prototype.symmetricDifference()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference)

要素と引数の、どちらか片方にしか含まれない値を返します。

```js
new Set([1, 2, 3]).symmetricDifference( new Set([1, 3, 5])); // Set{2, 5}
new Set([1, 2, 3]).symmetricDifference( new Set([4, 5, 6])); // Set{1, 2, 3, 4, 5, 6}
```

XORです。

![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/0b1f05da-26a5-dcf8-9a47-83e61651f797.png)

でもドキュメントでは一貫して[symmetric difference対称差](https://ja.wikipedia.org/wiki/%E5%AF%BE%E7%A7%B0%E5%B7%AE)と書かれていて、[exclusive disjunction排他的論理和](https://ja.wikipedia.org/wiki/%E6%8E%92%E4%BB%96%E7%9A%84%E8%AB%96%E7%90%86%E5%92%8C)とは書かれていないんですよね。
違いがわかんねえ。


# [Set.prototype.isSubsetOf()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSubsetOf)


要素が全て、引数に含まれる場合にtrueを返します。

```js
new Set([1, 2, 3]).isSubsetOf( new Set([1, 2, 3, 4])); // true
new Set([1, 2, 3]).isSubsetOf( new Set([1, 2, 4, 5])); // false

new Set([1, 2, 3]).isSubsetOf( new Set([1, 2, 3])); // true
new Set().isSubsetOf( new Set([1, 2, 3])); // true
```

部分集合です。

![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/c6fd44d8-0a7e-f54e-df87-a880769b7df1.png)


要素が空集合だったり、A==Bだったりする場合もtrueになります。


# [Set.prototype.isSupersetOf()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSupersetOf)

引数が、要素に含まれる場合にtrueを返します。


```js
new Set([1, 2, 3]).isSupersetOf( new Set([1, 2, 3, 4])); // false
new Set([1, 2, 3]).isSupersetOf( new Set([1, 2, 4, 5])); // false

new Set([1, 2, 3]).isSupersetOf( new Set([1, 2, 3])); // true
new Set([1, 2, 3]).isSupersetOf( new Set()); // true
```

要するに`isSubsetOf()`の逆ですね。

![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/f2f42bd9-2e18-1ad9-94a9-a94842363370.png)



# [Set.prototype.isDisjointFrom()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isDisjointFrom)

要素と引数に、一切の共通点がない場合にtrueを返します。

```js
new Set([1, 2, 3]).isDisjointFrom(new Set([4, 5, 6])); // true
new Set([1, 2, 3]).isDisjointFrom(new Set([2, 5, 6])); // false

new Set([]).isDisjointFrom(new Set([])); // true
```

図を見てのとおりです。

![07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/907cb19d-abe5-18a9-0d9b-3407903d90bc.png)



# その他

[Map](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map)とは演算が可能です。
ただし、比較に使われるのは値ではなくキーです。

```js
const set = new Set([1, 2]);
const map = new Map([[1, 2], [3, 4]]);

set.intersection(map) // Set{1}
```

両方に含まれる値は2ではなく1になります。
かなり直感に反する。

またMap側には実装されていないので、`map.intersection(set)`とはできません。

ところで、最も比較したくなるであろうArrayとは何故か演算できません。

```js
const set = new Set([1, 2]);
const arr = new Array([1, 2]);

set.intersection(arr) // TypeError: The .size property is NaN
```

Setメソッドたちは、たとえ使わない場合でも必ず`.size`・`.keys`・`.has`の3プロパティを参照します。
と、[ドキュメントには書いてあります](https://github.com/tc39/proposal-set-methods/blob/main/details.md)。
従って、これらを実装すれば自作クラスも比較に使えると思いますが、Arrayには`.size`がないのでエラーになります。
重複をどうするか問題とか色々あるので難しいかもしれませんが、でもまあやっぱりArrayには対応してほしかったところですね。


# 感想

試しにクラスを自作してみたら`next()`メソッドがないって言われて動かなかった。
実装しても言われる。
よくわからない。

なお、これらの関数は[ずっと昔からpolyfillが公開されて](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#implementing_basic_set_operations)いました。
いずれも10行もしない程度であり、集合演算の自力実装自体は全く難しくなかったわけですが、ネイティブ実装されることによって大きな高速化と普及が見込めることでしょう。