あすけんのデータを集計しグラフ化する

# 概要

あすけんに入力した各種栄養素の情報を集計し、グラフ化しました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47699/3f57b68f-a824-4dcf-b6a4-441a89cde725.png)

:::note info
私はあすけんプレミアムサービスを使用しているため、基本サービス(無料プラン)だと使えない可能性があります。
また、AndroidユーザーなのでAndroidでしかこの手法は使えません。
:::

# 動機

私は普段あすけんで食事管理をしているのですがPFCバランスの推移がどうだったのかをグラフで見たいと思いました。
だがしかし、あすけんの公式機能ではその機能が提供されていない・・・。(接種カロリーの推移のグラフはあります)
そこであすけんで入力されたPFCの値をGoogleスプレッドシートで集計し、自分でグラフ化しようと思い立ちました。
だがしかし、あすけんの公式機能ではデータのエクスポート機能が提供されていない・・・。(なんで・・・)
検索してみるとiPhoneの「ヘルスケア」アプリになら送信されているという情報は見つかるものの、Androidユーザーなのでかなわず・・・。
途方に暮れていましたがどうもGoogleFitにカロリーデータが届いているっぽい？ということに気付きそこからデータを取得するようにしてみました。

# やりかたの候補

やりかたはいくつかあると思います。

1. 手入力
    - 最終手段に・・・。結局不要でした。
1. iPhoneの「ヘルスケア」アプリを使用する
    - 調べたら出てくる情報ですが、iPhoneを持っていないので諦め。
1. Androidの「ヘルスコネクト」アプリに送信されたデータを使用する
    - 色々調べたところ、GoogleFitに「栄養」という項目があり、各種栄養素が送られていることがわかりましたのでこれを使います。
        - GoogleFit - 設定 - ヘルスコネクト - データとアクセス権限の管理 - データとアクセス - 栄養 と辿ると各種栄養素についてのデータがあることがわかります。
          ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47699/83684650-eae5-4e72-a5e8-f63f1a6e91d0.png)
    - 軽く調べたところこのデータのグラフ化アプリもなさそうに見える？(ありそうなんだけどなぁ)

# やりかた

## ヘルスコネクトからデータを取り出す

前述のデータがあることから、GoogleFitまたはヘルスコネクトからデータがエクスポートできるのでは？と調査してみました。

まずはGoogleFit。しかしAPIはObsorete・・・。
https://developers.google.com/fit/scenarios/read-daily-nutrition?hl=ja
ヘルスコネクトを使えとのことなのでそちらへ。

詳細は省きますが、端末側のAPIらしいので専用のアプリを入れないとだめっぽい？
アプリを作ってみてもいいのですがとりあえずそれ以外の方法を模索することに。

更にヘルスコネクトについて調べてみると定期的にバックアップしてくれる機能があるらしい。
https://support.google.com/android/answer/15323271?hl=ja-JP

zipファイルがアップロードされることを確認したのでバックアップされるのを待ちます。
その後アップロードされたzipファイルを解凍したところ `health_connect_export.db` というファイルが取り出せました。

~~雑に~~VSCodeに投げてみたところSQLiteファイルで `nutrition_record_table` というテーブルに情報が入っていそうということが判明。
↓のクエリで各種値が取り出せそうということもわかったのでこれをコピペすればデータの取り出しはできました。
```sql
SELECT DATE('1970-01-01', '+' || local_date || ' days') AS date, protein, total_fat as fat, total_carbohydrate as carbohydrate FROM nutrition_record_table;
```

## データ取り出しの自動化

解凍して、クエリを投げて、という手順を毎回やるのは結構面倒なので自動化してみます。手作業でいいよという方はスキップしてください。
私の場合C#(LinqPad)を使うのが一番手軽なのでそれを使用していますが、やってる処理は単純ですし他の手段でもできそうな気はしますね。

