【JavaScript】Arrayの重複を排除する最もシンプルな方法

 RDBMSなどでは、Distinctという構文があったりするのですが、JavaScriptでは重複を排除するために少しの工夫が必要になります。

 例えば、以下のような配列があるとします。

let arr = [0,1,1,2,3,4,4,4,5];

 このうち、重複するデータが必要ないケースを考えてみます。

Setを使う

 ES2015(ES6)で導入されたSetは、今回のように重複を許さない使い方には非常に有効なオブジェクトです。

 実際に配列を変換してみます。

let arr = [0,1,1,2,3,4,4,4,5];
let set = new Set(arr);
console.log(set); // -> Set { 0, 1, 2, 3, 4, 5 }

 簡単ですね。

SetからArrayへの変換

 Arrayオブジェクトには、mapやfilterなどの便利なメソッドがありますが、SetにはforEachしかありません。また、配列へのランダムアクセスもできません。

 ただ、SetからArrayへの変換は以下のように、Array.fromを利用して簡単にできます。

let arr = [0,1,1,2,3,4,4,4,5];
let set = new Set(arr);

let setToArr = Array.from(set);
console.log(setToArr); // -> [ 0, 1, 2, 3, 4, 5 ]

 あるいは、スプレッド構文を利用することもできますが、可読性には注意が必要です。

let arr = [0,1,1,2,3,4,4,4,5];
let set = new Set(arr);

let setToArr = [...set];
console.log(setToArr); // -> [ 0, 1, 2, 3, 4, 5 ]

 一見、スプレッド構文が簡潔に見えますが、可読性に優れているのはArray.from()です。

 スプレッド構文は一時期の三項演算子と同様、市民権を得るためにもう少し時間が掛かりそうですね。

Mapを使う

 Setを使うことには問題があります。distinctしたい対象がObjectの場合、たとえ内容が同じであっても別物として認識されるからです。

 この場合、MapのKeyは常にユニークなため、これを使うのがよい手段かもしれません。

let arr = [{id: 1}, {id: 2}, {id: 2}, {id: 3}];

 このようなデータを扱う場合、IDの要素で重複を排除するためには、以下のようにするとよいでしょう。

let arr = [{id: 1}, {id: 2}, {id: 2}, {id: 3}];
let map = new Map(arr.map(o => [o.id, o]));

console.log(map); // -> Map { 1 => { id: 1 }, 2 => { id: 2 }, 3 => { id: 3 } }

 しかし、オブジェクトにidなどの一意な要素が与えられていない場合は、少し工夫が必要になります。以下のような配列を想定してみます。

let arr = [{name: "alice", telephone: "0x0xxxxxxxx"}, {telephone: "0x0xxxxxxxx", name: "alice"}, {name: "alice", telephone: "0x0xxxxyyyy"}];

 このデータは、nameだけ見れば全てaliceですが、電話番号が違うものがあります。同姓同名のデータを扱うとき、名前と電話番号が同一なら同一人物とみなす、というロジックを考えてみましょう。

 この場合は、キーに双方の文字列を連結したものを指定すれば問題ありません。

const DELIMITER = String.fromCharCode("31");
let arr = [{name: "alice", telephone: "0x0xxxxxxxx"}, {telephone: "0x0xxxxxxxx", name: "alice"}, {name: "alice", telephone: "0x0xxxxyyyy"}];
let map = new Map(arr.map(o => [`${o.name}${DELIMITER}${o.telephone}`, o]));

console.log(map); // -> Map {
                  //      'alice\u001f0x0xxxxxxxx' => { telephone: '0x0xxxxxxxx', name: 'alice' },
                  //      'alice\u001f0x0xxxxyyyy' => { name: 'alice', telephone: '0x0xxxxyyyy' } }

 コード中、デリミタ文字を連結していますが、これが無いと以下のオブジェクトが同一のものとみなされます。

{name: "alice", telephone: "0x0xxxxxxx"}
{name: "alice0", telephone: "x0xxxxxxxx"}

 デリミタ文字には、データ上で絶対に使われない文字またはパターンを使用することが重要です。Wikipediaのこちらの項目に詳しいことが載っているので、参考にしてみてください。

 今回は、ASCII制御文字であるユニット分離文字を使用しました。

