【ちいかわ占いの順位を争う人へ】GASでちいかわ占いの順位を集計してみた！！

本稿はSTECH春の交流まつり1週目の記事となります。
初心者向けとして、単語に解説を入れていきます。

# 挨拶
こんにちは、マグロです。
皆さん、ちいかわは好きですか？

僕は好きすぎて、、、



***友人たちとちいかわ占いで争っています。***

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/50c19370-d225-eb24-85d7-905759465c80.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/7cd76cab-da0d-5f53-d3d6-cd77f2fe0013.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/817fc9cb-2e26-6d36-81a4-50c8b75e9a29.png)

***激化しすぎて親にドン引きされてます。***

https://twitter.com/sigumataityouda/status/1618500940784635906?t=pqlYBoADeDGgT4PL6cd8Og&s=19

しまいには**月間の順位を集計し争うことに、、、**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/cc14ab1c-fb7e-0fcd-5f96-1c7985a26f76.png)


# 背景
挨拶の通り、ちいかわ占いの順位を月ごとに集計することになりました。
当初は友人が手動で集計していましたが、

- 手動で集計するのに手間がかかる
- ローカルのExcelで集計していて、全員が現時点での順位を見られない

という問題がありました。

じゃあGASとスプレッドシートを使用して自動集計してみよう！！
という動機で作りました。

## GASとは
Google Apps Script(GAS)の略称で、主にGoogleのサービスの自動化の際に扱うスクリプトとなります。
HTMLと組み合わせて、簡単なWebアプリの作成も可能です。

トリガーと呼ばれる、時間を指定して関数を実行する機能もあります。
用途としてLINEBotの作成によく使用されます。

## スプレッドシートとは
正式名称はGoogleスプレッドシートといいます。
Googleが提供する表計算ソフトで、Excelとの違いとしてWebから操作できて複数人での共有が可能という点があります。
また上記のGASから操作することが可能です。

この二つを組み合わせて、ちいかわ占いの順位を集計してみましょう。

# 占い結果はどこから？
めざましテレビの公式サイトに、占いの結果が表示されているページがあります。

https://www.fujitv.co.jp/meza/uranai/index.html

当初はここからスクレイピング[^1]していましたが、GitHubに公開していたらなんとissuesからjsonファイル[^2]があるとのご指摘が、
調べてみると、、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/e07369d9-c820-d44e-fda7-82a769d44d85.png)

この```uranai.json```ですね。

https://www.fujitv.co.jp/meza/uranai/data/uranai.json

はい、1位から12位までの情報がありますね。
コイツから取得すればよさそうです。

、、、が上記のスクリーンショットは3月11日(**土**)に撮ったものです。
めざましテレビは土曜日も放送していて、占いもあるのですが、なぜか月～金までの順位しか表示されません。

土曜日はどこに結果があるの？？と友人に聞いてみると、、、

http://fcs2.sp2.fujitv.co.jp/fortune.php

なんとめざましテレビコンテンツサイトに掲載されているそうです。
なんで分かれているんでしょう、、、

土曜日のみコンテンツサイトからスクレイピングします。

まとめると
- 月～金はjsonから順位を取得
- 土曜日のみコンテンツサイトからスクレイピング

といった形で集計を行います。

## いつ集計する？
順位はめざましテレビ終了と同時に更新されるそうです。[^3]
月～金は8時に終了しますが、土曜日のみ8時半に終了します。

ということで毎日8時半あたりに定期実行をかけます。
これにはGASのトリガー機能を使います。詳細は後述します。

# 使うもの
- **Google Apps Script**
- **Google スプレッドシート**

ほかにもDiscord WebHookを使って、順位とラッキーポイントを投稿する機能も実装していますが、今回は集計するだけなので省きます。

# 設計
大まかな流れです。

1.スプレッドシートの情報を取得する。
2.名前が「年/月」のシート名を取得、ない場合は作成する。
3.jsonを取得する。ここに更新日時も記載されているので取得する。
4.現在時刻を取得する。日曜日だった場合終了。また本日分が書き込まれていれば終了。
5.土曜日の場合、または月初めの場合、フジテレビコンテンツサイトからスクレイピングする。
6.取得した順位をスプレッドシートに書き込む。合計点を計算し順位付けする関数も埋め込む。

