水泳部マネージャーがFINAポイントの計算用APIを公開した話


# はじめに

この記事は、Qiita Advent Calendar 7 日目の記事です。
以下の記事より、すべての記事をご覧になれます。

https://qiita.com/0_terarin_0/items/0f367210e2465f7772c9

# 筆者について

- 現在高校２年生
- 水泳部マネージャー
- 生徒会長
- とある団体の代表

# 本編

この API は MIT でオープンソース化しています。気になる方はぜひ下のリポジトリまで！

https://github.com/0-terarin-0/api-getFinaPoint

## API 概要

FINA ポイントを計算するための API です。ここで、多くの方は「FINA ポイントとはなんぞや？」というふうに思います。簡単に説明すると「どんだけ泳げるか」というのを 1000 をマックスに数値化したものです。以下に World Aquatics（旧 FINA）の分と翻訳を載せておきます。

> （原文）
> The World Aquatics Points Table allows comparisons of results among different events. The World Aquatics Point Scoring assigns point values to swimming performances, more points for world class performances typically 1000 or more and fewer points for slower performances.
>
> （翻訳）
> 世界水泳連盟表ポイントは違う種目の記録動詞の比較をすることができます。世界水泳連盟ポイントは泳力にポイントをつけます。世界記録に近い記録には 1000 ポイントが、より遅い記録にはより低いポイントが与えられます。
>
> 引用元：[https://www.worldaquatics.com/swimming/points](https://www.worldaquatics.com/swimming/points)

ちなみに、計算式は以下の通りです。

```math
\displaylines{
  自分のリザルトをM、世界記録をW、FINAポイントをPとすると\\
  P=1000 \cdot ( \frac{W}{M})^{3}\\
}
```

例えば、現役時代の僕が200mの個人メドレーを3'14"19でしたので、このFINAポイントを計算すると次の通りになります。

```math
\displaylines{
  1000 \cdot (\frac{114.0}{224.19})^{3} \fallingdotseq 131.48
}
```

よって、僕の200m個人メドレーのFINAポイントは131ポイントです。たとえば他の種目でこのポイントより高い数値が弾き出されたら僕より泳力があることを示し、逆に低い数値が出たら泳力がないことを示します。要するに、いろいろな種目の泳力を一つの指標で評価できるということですね。

さて、なぜ僕がこの超マニアックな計算をする API を公開しようと思ったのかと言いますと、他のことにも使われる可能性が今後ともあるからです。本来は以前まで書いていた記事のバックエンドサーバーに機能を組み込む予定だったのですが、この FINA ポイントの部分だけはオープンソースにして公開しました。

## API実装編

公開はGoogle CloudのCloud Functionで公開しました。ソースコードは以下の通りです。

```javascript:main.js
const functions = require('@google-cloud/functions-framework');
const { google } = require('googleapis');
const dotenv = require('dotenv');
require('dotenv').config()

functions.http('getFinaPoint', async (req, res) => {
  const queryStyle = req.query.style;
  const queryDistance = req.query.distance;
  const queryGender = req.query.gender;
  const queryTime = req.query.time;
  let response;
  let colGender;
  let nameStyle;
  let rowDistance
  try {
    switch (queryStyle) {
      case "01":
        nameStyle = "Freestyle";
        break;
      case "02":
        nameStyle = "Breaststroke";
        break;
      case "03":
        nameStyle = "Backstroke";
        break;
      case "04":
        nameStyle = "Butterfly";
        break;
      case "05":
        nameStyle = "IndividualMedley";
        break;
      case "06":
        nameStyle = "FreestyleRelay";
        break;
      case "07":
        nameStyle = "MedleyRelay";
        break;
    };

    switch (queryGender) {
      case "male":
        colGender = "B";
        break;
      case "female":
        colGender = "C";
        break;
      case "mixed":
        colGender = "D";
        break;
    };

    if (queryStyle == "06" || queryStyle == "07") {
      switch (queryDistance) {
        case "400":
          rowDistance = "1";
          break;
        case "800":
          rowDistance = "2";
          break;
      };
    } else if (queryStyle == "05") {
      switch (queryDistance) {
        case "200":
          rowDistance = "1";
          break;
        case "400":
          rowDistance = "2"
          break;
      }
    } else {
      switch (queryDistance) {
        case "50":
          rowDistance = "1";
          break;
        case "100":
          rowDistance = "2";
          break;
        case "200":
          rowDistance = "3";
          break;
        case "400":
          rowDistance = "4";
          break;
        case "800":
          rowDistance = "5";
          break;
        case "1500":
          rowDistance = "6";
          break;
      };
    };
    const targetCell = nameStyle + "!" + colGender + rowDistance;
    const auth = new google.auth.GoogleAuth({
      keyFile: process.env.CRED_PATH,
      scopes: ['https://www.googleapis.com/auth/spreadsheets']
    })
    //const sheetsAuth = await google.auth.getClient()
    const sheets = google.sheets({ version: 'v4', auth: auth });
    data = await sheets.spreadsheets.values.get({
      spreadsheetId: "10K_1kbfMQfHwI3J0wapjslGojIqw56aK-3i12DPlruU",
      range: targetCell,
    });
    const finaPoint_B = ((Number(data.data.values[0][0]) / Number(queryTime)) ** 3)
    const finaPoint = finaPoint_B * 1000
    response = Math.round(finaPoint);
  } catch (error) {
    response = "Error"
  };
  res.send({response:response})
});
```

例えば、URLは以下のようになります。

```
＊男子50m自由形で30"09の記録のFINAポイントを求める場合（これも現役自体の記録）
https://api.terarin-iw.net/getFinaPoint?distance=50&style=01&gender=male&time=30.09
```

それぞれパラメータを説明します。（GitHubのREADMEコピペ）

| パターン | 説明 |
| ---- | ---- |
| ```style``` | FINAポイントを計算したい記録の泳法を下にある表のIDの通りに指定してください。 |
| ```distance``` | FINAポイントを計算したい記録の距離を指定してください。 |
| ```gender``` | FINAポイントを計算したい記録の性別を指定してください。 <br> 選択肢:```male```\/```female```\/```mixed```|
| ```time``` | FINAポイントを計算したい記録のタイムを入力してください。 <br> 例: ```25.93```, ```67.93``` |

| 泳法 | ID |
| ---- | ---- |
| 自由形 | ```01``` |
| 平泳ぎ | ```02``` |
| 背泳ぎ | ```03``` |
| バタフライ | ```04``` |
| 個人メドレー | ```05``` |
| フリーリレー | ```06``` |
| メドレーリレー | ```07``` |

技術的な話は、普通にパラメータを処理してFINAポイントを調べたい記録と種目の世界記録を、Googleスプレッドシートから取得するというものです。おそらく本来ならCloud Funcitonのサービスアカウントをごにょごにょしたらクレディンシャルがいらないはずなのですが、できなかったので、環境変数にシークレットを指定して認証情報を埋めこんでいます。スプレッドシートは以下のリンクより見てみてください。

https://docs.google.com/spreadsheets/d/10K_1kbfMQfHwI3J0wapjslGojIqw56aK-3i12DPlruU/edit?usp=sharing

特に注意することもないのですが、強いて言うならNodeのバージョンに気をつけてください。それで結構苦労しましたので…。

# 最後に

今回は軽くAPIの公開について触れてみました。今後の記事はもう少しWeb系が続きますが、もうそろそろしたら検証系が出てくるので楽しみにしてください。それではは、引き続き今後のアドカレ 23 の記事をお楽しみにしてください。

https://qiita.com/0_terarin_0/items/0f367210e2465f7772c9