[GAS]連想配列はMapオブジェクトを使おう

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは 「連想配列はMapオブジェクトを使おう」 を、お送りします。

今日のアジェンダ

  • Mapオブジェクトの生成
  • Mapオブジェクトの主なメンバー
  • Mapオブジェクトの中身
  • 2次元配列からの変換
  • dictsMapsの加工処理
  • 2次元配列への変換

はじめに

連想配列とは、キーと値が:(コロン)でセットになった、データの集合です。

{ id: 'tg001', name: 'Tsujike', age: 35 }

これって、オブジェクトじゃないの?と思った方も多いでしょう。

オブジェクトは、連想配列でデータを保有するだけなく、クラスを作成するなどさまざまな機能をもちます。

その反面、処理速度が少し重たくなる、というデメリットもあります。

なので、データを処理するだけなら、連想配列がいいでしょう。

今回は、JavaScriptの連想配列である、Map型についてお届けします。(Array.map()メソッドとは異なります。)

Mapオブジェクトの生成

Mapオブジェクトとは

Mapオブジェクトとは、キーと値が関連付け(マップ)された、集合をあらわすオブジェクトです。

Mapオブジェクトの生成

リテラルはありませんので、new演算子をつかって、オブジェクトを生成します。

引数に要素を渡しながら、マップを生成することもできます。

const m = new Map(); //空のマップを作成する
const n = new Map([["one", 1], ["two", 2]]); //数値にマッピングされたマップを作成する

Mapオブジェクトの主なメンバー

主なメンバーは以下のとおりです。

メンバー種類説明
sizeインスタンスプロパティマップ内にある要素の数を返す
clear()インスタンスメソッドマップ内からすべてのキーと値の組を削除
delete(key)インスタンスメソッドマップ内に要素が存在し、削除された場合はtrueを返す
get(key)インスタンスメソッドkeyで指定されたキーに結び付けられた値を返す
has(key)インスタンスメソッドkeyで指定されたキーに結び付けられた要素がMapオブジェクト内に存在するかどうかを示すbooleanを返す
set(key, value)インスタンスメソッドマップ内のkeyで指定されたキーの値をvalueに設定する
keys()反復処理メソッドマップ内の各要素のキーが挿入順で含まれるイテレーターオブジェクトを返す
values()反復処理メソッドマップ内の各要素の値が挿入順で含まれるイテレーターオブジェクトを返す
entries()反復処理メソッドマップ内の要素に対して挿入順にすべての要素の [key, value] の配列を含む、新しいイテレーターオブジェクトを返す
forEach(callbackFn[, thisArg])反復処理メソッドマップの各要素について関数callbackFnを挿入順に呼び出して処理する

インスタンスプロパティ

要素の数を返すのはsizeプロパティです。

const n = new Map([["one", 1], ["two", 2]]);
console.log(n.size); // => 2

インスタンスメソッド

マップにたいして、要素を参照したり、削除したり、追加したりしてみましょう。

const n = new Map([["one", 1], ["two", 2]]);
console.log(n.get('one')); // => 1
n.delete('two');
n.set('three',3);
console.log(n.size);// => 2

反復処理メソッド

Mapオブジェクトは反復可能なオブジェクトです。スプレッド演算子を使うと、キーと値を要素にもつ配列を生成できます。

取り出す要素は、1番目がキー、2番目が値です。

const n = new Map([["one", 1], ["two", 2]]);
const array = [...n]; // [ [ 'one', 1 ], [ 'two', 2 ] ]

for of文では、以下のように分割代入と組み合わせることができます。

const n = new Map([["one", 1], ["two", 2]]);
for(const [key, value] of n){
  console.log(`${key}:${value}`); // => one:1, two:2
}

反復処理メソッドは、キーや値のみの配列を生成できます。

const n = new Map([["one", 1], ["two", 2]]);
const keys = [...n.keys()]; //	[ 'one', 'two' ]
const values = [...n.values()]; //	[ 1, 2 ]
const entries = [...n.entries()]; // [ [ 'one', 1 ], [ 'two', 2 ] ] これは[...n]と同じ

ArrayクラスのforEach()メソッドは、マップオブジェクトにも実装されています。

Arrayクラスと同様に、マップの各要素についてコールバック関数を挿入順に呼び出して処理します。

コールバック関数の引数が、value, keyの順であることもおなじです。

さきほどの、for of文などの取り出し順と異なりますので、混同しないようにしましょう。

const n = new Map([["one", 1], ["two", 2]]);
n.forEach( (value, key) => {
  console.log(`${key}:${value}`); // => one:1, two:2
});

Mapオブジェクトの中身

Mapオブジェクトのset()メソッドの戻り値は、Mapオブジェクトです。

なので、このように、メソッドチェーン記法で、マップを生成できます。

const n = new Map().set("one", 1).set("two", 2);

生成されたマップは、{ one: 1, two: 2, } のような構造になっていると想像しますが、直接ログ出力ができません

const n = new Map().set("one", 1).set("two", 2);
console.log(n); // => {} 

Mapオブジェクトの、反復処理メソッドの戻り値は、 Mapの新しいイテレーターオブジェクトです。

これらも同様に、直接ログ出力ができません

よくある質問だと思いますので、理解しておきましょう。

const n = new Map().set("one", 1).set("two", 2);
console.log(n.keys()); // => {} 
console.log(n.values()); // => {} 

2次元配列からの変換

GASでスプレッドシートのデータを加工するときは、まず、2次元配列で取得して、加工します。