MapからArrayへの変換

 やっぱりMapのまま使いたくない、という場合。

Array.from(map.values());

Objectを使う

 もちろんObjectも連想配列としてMapと同様に利用できます。

const DELIMITER = String.fromCharCode("31");
let arr = [{name: "alice", telephone: "0x0xxxxxxxx"}, {telephone: "0x0xxxxxxxx", name: "alice"}, {name: "alice", telephone: "0x0xxxxyyyy"}];
let obj = arr.reduce((acc, cur) =>{
  acc[`${cur.name}${DELIMITER}${cur.telephone}`] = cur;
  return acc;
}, {});

console.log(obj); // -> { 'alice\u001f0x0xxxxxxxx': { telephone: '0x0xxxxxxxx', name: 'alice' },
                  //      'alice\u001f0x0xxxxyyyy': { name: 'alice', telephone: '0x0xxxxyyyy' } }

ObjectからArrayへの変換

 Object.values()関数を使用します。

Object.values(obj);

 ただし、比較的新しい構文のためブラウザのバージョンに注意が必要です。

パフォーマンス計測

 ランダムな重複データを持つデータを作成し、どれが最も高速に動作するかを調べてみます。

テストデータの作成

 まず、0~9999までのデータを作成し、同数のランダムデータを付加した2万件のデータを用意します。

const TESTDATA = (dataLength=>{
  let arr = Array.from({length: dataLength}, (v,i)=>i);
  let rnd = Array.from({length: dataLength}, ()=>Math.floor(Math.random() * dataLength));
  return arr.concat(rnd);
})(10000);

 連続データなどのテストデータを作成するときは、Array.fromが便利ですね。

計測器の作成

 JavaScriptのconsole.timeを利用します。

function measurePerform(func){
  console.time(func.name);
  for(let i = 0; i < 100; i++){
    func();
  }
  console.timeEnd(func.name);
}

 1回の処理ではPCの状況によって正確な値が出ないため、100回処理を繰り返します。

テストメソッドの作成

 実際に、uniqueにする処理を行うメソッドを作成します。

function setTest(){
  let set = new Set(TESTDATA);
  Array.from(set);
}

function mapTest(){
  let map = new Map(TESTDATA.map(o => [o, o]));
  Array.from(map.values());
}

function objTest(){
  let obj = TESTDATA.reduce((acc, cur) =>{
    acc[cur] = cur;
    return acc;
  }, {});
  Array.from(Object.values(obj));
}

完成形

const TESTDATA = (dataLength=>{
  let arr = Array.from({length: dataLength}, (v,i)=>i);
  let rnd = Array.from({length: dataLength}, ()=>Math.floor(Math.random() * dataLength));
  return arr.concat(rnd);
})(10000);

function measurePerform(func){
  console.time(func.name);
  for(let i = 0; i < 100; i++){
    func();
  }
  console.timeEnd(func.name);
}

function setTest(){
  let set = new Set(TESTDATA);
  Array.from(set);
}

function mapTest(){
  let map = new Map(TESTDATA.map(o => [o, o]));
  Array.from(map.values());
}

function objTest(){
  let obj = TESTDATA.reduce((acc, cur) =>{
    acc[cur] = cur;
    return acc;
  }, {});
  Array.from(Object.values(obj));
}

measurePerform(setTest);
measurePerform(mapTest);
measurePerform(objTest);

計測結果

Set Map Object
314.31ms 470.80ms 317.03ms

 Mapは少し遅いようですね。

まとめ

 ES2015(ES6)になって様々なビルトインオブジェクトが新規実装されたため、苦労していた重複の排除も簡単にできるようになりました。

 工夫すればネストされたオブジェクトなども容易にユニークにすることができますが、適切にデータ設計されていればあまり使うことはないでしょう。

 他にも、ES2015(ES6)以降、簡潔な書き方が出来るようになったロジックがありますので、もし時間があったら考えてみてください。