# 実装
## スプレッドシートの用意
Google Driveに移行し、新規をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/a65fcb92-33c7-fa7e-0579-55e06c5cf778.png)
```空白のスプレッドシート```から新規で作成します。
その後シートの画面に移行します。URLの
```docs.google.com/spreadsheets/d/{シートid}/``` 
のシートidを控えておきましょう。
## GASの立ち上げ
Google Driveに移行し、新規をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/9f24c338-64c8-5680-03ae-2677472df3a7.png)
その他の欄から```Google Apps Script```を選択します。
ない場合は```アプリを追加```から追加してください。

するとエディタが開くと思うので、コーディングをしていきます。
その前にスクレイピング用のライブラリ、Parserを追加します。
エディタのライブラリ欄にある+をクリックし、以下のライブラリidで検索します。

```
1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/0f9400fa-b554-c6ba-8e5a-6d93a5c0ff51.png)
こんな感じの画面になると思うので保存をクリックします。すると使えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/fad8e756-2ca9-8669-7873-188ee17fd1e1.png)


## コーディング
### 定数
```js:コード.gs
const JSONURL = "https://www.fujitv.co.jp/meza/uranai/data/uranai.json"
const SATURDAYURL = "http://fcs2.sp2.fujitv.co.jp/fortune.php"
// スプレッドシートのid
const SSID = ""
const SEIZA = {
    "おひつじ座":'B',
    "おうし座":'C',
    "ふたご座":'D',
    "かに座":'E',
    "しし座":'F',
    "おとめ座":'G',
    "てんびん座":'H',
    "さそり座":'I',
    "いて座":'J',
    "やぎ座":'K',
    "みずがめ座":'L',
    "うお座":'M'
}
```
```SSID```には先ほど控えておいたスプレッドシートのidを入れてください。
```SEIZA```は連想配列を用いてそれぞれ対応する列を格納しています。使い方は後ほど説明します。

### main
```js:コード.gs
function main(){
  // jsonを取得
  const response = UrlFetchApp.fetch(JSONURL).getContentText();
  const uranaiJson = JSON.parse(response);

  // yyyy/MM/dd の形式で現在の日付を取得
  const dayNow = new Date()
  const data = Utilities.formatDate(dayNow, "Asia/Tokyo", "yyyy/MM/dd");

  // シートの名前を年月で指定
  const mon = Utilities.formatDate(dayNow, "Asia/Tokyo", "yyyy/MM");

  const spreadSheet = SpreadsheetApp.openById(SSID);
  let sheet = spreadSheet.getSheetByName(mon)

  // O-Zの配列を作成
  const oToZ = "OPQRSTUVWXYZ".split("");
  let i = 0

  // シートが存在しない場合、作成
  if (sheet === null){
    sheet = spreadSheet.insertSheet().setName(mon);
    sheet.getRange('A1').setValue("日付")
    sheet.getRange('N2').setValue("月間順位")
    sheet.getRange('N3').setValue("順位の合計")
    Object.keys(SEIZA).forEach(seiza => {
      sheet.getRange(SEIZA[seiza]+'1').setValue(seiza)
      sheet.getRange(oToZ[i]+'1').setValue(seiza)
      i++
    })
  }

  // 最終更新日と今日を比較(時間分秒を取り除く)
  const lastDay = new Date(Utilities.formatDate(new Date(uranaiJson["date"]), "Asia/Tokyo", "yyyy/MM/dd"))
  const nowTime = new Date(Utilities.formatDate(new Date, "Asia/Tokyo", "yyyy/MM/dd"))

  // 日曜日の場合、終了
  if (
    dayNow.getDay() === 0
  ){
    return
  }

  // 土曜日、年末の場合、フジテレビコンテンツストアからスクレイピング
  if (
    dayNow.getDay() === 6 ||
    lastDay < nowTime
  ){
    // すでに書き込まれていた場合終了
    if (data === getLastTime(sheet)){
      return
    }
    saturdayUranai()
  }else{
    uranai(uranaiJson)
  }

  i = 0
  // 一番下の要素の行数を取得
  let lastRaw = sheet.getLastRow()
  Object.keys(SEIZA).forEach(seiza =>  {
    sheet.getRange(oToZ[i]+'1').setValue(seiza)
    sheet.getRange(oToZ[i]+'3').setFormula('=SUM('+SEIZA[seiza]+'2:'+SEIZA[seiza]+lastRaw+')');
    sheet.getRange(oToZ[i]+'2').setFormula('=RANK('+oToZ[i]+'3,$O3:$Z3,1)');
    i++
  })
}
```

ポイント
```js
  // jsonを取得
  const response = UrlFetchApp.fetch(JSONURL).getContentText();
  const uranaiJson = JSON.parse(response);
