連絡先の電話番号やメールアドレスを並び替える(Google Apps Scriptで)

# やること: 連絡先の電話番号やメールアドレスの並び替え
Google Apps Scriptで連絡先の電話番号やメールアドレスの並び替えをしてみたいと思います。
**連絡先(人)の並び替えではなくて**、1人の連絡先の中に複数の電話番号(携帯、家電、職場…)があったり、複数のメールアドレス(キャリア、Gmail、Yahoo、会社…)があったりするわけですが、これらの電話番号やメールアドレスを一定のルールのもと、並び替えようという試みです。

# 背景
何故かGoogle連絡先には電話番号やメールアドレスの項目順を入れ替える機能がないんですよね…加えて、僕が使っているMacOSの連絡先やiOSの連絡先にもそういった機能がなくて。サードパーティのアプリも探してみたものの、探し方が悪いのかヒットしなくて…

皆さん、あまり項目の順序なんて気にされないんですかね?
よく使う電話番号がよく使わない番号より下にあったら不便だと思うのですが。
手作業で直したり、エクスポート→Excelとかで加工→インポートしたりとかされているんでしょうか。

僕の場合、[別の投稿](https://qiita.com/ksyk/items/449f967302e68814db1c)でも言ったように、これまでに使用したメールエイリアスの大半を自分の連絡先に蓄えているという事情もあり、(項目数が多いので)手作業で切り貼りしての並び替えは避けたかったという事情もあります。

# 必要な知識
Google Apps Scriptで扱う連絡先(`Contact`)のクラスやメソッドは下記を参考にしてください。
https://developers.google.com/apps-script/reference/contacts/contact

中でも、

- `Contact`
  - `addEmail(label, address)`
  - `getEmails()` : `EmailField[]`
- `EmailField`
  - `getLabel()` : `Object` ( `String` OR Enum `Field` )
  - `getAddress()`
  - `deleteEmailField()`

辺りが肝になります。

# やってみよう
手元で動かしているロジックは、もっといろんなことをやっているので、本質的な部分を切り出して紹介します。
(間違いを見つけられましたら、指摘してもらえると嬉しいです。)
それから、タイトルは「項目の並び替え」としていますが、見ていただくとわかるとおり「重複項目の削除」も行っています。

***言うまでもありませんが、自己責任でお願いします。電話番号やメールアドレスを削除するメソッドが含まれますので、バックアップを取るとか、削除(と追加)の部分はコメントアウトして動かすなど、各自対策していただければ。***

```.js
function sortFields() {
  // 0.1. 通知用
  const slackApp = new SlackApp();

  ContactsApp.getContacts().forEach(function (contact) {
    // 0.2. 会社(削除時通知用)
    const companies = contact.getCompanies();
    // 0.3. フルネーム(削除時通知用)
    const fullname = 
      (companies.length ? companies[0].getCompanyName() : "") + 
      contact.getFamilyName() + contact.getGivenName();
    
    // 1. 同じメールアドレスがいくつも登録されていたら削除する

    // 1.1. メールフィールド配列(登録状態)
    const emailFieldsOriginal = contact.getEmails();
    // 1.2. メールフィールド配列(ユニークにする)
    const emailFieldsUniq = emailFieldsOriginal.uniq(function (field1, field2) {
      return field1.getAddress() === field2.getAddress();
    });
    // 1.3. メールフィールド配列(重複分 = 登録状態 - ユニーク化したもの)
    const emailFieldsDiff = _.difference(emailFieldsOriginal, emailFieldsUniq);

    // 1.4. 重複削除
    emailFieldsDiff.forEach(function (field) {
      // 1.4.1. Slackで自分に通知
      slackApp.sendAttachments(
        fullname + "の連絡先から重複フィールド(" + field.getLabel() + ": " + field.getAddress() + ")を削除します。");
      // 1.4.2. 重複フィールドの削除
      field.deleteEmailField();
    });
    
    // 2. 並び替え

    // 2.1. メールフィールド配列(並び替え用)
    const emailFieldsSort = emailFieldsUniq.concat();
    // 2.2. 並び替え(並び替えのアルゴリズムは比較関数(Comparator)で提供)
    emailFieldsSort.sort(function (field1, field2) {
      return ※ // -1 OR 0 OR +1;
    });
    // 2.3. 並び替えで変更があったら、並び替えを連絡先に適用
    if (JSON.stringify(emailFieldsUniq) != JSON.stringify(emailFieldsSort)) {
      emailFieldsSort.forEach(function (field) {
        // 2.3.1. 新しい項目として連絡先に追加
        contact.addEmail(field.getLabel(), field.getAddress());
        // 2.3.2. 元からあった項目は消す
        field.deleteEmailField();
      });
    }
  });
}
```

# 重複項目を削除しよう

全体的にな流れ(アルゴリズム)としては、(メールアドレスで)ユニークにしたものと、する前のものを比較して、その差(=重複分)を削除するものとなります。
重要なのは比較対象がオブジェクト(`EmailFiled`)なので、「比較」には一工夫が必要ということです。

```js
    // 1.1. メールフィールド配列(登録状態)
    const emailFieldsOriginal = contact.getEmails();
```
```js
    // 1.2. メールフィールド配列(ユニークにする)
    const emailFieldsUniq = emailFieldsOriginal.uniq(function (field1, field2) {
      return field1.getAddress() === field2.getAddress();
    });
```
さらっと`uniq`なるメソッドを呼んでいますが、自前で実装したメソッド(下記参照)です。
引数に比較関数を与え、「何をもって等価、何を持って重複と見なすのか」を提供します。
`Array.indexOf()`で探したり、(後で出てくる)Undersocreの`uniq`を使うにしても、オブジェクトのポインタ同士を比較しても意味がないからです。

今回の要件はメールアドレスが同じであれば重複とみなすので、「メールアドレスで比較する」知識を持たせることにしています。

```js
Array.prototype.uniq = function (callback) {
  return this.filter(function (element, i, array) {
    for (var j = 0; j <= i; j++) {
      if (callback.call(this, element, array[j])) {
        return i === j;
      }
    }
    return false;
  });
}
```
```js
    // 1.3. メールフィールド配列(重複分 = 登録状態 - ユニーク化したもの)
    const emailFieldsDiff = _.difference(emailFieldsOriginal, emailFieldsUniq);
```
`_`は[Undersocore](https://underscorejs.org/)のメソッドです。
(メールアドレスで)ユニークにしたものと、する前のものを比較して、その差分が重複分なので。
ここはオブジェクト同士の比較で大丈夫ですよね。

```js
    // 1.4. 重複削除
    emailFieldsDiff.forEach(function (field) {
           :
          中略
           :
      // 1.4.2. 重複フィールドの削除
      field.deleteEmailField();
    });
```
# 一定のルールで項目を並び替えよう

結論から言うと、各`EmailField`を地道に1個1個、追加と削除を繰り返すことになります。
本当は`Contact`クラスに登録順を入れ替えられたり、任意の位置に挿入できるメソッドがあればいいのですがね。
ですので、流れとしては「並び替えた`EmailField`を新たに新規追加(末尾に追加)」、「追加元のフィールドは削除する」というものになります。

```js
    // 2.1. メールフィールド配列(並び替え用)
    const emailFieldsSort = emailFieldsUniq.concat();
```
あとでこの配列を並び替えることになるのですが、オリジナルの配列は取って置きたいので、配列を複製します。
後ろに`concat()`を付けているのは、「配列を複製」したいからです。(単なる代入では複製にはなりませんよね)

```js
    // 2.2. 並び替え(並び替えのアルゴリズムは比較関数(Comparator)で提供)
    emailFieldsSort.sort(function (field1, field2) {
      return ※ // -1 OR 0 OR +1;
    });
```
ここが肝なのですが、肝心なところは書いてないです…
オブジェクト(`EmailField`)の並び替えなので、自然順序付けは使えません。ここでも順序付けの知識を与える「比較関数」が必要なのです。
`sort`メソッドには引数に比較関数を渡します。比較関数は引数に2つの要素を受け取り、

- `引数1` < `引数2`であれば`-1`
- `引数1` > `引数2`であれば`+1`
- `引数1` = `引数2`であれば`0`

を返却することで、`sort`メソッドはオブジェクトの大小関係が理解できるようになり、並び替えを行います。

肝心な実装ですが、並び替えの要件は読み手それぞれなので、ここでは記載してないです。
僕の場合は`EmailField`のラベルと、メールアドレスのドメインを元に点数化して、大小関係を比較しています。ラベルが「廃止」とかだったら下位になるようにしたり、ドメインがキャリアメールだったら／クラウドサービスだったら、上位にする／下位にするとか、そんな具合です。
需要がありそうだったら、実装を公開するかもしれませんが、限りなく個人情報に近い要素があったりするわけで…

```.js
    // 2.3. 並び替えで変更があったら、並び替えを連絡先に適用
    if (JSON.stringify(emailFieldsUniq) != JSON.stringify(emailFieldsSort)) {
           :
          後述
           :
    }
```
後述の`EmailField`の追加・削除はコストがかかるので、並び替えの必要がない(=並び替え前後で変更がない)のなら、並び替えをスキップします。(そのために2.1.で配列をコピーしておきました)
配列の順序が変わっていないか比較するために、(荒っぽいですが)`JSON.stringify`化して比較しています。

```.js
      emailFieldsSort.forEach(function (field) {
        // 2.3.1. 新しい項目として連絡先に追加
        contact.addEmail(field.getLabel(), field.getAddress());
        // 2.3.2. 元からあった項目は消す
        field.deleteEmailField();
      });
}
```
新規の項目として末尾に追加、既存の項目は削除、これらを繰り返すことで、連絡先の`EmailField`が並び替えた配列順になります。

# 電話番号の並び替えはどうなるのか
電話番号の並び替えも上記と同じ要領でやるしかなさそうです。
基本的には`Email`を`Phone`に、`getAddress`を`getPhoneNumber`に読み替えることで、実現できます。
並び替えの比較関数の中身はきっとラベル(自宅を優先とか…)や電話番号の市外局番(携帯を優先とか…)にするといいと思います。

# 最後に
最後まで読んでくださってありがとうございます。
連絡先の中の電話番号やメールアドレスの並び替えの仕方がわからなくて、それなりに調べたのですが、それらしい方法に行き当たらず少々苦労したので公開することにしました。
もしかしたら、調べ方が悪くて、車輪の再発明だったり、より良い方法があるのかもしれませんが。

あとは自分の経験がなさすぎて、記述の粒度というか、何をどの程度まで書けばいいのかが悩みどころです。
今回で言うと比較関数の説明が長過ぎたかも。
記載するか否かを迷ったときは(億劫にならない程度に)記載することにしました。記載しない方に倒すと、そもそもこの投稿自体がいらないということにもなりかねないので…

それでは。