そのさい、配列のインデックスだと、数えるのも大変ですし、列が変更されてもいいように、2次元配列を連想配列に変換するテクニックがあります。

//このような配列のままだと、インデックスで配列を操作することになってしまう
[['id', 'name', 'age'], ['tg001', 'Tsujike', 35], ['tg002', 'Etau', 38]]

//なので、このような連想配列に変換する
[{id:'tg001', name:'Tsujike', age:35}, {id:'tg002', name:'Etau', age:38}]

この、連想配列を、ディクショナリ配列と呼ぶことにします。(今まではobjectArrayと呼んでいました。)

それでは、ディクショナリを、Mapオブジェクトを使って処理してみましょう。

完成したものは、ディクショナリのMapオブジェクト配列なので、dictsMapsと、呼ぶことにします。

map()メソッドによる変換

map()メソッドのネストによる変換はこちらです。

function myFunction2_01() {

  const [header, ...records] = [['id', 'name', 'age'], ['tg001', 'Tsujike', 35], ['tg002', 'Etau', 38]];

  const dictsMaps = records.map(record => {
    const obj = new Map();
    header.map((element, index) => obj.set(element, record[index]));
    return obj;
  });

  console.log(dictsMaps); // =>	[ {}, {} ]
  console.log([...dictsMaps[0]]); // => [['id', 'tg001'], ['name', 'Tsujike'], ['age', 35]]
  console.log([...dictsMaps[1]]); // => [['id', 'tg002'], ['name', 'Etau'], ['age', 38]]

}

reduce()メソッドによる変換

map()メソッドと、reduce()メソッドのネストによる変換はこちらです。

function myFunction2_02() {

  const [header, ...records] = [['id', 'name', 'age'], ['tg001', 'Tsujike', 35], ['tg002', 'Etau', 38]];

  const dictsMaps = records.map(
      record => record.reduce((acc, value, index) => acc.set(header[index], value), new Map())
    );

  console.log(dictsMaps); // => [ {}, {} ]
  console.log([...dictsMaps[0]]); // => [['id', 'tg001'], ['name', 'Tsujike'], ['age', 35]]
  console.log([...dictsMaps[1]]); // => [['id', 'tg002'], ['name', 'Etau'], ['age', 38]]

}

社内コーディングガイドライン

1万行程度のデータで確認しましたが、map()メソッドタイプと、reduce()メソッドタイプの、処理速度はほとんど変わりませんでした。

なので、弊社では、reduce()メソッドタイプで統一しようと思います。

dictsMapsの加工処理

dictsMapsは、とても便利です。

配列の各要素を、インデックスではなく、header(スプレッドシートの見出し行)で指定できます。

フィルターを掛ける

  const [header, ...records] = [['id', 'name', 'age'], ['tg001', 'Tsujike', 35], ['tg002', 'Etau', 38]];

  const dictsMaps = records.map(
    record => record.reduce((acc, value, index) => acc.set(header[index], value), new Map())
  );

  //37歳以上の人をフィルター掛けする
  const filter = dictsMaps.filter(dictsMap => dictsMap.get('age') >= 37);
  console.log(filter.length); // => 1
  console.log([...filter[0]]); // => [['id','tg002'], ['name','Etau'], ['age',38]]

合計値を出す

  //平均年齢を出す
  const total = dictsMaps.map(dictsMap => dictsMap.get('age')).reduce((acc, cur) => acc + cur);
  console.log(total / dictsMaps.length); // => 36.5 ((35 + 38) / 2)

不要な列を削除する

  //不要な列を削除する
  dictsMaps.forEach(dictsMap => dictsMap.delete('age'));
  console.log(dictsMaps[0].size); // => 2
  console.log([...dictsMaps[0]]); // => [['id','tg001'], ['name','Tsujike']]
  console.log([...dictsMaps[1]]); // => [['id','tg002'], ['name','Etau']]

2次元配列への変換

スプレッドシートへsetValuesするために、dictsMapsを2次元配列に変換します。

全体を変換する

dictsMaps全体を、変換するコードはこちらです。

  const [header, ...records] = [['id', 'name', 'age'], ['tg001', 'Tsujike', 35], ['tg002', 'Etau', 38]];

  const dictsMaps = records.map(
    record => record.reduce((acc, value, index) => acc.set(header[index], value), new Map())
  );

  const finalRecords = dictsMaps.map(dictMap => [...dictMap.values()]);
  console.log(finalRecords); // => 	[['tg001','Tsujike',35], ['tg002','Etau',38]]

必要列を指定して変換する

列を選択した2次元配列を、変換するコードはこちらです。

  //必要な列で構成する
  const newHeaders = ['id', 'name'];
  const newRecords = dictsMaps.map(dictsMap => newHeaders.map(key => dictsMap.get(key)));
  console.log(newRecords); // => [ [ 'tg001', 'Tsujike' ], [ 'tg002', 'Etau' ] ]

いかがでしたでしょうか。

まとめ

以上で、「連想配列はMapオブジェクトを使おう」をお送りました。

実は、連想配列をオブジェクトで操作したばあいと、Mapオブジェクトで操作したばあいは、処理速度に違いがありました。

明らかに、Mapオブジェクトの方が高速です。

弊社では、「連想配列はMapオブジェクトを使う(ただし、UrlFetch()メソッドなどのパラメーター生成時はのぞく)。」 をコーディングガイドラインとします。

参考資料

Comments

Copied title and URL