```
jsonを取得して```JSON.parse```で展開しています。
これでプログラム内でjsonの内容を扱えるようになります。

```js
  // yyyy/MM/dd の形式で現在の日付を取得
  const dayNow = new Date()
  const data = Utilities.formatDate(dayNow, "Asia/Tokyo", "yyyy/MM/dd");

  // シートの名前を年月で指定
  const mon = Utilities.formatDate(dayNow, "Asia/Tokyo", "yyyy/MM");
```
```new Date```で時刻を取得しています。
```new Date('2023/3/12')```とすると2023年3月12日0時0分を時間を扱う型として受け取れます。
引数がない場合、現在の時刻を取得します。

```Utilities.formatDate```は、上記の時間を文字列に変換することができます。
現在時刻が2023年3月12日0時0分とすると、
dataは```2023/03/12```
monは```2023/03```
といった形で取得できます。

```js
  const spreadSheet = SpreadsheetApp.openById(SSID);
  let sheet = spreadSheet.getSheetByName(sheetName)
```
```openById```でidからスプレッドシートの情報を取得しています。
```getSheetByName```で、シート名からシートの中身を取得しています。

上記の例の通り、monは```2023/03```とすると
シート名```2023/03```を取得します。
存在しない場合はnullを返します。

```js
  // シートが存在しない場合、作成
  if (sheet === null){
    sheet = spreadSheet.insertSheet().setName(mon);
    sheet.getRange('A1').setValue("日付")
    sheet.getRange('N2').setValue("月間順位")
    sheet.getRange('N3').setValue("順位の合計")
    Object.keys(SEIZA).forEach(seiza => {
      sheet.getRange(SEIZA[seiza]+'1').setValue(seiza)
      sheet.getRange(oToZ[i]+'1').setValue(seiza)
      i++
    })
  }