プログラム全文は[こちら](https://gist.github.com/the9ball/8154f245428a30e27765cc1b8c1b3cf9)

:::note info
前述のバックアップファイルはGoogleドライブに出力されるため、ドライブがマウントされている前提で進めています。
ダウンロード等が必要な場合は別途じっこうしてください
:::

### テンポラリディレクトリの作成

zipの展開先ディレクトリを作成し、finallyで削除するようにしておきます。

```cs
var destinationPath = Directory.CreateTempSubdirectory().FullName;
try
{
    // TODO: Work
}
finally
{
    try
    {
        // Delete Temporary Directory
        if (Directory.Exists(destinationPath))
        {
            Directory.Delete(destinationPath, true);
        }
    }
    catch (Exception e)
    {
        e.Dump();
    }
}
```

### 展開

[ZipFile.ExtractToDirectory](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.compression.zipfile.extracttodirectory) を呼ぶだけです。

```cs
ZipFile.ExtractToDirectory(sourcePath, destinationPath);
```

### データの読み込み

SQLiteを扱うには[System.Data.SQLite.Core](https://www.nuget.org/packages/system.data.sqlite.core/)パッケージが必要です。
特に難しいことはないですね。`IAsyncEnumerable`を使っているのは単に趣味です。全部一度に読み込んでも問題ないと思います。

```cs
record class Entry(DateTime date, double protein, double fat, double carbohydrate);

async IAsyncEnumerable<Entry> ReadDataAsync(string destinationPath, [EnumeratorCancellation] CancellationToken ct)
{
    const string dataName = "health_connect_export.db";
    const string query = "SELECT DATE('1970-01-01', '+' || local_date || ' days') AS date, protein, total_fat as fat, total_carbohydrate as carbohydrate FROM nutrition_record_table;";

    string connectionString = $"Data Source={Path.Combine(destinationPath, dataName)};Pooling=false;";

    await using var connection = new SQLiteConnection(connectionString);
    await connection.OpenAsync(ct);
    try
    {
        await using var command = new SQLiteCommand(query, connection);
        var reader = await command.ExecuteReaderAsync(ct);

        while (await reader.ReadAsync(ct))
        {
            yield return toEntry(reader);
        }
    }
    finally
    {
        await connection.CloseAsync();
        SQLiteConnection.ClearAllPools();
    }

    static Entry toEntry(System.Data.Common.DbDataReader reader)
        => new(
            date: DateTime.Parse(reader["date"].ToString()),
            protein: reader["protein"] is var p && p is DBNull ? 0.0 : Convert.ToDouble(p),
            fat: reader["fat"] is var f && f is DBNull ? 0.0 : Convert.ToDouble(f),
            carbohydrate: reader["carbohydrate"] is var c && c is DBNull ? 0.0 : Convert.ToDouble(c)
        );
}
```

ただ、SQLiteがなかなかファイルハンドルを放してくれないようでテンポラリディレクトリの削除時に `IOException` が発生してしまうようです。
以下の点を対応すると回避できたのですが正しいかどうかはわからないので自己責任でお願いします。

1. `connectionString` に `Pooling=false` を追加
2. finallyで `connection.CloseAsync` と `SQLiteConnection.ClearAllPools` を呼ぶ
3. テンポラリディレクトリの削除前に `Task.Delay` と `GC.Collect` 、 `GC.WaitForPendingFinalizers` を置いておく
    - 手元で試す感じだと `GC.Collect` だけで良さそうではあるのですが詳細不明なので全部置いておくことにしました

### ファイルの書き出し

特にひねりもなく書き出します。

```cs
async Task WriteAsync(IAsyncEnumerable<Entry> data, CancellationToken ct)
{
    using var s = new StreamWriter(DestinationFilePath);
    await foreach (var d in data)
    {
        await s.WriteLineAsync(d.ToString());
    }
}
```

フォーマットはどこに書いてもいいのですが読みやすさ重視で `Entry` の `ToString` に書いてあります。
タブ区切りにすることでペーストしやすいようにしてあります。

```cs
public override string ToString() => $"{date:yyyy/MM/dd}\t{protein}\t{fat}\t{carbohydrate}";
```

### 出力内容のチェック

出力内容に問題ないかチェックします。

## データの入力

データは入手できたのでスプレッドシートへ入力し、グラフ化してみます。

用意したのは以下のカラム。青い部分にコピペして、それより右は自動計算させています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47699/51f8dd62-9084-4452-b20b-4c71078a386c.png)

### データの計算

具体的な計算式はこんな感じです。

グラム換算
入力しなかった日は前日と翌日の中間値を入力してグラフが0に凹まないよう対応しました。
```
=IF($A2<>"", IF(B2>0, B2, (B1+B3)/2), )
```

カロリー換算
別のシートに名前付きで1gあたりのカロリーを定数として入力しておき、計算します。
Pは4kcal/g、Fは9kcal/g、Cは4kcal/g(3.7kcalが正しい説もあるらしいのでお好みで)で計算しています。
```
=IF($A2<>"", E2*Pcpg, )
```

PFCバランス計算
カロリーの合算をして、割るだけです。
```
=IF($A2<>"", H2/$K2, )
```

### グラフの作成

グラフの作り方は別途調べてください。(手抜き)
折れ線グラフや積み上げグラフなどお好みのもので。

### データの入力

スプレッドシートへ前述のC#から書き込むのは大変すぎるので流石にコピペします。
SQLiteからのコピペの場合は`,`区切りになっていることがあるのでご注意ください。(単純置換すればいいはずです)

# 完成

グラフ化することができました！
よいダイエットライフを！