Siriに話すだけで日々の支出管理がしたい(GAS)

こんにちは、前回の投稿から長らく期間が空いてしまいました。
今回はこれまでのWebアプリケーション周りの記事とは打って変わって
「ただただ自分の生活の一部を楽なものにしたい」というモチベーションのもと、
**Siriに話すだけで家計簿をつけられる**サービスの開発に挑戦しました。

# はじめに

### モチベーション

私は、普段スマホアプリを使って支出管理をしていますが、以下のような課題がありました。

- いちいち家計簿アプリを開くのがめんどくさい
- レシートを撮影するのがめんどくさい
- カードと紐づけてもこまめに更新するには有料会員になる必要がある（お金払いたくない）

要は、**家計簿をつけるのはめんどくさい**ということです。
このような背景を受けて、もっと楽に家計簿をつけ、後から振り返れる方法はないかと考えました。
後述の参考文献を参照しながら、個人的に便利だなと思う「**Siriに話すだけで家計簿をつけられる**」機能をつけていきます。

## 仕様

下記のような機能を作っていきます。

- 記入内容は「日付」、「金額」、「項目名」とする
- iPhoneのショートカット機能を用いて入力する項目を指示
- 月次で支出を管理できるようにする
- 事前に設定した制限額を超えた場合や入力があった場合にメールを送信して通知する(オプション)
<br>
- 全体図
    
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1071957/ed61327e-fb73-88bf-a8f7-95d7e76f4648.png)

    

# 実装

## 事前準備

- iPhone/ iPad / Mac のいずれかを購入する(Apple Watchがあると便利かも)
- Googleアカウントを用意 (Google Spread Sheetの作成)
<br>
- 使用するツール
    - iPhoneのショートカット
    - GSS (Google Spread Sheet)
    - GAS (Google App Script)

## 機能実装

### 1. Template Sheet の作成

まずは、各月の支出を記録していくシートの雛形を作成します。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1071957/417b7470-3a7d-dcfe-3522-170b6fb0b761.png">

次に、仕様のうち、以下の項目を満たすための機能をGASを使って実装します。

- 月次で支出を管理できるようにする

GSS編集画面上部の
「拡張機能 > App Script」
から、コードを記述していきましょう。このコードを実行することで、月次の支出管理を行うシートの作成を自動化します。（実行タイミングでその月のシートをTemplateシートをコピーして作成）

```
// utils.gs

/**
 * 現在の年月を取得する関数
 */
function getCurrentYearMonth() {
  var today = new Date();
  var year = today.getFullYear();
  var month = ("0" + (today.getMonth() + 1)).slice(-2); // 2桁になるようにゼロ埋め
  return { year: year, month: month };
}

/**
 * 指定された年月のシートを取得する関数
 * シートが存在しない場合はTemplateシートをコピーして作成する
 * @param {string} year 年
 * @param {string} month 月
 * @return {Sheet} シートオブジェクト
 */
function getOrCreateSheetByYearMonth(year, month) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheetName = "Expenses_" + year + month;
  var sheet = ss.getSheetByName(sheetName);
  if (!sheet) {
    // Templateシートをコピーして新しいシートを作成
    var templateSheet = ss.getSheetByName("Template");
    sheet = templateSheet.copyTo(ss);
    sheet.setName(sheetName);
  }
  return sheet;
}

/**
 *  動作確認用の関数です
 *　必要がなければ削除してください
 */
function testCreateSheet(){
  var yearMonth = getCurrentYearMonth();
  var sheet = getOrCreateSheetByYearMonth(yearMonth.year, yearMonth.month);
}

```

実行結果：　「Expenses_YYYYMM」という名前のシートが作成される

### 2. GASが外部からの入力を受けとれるようにする