```
nullが返ってきた場合、存在しないため```insertSheet().setName(mon)```で作成します。
作成後、A,Nを除く各1行目のセルにおひつじ座～うお座を書き込みます。
```.getRange('A1')```はセルの情報を取得し、```.setValue("日付")```でセルに書き込みます。
この場合A1に日付を書き込みます。
```Object.keys(SEIZA).forEach(seiza =>```で、連想配列の初めの部分から順にループします。
ループごとに```seiza```には星座名がセットされていて、getRangeで列に対応する星座名をセルに書き込みます。

実行すると以下の図のように書き込まれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/477e0057-5fae-ef52-f7e1-c2684b02d6ba.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/7fb07e13-ea5d-88f4-88d8-39c894d4fabf.png)

B~Mが日ごとの順位を集計し、O~Zが合計と順位を示します。

```js
  // 最終更新日と今日を比較(時間分秒を取り除く)
  const lastDay = new Date(Utilities.formatDate(new Date(uranaiJson["date"]), "Asia/Tokyo", "yyyy/MM/dd"))
  const nowTime = new Date(Utilities.formatDate(new Date, "Asia/Tokyo", "yyyy/MM/dd"))
```
日付の大小を比較します。
ポイントとして```new Date```→```Utilities.formatDate```とすることで現在時刻を強制的に0時0分に変換しています。
こうすることで日付のみの大小の比較を可能としています。

```js
  // 一番下の要素の行数を取得
  let lastRaw = sheet.getLastRow()
  Object.keys(SEIZA).forEach(seiza =>  {
    sheet.getRange(oToZ[i]+'1').setValue(seiza)
    sheet.getRange(oToZ[i]+'3').setFormula('=SUM('+SEIZA[seiza]+'2:'+SEIZA[seiza]+lastRaw+')');
    sheet.getRange(oToZ[i]+'2').setFormula('=RANK('+oToZ[i]+'3,$O3:$Z3,1)');
    i++
  })
```
月間順位の集計を行う関数を埋め込んでいます。
```.getLastRow()```で書き込まれている一番下の行数を取得し、SUM関数の範囲を指定します。
```.setFormula```で関数を埋め込みます。
SUM関数で合計した値をRANK関数で順位付けします。

### uranai
月金の占い結果を取得します。
uranaiTextは前述したとおり、Discord WebHookの名残です。
```uranai```はmainで取得したjsonが入っています。
```js:コード.gs
function uranai(uranai) {
  // アドバイスとラッキーパーソン
  let advice = []
  let person = []
  // 星座(順位順)
  let zodiacSign = []
  let text = []
  let luckyPoint = []

  // 順位の重みづけ
  let rankNum = []
  
  // Discordに送信するテキスト
  let uranaiText = ""

  for (let i = 0; i < 12; i++){
    
    advice[i] = uranai["ranking"][i]["advice"]
    person[i] = uranai["ranking"][i]["person"]
    zodiacSign[i] = uranai["ranking"][i]["name"]
    text[i] = uranai["ranking"][i]["text"].replace('<br>','\n')
    luckyPoint[i] = uranai["ranking"][i]["point"]

    let sendText = text[i] + "\nラッキーポイント:" + luckyPoint[i]

    if (advice[i].length > 0 && i === 0) {
      sendText = sendText + '\nアドバイス:' + advice[i]
    } else if (advice[i].length > 0 && i === 11) {
      sendText = sendText + '\nおまじない:' + advice[i]
    }
    if (person[i].length > 0) {
      sendText = sendText + '\nラッキーパーソン:' + person[i]
    }
    uranaiText = uranaiText + uranai["ranking"][i]["rank"] + "位\n" + "***" + zodiacSign[i] + "***" + "\n" + "```" + sendText + "\n" + "```"
    
    // スプレッドシートへの書き込みのため列の代入
    // おうし座の場合B列へ書き込み
    rankNum[i] = SEIZA[zodiacSign[i]]
  }

  // スプレッドシートに順位の書き込み
  outPut(rankNum)

}
```

あまり説明することもありませんが一応
```js
    if (advice[i].length > 0 && i === 0) {
      sendText = sendText + '\nアドバイス:' + advice[i]
    } else if (advice[i].length > 0 && i === 11) {
      sendText = sendText + '\nおまじない:' + advice[i]
    }
    if (person[i].length > 0) {
      sendText = sendText + '\nラッキーパーソン:' + person[i]
    }
```
```(advice[i].length > 0 && i === 0)```は1位のアドバイスがあるか判断しています。
逆に最下位の場合、おまじないとなります。
また1位にはもう一つ、ラッキーパーソンが設けられています。

```js
    // スプレッドシートへの書き込みのため列の代入
    // おうし座の場合B列へ書き込み
    rankNum[i] = SEIZA[zodiacSign[i]]
```
すごく複雑そうですが、一つ一つ解説します。
```zodiacSign[i]```には星座名が格納されています。
```i```は順位を示しています。
```SEIZA```は連想配列で、星座名ごとに対応する列名が格納されています。
したがって```SEIZA[zodiacSign[i]]```はB~Mが格納されていて、```rankNum[i]```はどの星座がどの列にあるかを示しています。
例:
```rankNum[0]='B'```の場合、1位はおひつじ座。
```rankNum[11]='I'```の場合、12位はさそり座。

### saturdayUranai
コンテンツサイトから占いの結果を取得します。
```js:コード.gs
function saturdayUranai(){
  // めざましコンテンツサイトからスクレイピング
  const res = UrlFetchApp.fetch(
    SATURDAYURL,
    {
      "contentType":"text/html;",
      "method":"get"
    }
  ).getContentText("Shift_JIS");

  // 占い結果の抽出
  let ura = Parser.data(res).from('<div class="rankArea">').to('</div>').iterate()
  
  // 更新日を取得
  let updateTime = Parser.data(res).from('<h1 class="pageTitle">').to('</h1>').iterate()

  // 最終更新日をyyyy/MM/ddで取得
  let lastDay = ""
  updateTime.map(update => {
    // spanから「月」「日」「のランキング」を抽出
    let span = Parser.data(update).from('<span>').to('</span>').iterate()

    // spanタグの削除
    update = update.replaceAll('<span>','')
    update = update.replaceAll('</span>','')
    // 「月」「日」「のランキング」を/に変換
    span.map(u => {
      update = update.replace(u,'/')
    })
    // 最終更新日をyyyy/MM/ddで取得
    lastDay = new Date(new Date().getFullYear() + '/' + update)
  })

  // 最終更新日と今日を比較(時間分秒を取り除く)
  const lastUpdateDay = Utilities.formatDate(lastDay, "Asia/Tokyo", "yyyy/MM/dd")
  const nowTime = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd")

  // 日付が合わない場合、終了
  if (lastUpdateDay !== nowTime){
    return
  }

  let zodiac = []

  // 順位の重みづけ
  let rankNum = []
  
  // Discordに送信するテキスト
  let uranaiText = ""

  let i = 1
  ura.map(uranai => {
    let span = Parser.data(uranai).from('<span>').to('</span>').iterate()
    zodiac.push(span[0])
    uranaiText = uranaiText + i + "位\n" + "***" + span[0] +"***\n"

    // スプレッドシートへの書き込みのため列の代入
    // おうし座の場合B列へ書き込み
    rankNum[i-1] = SEIZA[zodiac[i-1]]
    i++
  })

  // スプレッドシートに順位の書き込み
  outPut(rankNum)

}
```

ポイント
```js
  // めざましコンテンツサイトからスクレイピング
  const res = UrlFetchApp.fetch(
    SATURDAYURL,
    {
      "contentType":"text/html;",
      "method":"get"
    }
  ).getContentText("Shift_JIS");
```
コンテンツサイトの要素を取得します。
Shift_JISに指定しないと文字化けするので気を付けましょう。

```js
  // 占い結果の抽出
  let ura = Parser.data(res).from('<div class="rankArea">').to('</div>').iterate()
```
```Parser```でサイトのコンテンツを抜き取ります。
```.from('<div class="rankArea">').to('</div>')```でランキング1位から12位までの要素を配列で抜き取っています。

```js
  // 更新日を取得
  let updateTime = Parser.data(res).from('<h1 class="pageTitle">').to('</h1>').iterate()

  // 最終更新日をyyyy/MM/ddで取得
  let lastDay = ""
  updateTime.map(update => {
    // spanから「月」「日」「のランキング」を抽出
    let span = Parser.data(update).from('<span>').to('</span>').iterate()

    // spanタグの削除
    update = update.replaceAll('<span>','')
    update = update.replaceAll('</span>','')
    // 「月」「日」「のランキング」を/に変換
    span.map(u => {
      update = update.replace(u,'/')
    })
    // 最終更新日をyyyy/MM/ddで取得
    lastDay = new Date(new Date().getFullYear() + '/' + update)
  })
```

```updateTime```には日付が入ります。
日付はページのタイトルにもなっているので```<h1 class="pageTitle">```から抜き取ります。
```html
<h1 class="pageTitle">
    3
    <span>月</span>
    13
    <span>日(月)</span>
    <span>のランキング</span>
</h1>
```
updateTimeの中身はこうなっています。
```js
updateTime = [`
    3
    <span>月</span>
    13
    <span>日(月)</span>
    <span>のランキング</span>
`]
```
この文字列から```<span>```タグを抜き取り、「月」「日」「のランキング」を/に変換します。
こうすることで月日を```MM/dd```の形式に変換でき、最終更新日を```yyyy/MM/dd```として取得できます。

```js
  // 最終更新日と今日を比較(時間分秒を取り除く)
  const lastUpdateDay = Utilities.formatDate(lastDay, "Asia/Tokyo", "yyyy/MM/dd")
  const nowTime = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd")

  // 日付が合わない場合、終了
  if (lastUpdateDay !== nowTime){
    return
  }
```
最終更新日と現在の日付を比較します。
合わなかった場合、更新がなかったとして終了します。

```js
  ura.map(uranai => {
    let span = Parser.data(uranai).from('<span>').to('</span>').iterate()
    zodiac.push(span[0])
    uranaiText = uranaiText + i + "位\n" + "***" + span[0] +"***\n"

    // スプレッドシートへの書き込みのため列の代入
    // おうし座の場合B列へ書き込み
    rankNum[i-1] = SEIZA[zodiac[i-1]]
    i++
  })
```
1位から12位の星座を1位から順に配列に追加します。
星座名が```<span>```で囲われているため、```Parser```で抜き取ります。

```runkNum```の流れは```uranai```と同様なので省きます。

### getSheet
スプレッドシートの情報を取得する関数です。
引数の```sheetName```のシートを探します。
```js:コード.gs
// スプレッドシートを取得
function getSheet(sheetName){
  const spreadSheet = SpreadsheetApp.openById(SSID);  
  const sheet = spreadSheet.getSheetByName(sheetName)
  return sheet
}
```

```js
  const spreadSheet = SpreadsheetApp.openById(SSID);
  const sheet = spreadSheet.getSheetByName(sheetName)
```
```openById```でidからスプレッドシートの情報を取得しています。
```getSheetByName```で、シート名からシートの中身を取得しています。

### getLastTime
順位の最終更新日を取得します。
引数の```sheet```は```getSheetByName```で取得したスプレッドシートを示します。
```js:コード.gs
// スプレッドシートのA行目の最後(書き込み時刻)を取得
function getLastTime(sheet){
  // A列の一番下の行数を取得
  let lastRaw = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
  // A列に1行しか値がない場合、大きな値になるので1にする
  if (lastRaw >= 30) {
    lastRaw = 1
  }
  // スプレッドシートに書き込んだ日付を取得
  let lastTime = sheet.getRange(lastRaw,1).getValue()

  // 現在の日付を取得
  const startDate = new Date();

  // 月初めの場合
  if (typeof(sheet.getRange(2,1).getValue()) === "string"){
    // 先月を取得
    const lastMonth = new Date(startDate.getFullYear() , startDate.getMonth()-1);
    const sheetName = Utilities.formatDate(lastMonth, "Asia/Tokyo", "yyyy/MM")
    lastRaw = getSheet(sheetName).getLastRow()
    // スプレッドシートに書き込んだ日付を取得
    lastTime = sheet.getRange(lastRaw,1).getValue()
  }
  lastTime = Utilities.formatDate(lastTime, "Asia/Tokyo", "yyyy/MM/dd");

  return lastTime
}
```
ポイント
```js
  // A列の一番下の行数を取得
  let lastRaw = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
  // A列に1行しか値がない場合、大きな値になるので1にする
  if (lastRaw >= 30) {
    lastRaw = 1
  }
  // スプレッドシートに書き込んだ日付を取得
  let lastTime = sheet.getRange(lastRaw,1).getValue()
```
```sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();```でA列の一番下の行数を取得しています。
```getRange(lastRaw,1).getValue()```で取得することで以下のようなセルを取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/b06bd133-d13c-5ab9-32bf-1ca9e0b22a34.png)

この場合3/13が最終更新日となります。

```js
  // 月初めの場合
  if (typeof(sheet.getRange(2,1).getValue()) === "string"){
    // 先月を取得
    const lastMonth = new Date(startDate.getFullYear() , startDate.getMonth()-1);
    const sheetName = Utilities.formatDate(lastMonth, "Asia/Tokyo", "yyyy/MM")
    lastRaw = getSheet(sheetName).getLastRow()
    // スプレッドシートに書き込んだ日付を取得
    lastTime = sheet.getRange(lastRaw,1).getValue()
  }
  lastTime = Utilities.formatDate(lastTime, "Asia/Tokyo", "yyyy/MM/dd");

  return lastTime
```
月初めの場合、先月のシートを取得します。
年をまたいでも問題なく機能します。
(23年1月の前月を22年12月として認識する。)

また、シートから更新日を取得する際、```Utilities.formatDate```をしていますが、どうやら書き込む際**時刻のデータ**として扱われているらしくこの手順を踏む必要があります。
そのため型チェックの```typeof```では```object```となります。A列2行目が```string```の場合書き込みがないため、月初めと判断します。

### outPut
星座の順位をスプレッドシートに書き込みます。
引数の```result```は順位の順に星座の列名が格納されている配列になります。
```js:コード.gs
function outPut(result){
  // yyyy/MM/dd の形式で現在の日付を取得
  const dayNow = new Date()
  const data = Utilities.formatDate(dayNow, "Asia/Tokyo", "yyyy/MM/dd");

  const mon = Utilities.formatDate(dayNow, "Asia/Tokyo", "yyyy/MM");

  const sheet = getSheet(mon)

  // 一番下の要素の行数を取得
  let lastTime = getLastTime(sheet)

  // 本日分がもう書き込まれていた場合、または日曜日の場合終了
  if (dayNow.getDay() === 0 || lastTime === data){
    return
  }

  // A列の一番下の行数を取得
  let lastRaw = sheet.getRange(1, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
  // A列に1行しか値がない場合、大きな値になるので1にする
  if (lastRaw >= 30) {
    lastRaw = 1
  }

  // 一番下の次の行を参照
  lastRaw = lastRaw + 1
  
  // A列に日付を書き込む
  sheet.getRange('A' + lastRaw).setValue(data)
  // それぞれの星座の順位を書き込む
  for (let i = 1; i < 13; i++) {
    let j = i - 1
    sheet.getRange(result[j] + lastRaw).setValue(i)
  }
}
```
あまり解説することもないですが一応ポイント
```js
  const sheet = getSheet(mon)

  // 一番下の要素の行数を取得
  let lastTime = getLastTime(sheet)
```
上述の```getSheet```関数を利用して、シートの情報を取得します。
```getLastTime```も同様にシートの最終更新日を取得しています。

```js
  // A列に日付を書き込む
  sheet.getRange('A' + lastRaw).setValue(data)
  // それぞれの星座の順位を書き込む
  for (let i = 1; i < 13; i++) {
    let j = i - 1
    sheet.getRange(result[j] + lastRaw).setValue(i)
  }
```
A列に本日の日付を書き込みます。
その後、```result```に格納されている列名を参照して順位を書き込んでいきます。

はい！以上がメインのプログラムとなります。

# トリガーの設定
毎日集計するため、一日おきに実行する必要があります。
2つ方法があります。

- 時間指定で設定する。
- プログラム側から設定する。

## 時間指定で設定
左側の時計のアイコンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/d90f0ee8-b732-be0c-33e8-52c2d386a64d.png)
右下のトリガーの追加をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/afa7d67d-937e-15fd-2e33-b8b762b6cda7.png)
以下のような画面になるので、画面と同じ設定をしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/b021c212-a73a-98f4-a002-60c3305b603f.png)

これで毎朝9~10時の間に自動更新されます。

## プログラム側から設定
上記の場合、8時半の更新からすぐに反映されない欠点があります。
もっと早く集計したい場合、以下のコードを追記します。
```js
//0時にLINE側のプッシュ状況を知らせるためのトリガー設定
function setTrigger(){
  
  delTrigger();
  const time = new Date();
  time.setHours(8);
  time.setMinutes(31);
  ScriptApp.newTrigger('main').timeBased().at(time).create();

}
//トリガーの削除
function delTrigger() {

  const triggers = ScriptApp.getProjectTriggers();
  for(const trigger of triggers){
    if(trigger.getHandlerFunction() == "main"){
      ScriptApp.deleteTrigger(trigger);
    }
  }
}
```
その後、```setTrigger```のトリガーを以下のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2853914/ea6cc44a-fb82-876f-3a3e-8c91e61b501f.png)
時刻に関してはいつでも構いません。
これにより毎朝8時31分にプログラムが実行されます。

# 完成
お疲れ様でした。これにて完成です！
これでちいかわ占いの集計が非常に楽になります。

いやーこれで普段の生活がより良いものになりますね！！！

**これで君もちいかわ占いの順位を争えるぞ！！！！！！**



[^1]: プログラムからWebページの情報を取得すること。
厳密にはWebスクレイピングといいます。
[^2]: Web上でデータ交換を行うファイル形式。
[^3]: めざましテレビコンテンツサイトより。