結論
最初に結論を言います。
これを
const getGoogleFiles = () => {
const files = DriveApp.getFiles();
const result = [];
while (files.hasNext()) {
const file = files.next();
const fileName = file.getName().toLowerCase();
if (fileName.includes('google')) result.push(file.getUrl());
}
Logger.log(result);
};
wrapper関数を1つ噛ませてこう書く方が好きという話です。
const getGoogleFiles = () => {
const result = gasUtils.toIterable(
DriveApp.getFiles(),
{
mapFn: (file) => file.getUrl(),
filterFn: (file) => file.getName().toLowerCase().includes('google'),
}
);
Logger.log([...result]);
ちなみにwrapper関数toIterable
は以下です。
const gasUtils = {
/**
* DriveAppのFileIteratorとFolderIteratorをiterableにする関数
* @template {DriveApp.FileIterator | DriveApp.FolderIterator} GASIterator
* @template {DriveApp.File | DriveApp.Folder} GASFileOrFolder
* @param {GASIterator} iter - Google Apps Scriptのイテレータ
* @param {{
* mapFn: (val: GASFileOrFolder, index: number) => any
* filterFn: (val: GASFileOrFolder, index: number) => boolean
* }}
* @return {Generator}
*/
toIterable: (iter, {
mapFn = (val) => val,
filterFn = () => true,
}) => {
return {
*[Symbol.iterator]() {
let i = 0;
while (iter.hasNext()) {
const [nv, ni] = [iter.next(), i++];
if (filterFn(nv, ni)) yield mapFn(nv, ni);
}
}
};
},
};
ご興味があればどぞ。
はじめに
Google Apps Scriptは便利です。
ちょっとしたツールや自動化にはもってこいです。
Google系のサービスとは簡単に連携できます。
そんなわけで、Google Drive上のファイルやフォルダを操作したくなるわけです。
マイドライブの中でファイル名にgoogle
の文字が入るもののURLの一覧が取得したい…!
それもSpreadsheetに一発で貼り付けられるように配列で取得したい…!!
調べてみればDriveApp
なるものが簡単に見つかります。
早速試してみましょう。
/** !!これは動きません!! */
const getGoogleFiles = () => {
// ファイルを全て取得
const files = DriveApp.getFiles();
// 名前に'google'を含むファイルのURLを取得
const result = files.flatMap(file => {
const fileName = file.getName().toLowerCase();
return fileName.includes('google')? [file.getUrl()] : [];
});
// 出力を確認
Logger.log(result);
};
これでエディタからgetGoogleFiles
を実行すればめでたく目標のものがとれるはず。
とっても簡単だぁ。
と思いきや、帰ってくるのはエラー。
TypeError: files.flatMap is not a function
な、なんだってー?!
それもそのはず、DriveApp.getFiles
の返り値の型はDriveApp.FileIterator
。
配列じゃないのです。イテレータなのです。
というわけで、目的の結果を得るには次のように書く必要があります。
const getGoogleFiles = () => {
// ファイルを全て取得
const files = DriveApp.getFiles();
// 名前に'google'を含むファイルのURLを取得
const result = [];
while (files.hasNext()) {
const file = files.next();
const fileName = file.getName().toLowerCase();
if (fileName.includes('google')) result.push(file.getUrl());
}
// 出力を確認
Logger.log(result);
};
うーん。なんともコレジャナイ感。
何が不満なのか
個人的な嗜好…もとい志向として以下が気に入りません
- 最初に空の配列を定義して
while
ループでpush
するのがつらい。破壊的メソッドは滅ぶべし - 終端判定のメソッドが
hasNext
なところ。MDNのイテレータの説明ではdone
なのに - iterableじゃない。反復可能ならスプレッド構文なんかで簡単に配列化できるのに
といわけで、iterable(反復可能)にするwrapper関数を挟めば使い勝手が良くなりそう。
どちらにせよwrapper関数はあった方が将来的な保守性が上がるので、やらない手はない。
余談 反復可能にする。どうやって?
(ご存じの方は飛ばしてください)
JavaScriptのオブジェクトには2種類ある。
反復可能なオブジェクトと、反復可能ではないオブジェクトだ。
その違いは、反復可能なオブジェクトとは**@@iterator
メソッド**が実装されているか否かです。
// 反復可能じゃない
const notIterable = {
hoge: 'hoge',
fuga: 'fuga',
piyo: 'piyo',
};
// 反復可能
const iterable = {
[Symbol.iterator]: function* () {
yield 'hoge';
yield 'huga';
yield 'piyo';
}
};
// 省略記法 iterableと意味は同じ
const shortIterable = {
*[Symbol.iterator]() {
yield 'hoge';
yield 'huga';
yield 'piyo';
}
};
この*
がついている厳めしい関数はジェネレータと呼ばれるものです。
肩肘張るようなものでもなく、yield
で値を返して実行を一旦停止する関数といった感じ。
重要なのはこの**Symbol.iterator
という名前のメソッドがジェネレータで実装されていること**。
これが反復可能か、そうでないかの分かれ道です。
とにかく、この名前のジェネレータのメソッドを持っていればそのオブジェクトは反復可能です。
では反復可能だと何が嬉しいの?といったところですが、for-of
やスプレッド構文で各値を取り出すことができます。
// for-ofで各値を取り出せる
for (const item of iterable) {
Logger.log(item);
// expected output:
// hoge
// huga
// piyo
}
// スプレッド構文で各値を取り出せる
Logger.log([...iterable]);
// expected output:
// [hoge, huga, piyo]
// for-ofで取り出せない
for (const item of notIterable) {
Logger.log(item);
// TypeError: notIterable is not iterable
// 地味にこのエラーメッセージ面白い
}
察しの良い方はお気づきでしょうが、Array
オブジェクトなんかには元々この@@iterator
メソッドが実装されています。
とっても便利だぁ!
ともあれこの@@iterator
メソッドがApps Scriptのイテレータ、厳密にいうとDriveApp.FileIterator
やDriveApp.FolderIterator
といったオブジェクトに実装されていないことが問題なわけです。
ならどうすればいいか?
Apps Scriptのイテレータの各値をyield
するジェネレータを@@iterator
メソッドとして持ったオブジェクトを返す関数があれば必要十分ですね。
そう考えると案外簡単かもしれない。
Simple is best
複雑なのは良くないので、できるだけシンプルに反復可能性だけ付与します。
// Apps Scriptのイテレータ類をiterableにする
const toIterable = (iter) => {
return {
*[Symbol.iterator]() {
while(iter.hasNext()) yield iter.next();
}
};
};
const getGoogleFiles = () => {
const files = toIterable(DriveApp.getFiles());
const result = [...files].flatMap(file => {
const fileName = file.getName().toLowerCase();
return fileName.includes('google')? [file.getUrl()] : [];
});
Logger.log(result);
};
うむ。悪くない。
大分最初の姿に近づくことができました。
wrapper関数のコード量もそれほど多くなく、必要十分にシンプルだと思います。
ただ、ちょっと寄り道が多すぎるかもしれない。
特に、一旦配列にしてからflatMap
するところ。
ものごとはできるかぎりシンプルにすべきだ。しかし、シンプルすぎてもいけない。
先程の実装はスタート地点としてはこれ以上なく優秀でした。
しかし、もう少しwrapper関数に機能を追加すれば格段に便利かもしれません。
例えば、次のようなmapFn
とfilterFn
を追加してみるのはどうでしょう。
// Apps Scriptのイテレータ類をiterableにする
const toIterable = (iter, {
mapFn = (val, index) => val,
filterFn = (val, index) => true,
}) => {
return {
*[Symbol.iterator]() {
let i = 0;
while (iter.hasNext()) {
const [nv, ni] = [iter.next(), i++];
if (filterFn(nv, ni)) yield mapFn(nv, ni);
}
}
};
};
const getGoogleFiles = () => {
const result = toIterable(
DriveApp.getFiles(),
{
mapFn: (file) => file.getUrl(),
filterFn: (file) => file.getName().toLowerCase().includes('google'),
}
);
// これまでと同じ結果
Logger.log([...result]);
};
filterFn
で条件を満たしたものだけyield
され、mapFn
の処理を通したものを取得します。
それほどwrapper関数のコード量も増えず、かつ本関数内の変数がresult
1つになります。
ループの回数も減るので、気持ちが穏やかになります。
ちなみにこのコード内では使用していませんが、mapFn
とfilterFn
の二番目の引数でインデックスも使えるようにしています。こんなんなんぼあってもいいですからね。
個人的にはこれが一番使いやすいと思います。
getGoogleFiles
の中の処理の見通しもこれが一番良く感じます。
JSDocで補完が効くようしたり
Apps ScriptではJSDocを利用してエディタの補完を効かせるようにすることができます。
よって以下のようにすればmapFn
やfilterFn
の中でもきちんと補完が効くようになります。
const gasUtils = {
/**
* DriveAppのFileIteratorとFolderIteratorをiterableにする関数
* @template {DriveApp.FileIterator | DriveApp.FolderIterator} GASIterator
* @template {DriveApp.File | DriveApp.Folder} GASFileOrFolder
* @param {GASIterator} iter - Google Apps Scriptのイテレータ
* @param {{
* mapFn: (val: GASFileOrFolder, index: number) => any
* filterFn: (val: GASFileOrFolder, index: number) => boolean
* }}
* @return {Generator}
*/
toIterable: (iter, {
mapFn = (val) => val,
filterFn = () => true,
}) => {
return {
*[Symbol.iterator]() {
let i = 0;
while (iter.hasNext()) {
const [nv, ni] = [iter.next(), i++];
if (filterFn(nv, ni)) yield mapFn(nv, ni);
}
}
};
},
};
もちろん関数の動作は変わりません。
が、しれっとgasUtils
というオブジェクトで包みました。
なので、getGoogleFiles
のtoIterable
の参照をgasUtils.toIterable
に変えてください。
ちなみにこれをすると
- 名前空間が切れる
- エディタの実行関数候補から
toIterable
が消える
という効用があります。
エディタから実行する予定がない関数は積極的にオブジェクトで包みましょう。
みなが幸せになります。
おわりに
大体伝えたかったことは以上です。
強いて言うなら速度比較とかもしたかったですが、とりあえずここまでで一旦投稿しておきます。
よろしくお願いします。