[こちら](https://note.com/tech_lifemap/n/neec36d0fac53)のサイトを参考にGASが外部からの入力(今回はSiriの音声入力)を受け取れるように、サービスをウェブアプリケーションをしてデプロイします。

デプロイが完了すると、作成したウェブアプリのURLが表示されます。
後で使用するのでコピーしておいてください。

:::note warn
誰でもアクセスできてしまうので、絶対に誰にもウェブアプリURLは共有しないでください。
:::

### 3. ショートカットの作成

- 記入内容は「日付」、「金額」、「項目名」とする
- iPhoneのショートカット機能を用いて入力する項目を指示

上記の仕様に準拠すると、ショートカットは下記のようになります。

(補足)結果的には、「XXXX円(料金)、YYYYY(項目名)」とすることで、スプレッドシートに新しい列(記録内容)が追加されていくことになります。日付のデータについてはGAS側で「POSTリクエストを受信した時刻」として管理、記載します。

下記の画像のようなショートカットを作成していく際に、[こちら](https://note.com/ibushi_maru/n/nf4f8abd32953)を参考にさせていただきました。

※[参考サイト](https://note.com/ibushi_maru/n/nf4f8abd32953)より引用（こちらを少し編集しました。）

https://www.icloud.com/shortcuts/42df07227e074c00a90188794023147e

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1071957/4eef2eff-b90e-cf28-b066-3ba545158493.png)

### 4. 音声入力の内容をGSSに反映する

いよいよ、ショーカット実行時の音声入力をGSSに反映していきましょう。
といってもリクエストの送信や受信の機構は前工程までですでに実現しているので、
残るは、GASの基本的な関数(セルへの入力など)を駆使して、値を適切な箇所に入力するだけです。

コードの実装は以下になります。

```
// main.gs

/**
 * POSTリクエストを処理するメインの関数
 * @param {Object} e POSTリクエストのパラメータ
 */
function doPost(e) {
  var data = JSON.parse(e.postData.contents);
  var fee = data.fee;
  var label = data.label;
  var content = data.content;
  var date = new Date()
  var timestamp = date.toISOString();
  var formatedDate = Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy/MM/dd")

  var yearMonth = getCurrentYearMonth();
  var sheet = getOrCreateSheetByYearMonth(yearMonth.year, yearMonth.month);

  // 最後の行の次の行にデータを追加
  var lastRow = sheet.getLastRow() + 1;
  sheet.getRange(lastRow, 1).setValue(timestamp);
  sheet.getRange(lastRow, 2).setValue(formatedDate);
  sheet.getRange(lastRow, 3).setValue(fee);
  sheet.getRange(lastRow, 4).setValue(label);
  sheet.getRange(lastRow, 5).setValue(content);
}

```

## 動作確認

さてここまで来たらいよいよ動作確認です。
確認する項目としては、

- 今月(2024年5月実行)の名前でシート「Expenses_202405」が新規作成されるか
- Siriでショートカット名「家計簿」と話すことでショートカットが起動・滞りなく動作するか
- 音声入力(例：「750円、学食のカレー」)の内容がGSSに反映されるか

の各点になります。
なお、Apple Watchでの実行はショートカットの音声実行は現在使用できない事例が見られているようで、（私もそれに該当してしまったので）ショートカットのアプリから画面操作による実行 or その他のデバイスで音声実行という方法で本機能を使いたいと考えています。
参考：

https://discussionsjapan.apple.com/thread/254599929?sortBy=best

**検証準備**

- GASの最新アプリを(最後の更新の保存を行った後、再度)デプロイする
- 音声入力のための各種アクセス許可（マイクによる音声入力など）

**検証結果**

!![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1071957/5d7d7beb-87f6-bfe4-052f-4374b9b8b212.gif)

画面が小さくて見づらいかもしれませんが、問題なく機能していることが確認できます。

オプション的な機能として支出の時系列グラフや、ラベルごとの支出の円グラフを表示し、振り返り易くしてみました。（グラフの挿入により作成）

### オプション機能の実装

また、オプション機能として、configシート記載のアドレスに対して

- 新しい記録が追加されるたびに、その内容と今月の使用額、残りの使用可能金額を含むメールを送信する機能
- 残りの使用可能金額が負になった場合にも、その旨を通知するメールを送信する機能

を追加しました。詳細は下記です。

<details><summary>オプション機能のコード(メール送信)</summary>

```
// utils.gs

/**
 * 現在の年月を取得する関数
 */
function getCurrentYearMonth() {
  var today = new Date();
  var year = today.getFullYear();
  var month = ("0" + (today.getMonth() + 1)).slice(-2); // 2桁になるようにゼロ埋め
  return { year: year, month: month };
}

/**
 * 指定された年月のシートを取得する関数
 * シートが存在しない場合はTemplateシートをコピーして作成する
 * @param {string} year 年
 * @param {string} month 月
 * @return {Sheet} シートオブジェクト
 */
function getOrCreateSheetByYearMonth(year, month) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheetName = "Expenses_" + year + month;
  var sheet = ss.getSheetByName(sheetName);
  if (!sheet) {
    // Templateシートをコピーして新しいシートを作成
    var templateSheet = ss.getSheetByName("template");
    sheet = templateSheet.copyTo(ss);
    sheet.setName(sheetName);
  }
  return sheet;
}

function sendEmail(subject, body) {
  var address_main = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("config").getRange("A2").getValue();
  var address_sub = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("config").getRange("A3").getValue();
  MailApp.sendEmail(`${address_main}, ${address_sub}`, subject, body);
}

function testCreateSheet(){
  var yearMonth = getCurrentYearMonth();
  var sheet = getOrCreateSheetByYearMonth(yearMonth.year, yearMonth.month);
}

```

```
// main.gs

/**
 * POSTリクエストを処理するメインの関数
 * @param {Object} e POSTリクエストのパラメータ
 */
function doPost(e) {
  var data = JSON.parse(e.postData.contents);
  var fee = data.fee;
  var label = data.label;
  var content = data.content;

  var date = new Date()
  var timestamp = date.toISOString();
  var formatedDate = Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy/MM/dd")

  var yearMonth = getCurrentYearMonth();
  var sheet = getOrCreateSheetByYearMonth(yearMonth.year, yearMonth.month);

  // 最後の行の次の行にデータを追加
  var lastRow = sheet.getLastRow() + 1;
  sheet.getRange(lastRow, 1).setValue(timestamp);
  sheet.getRange(lastRow, 2).setValue(formatedDate);
  sheet.getRange(lastRow, 3).setValue(fee);
  sheet.getRange(lastRow, 4).setValue(label);
  sheet.getRange(lastRow, 5).setValue(content);

  // 使用額の合計と残りの使用可能額を計算
  var totalExpenses = sheet.getRange("B2").getValue();
  var remainingBudget = sheet.getRange("B3").getValue();

  // メールの送信
  var subject = "新しい記録が追加されました";
  var body = "新しい記録が追加されました。\\n\\n" +
             "追加されたデータ:\\n" +
             "金額: " + fee + "\\n" +
             "ラベル: " + label + "\\n" +
             "内容: " + content + "\\n\\n" +
             "---------------------" +
             "今月の使用額の合計: " + totalExpenses + "\\n" +
             "残りの使用可能額: " + remainingBudget;

  sendEmail(subject, body);

  // 残りの使用可能額が負の場合は通知メールを送信
  if (remainingBudget < 0) {
    var negativeBudgetSubject = "今月の使用可能金額を上回りました";
    var negativeBudgetBody = "今月の使用可能金額を上回りました。\\n" +
                              "使用額の合計: " + totalExpenses + "\\n" +
                              "残りの使用可能金額: " + remainingBudget;
    sendEmail(negativeBudgetSubject, negativeBudgetBody);
  }
}

```

</details>

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1071957/f4d4bbad-e2dc-2014-297d-214a125602f1.jpeg">

# おわりに

いかがだったでしょうか。今回はとにかく楽したいというシンプルな欲望を解消してくれるような家計簿記録サービスの開発に挑戦しました。
作ってみたら想像してたよりカスタマイズ性が高く、今後も好き勝手機能追加をしながら、私生活でも活用していこうと思っています。

# 参考文献

https://note.com/ibushi_maru/n/nf4f8abd32953

https://zenn.dev/imaizume/articles/402999b8462e63

https://note.com/tech_lifemap/n/neec36d0fac53