ASP.NETお試し会

本記事はMuroran Institute of Technology Advent Calendar 2019 12/7の記事です.

adventar.org

はじめに

唐突にASP.NETお試し会を開催しました. 参加者は3名(表示名:参加者1,参加者2,参加者3)です. 内容はASP.NETを用いたジャンケンAPIの試作です. 以下では,開催に至った経緯,ASP.NETお試し会でやったこと,試作したジャンケンAPIの動作例,3名の感想を書きます.

開催に至った経緯

参加者1は普段からUnityでゲーム開発をしています. さらに,参加者1はオンラインゲームの開発に挑戦しようとしています. 参加者1は,特にサーバサイドにおける通信を扱うプログラミングについて,参加者2から教えてもらうことにしました. 参加者2は,参加者1がC#に慣れていると考え,ASP.NETを利用してサーバプログラムを開発したら良いと考えました. しかし,参加者2はサーバプログラムの開発経験はKtorを利用した簡単なWebAPIの開発だけであり,ASP.NETについては全く知らないため教えることができません. 参加者2はKtorを用いないサーバプログラム開発にも興味を持っていたため,参加者1と一緒に調べながら簡単なサーバプログラムを試作し,理解を深めることにしました. そこに参加者3が合流して,ASP.NETお試し会が開催されることとなりました.

ASP.NETお試し会でやったこと

ASP.NETお試し会では以下の作業を行いました.

  1. VisualStudioの起動とプロジェクトの作成
  2. ジャンケンAPIの試作
    1. モデルの構成
    2. コントローラーの構成

VisualStudioの起動とプロジェクトの作成

VisualStudioの起動して,「Welcome Page」から「New Project...」,「ASP.NET Core Web API」,「Next」と進み,「Project Name」と「Solution Name」を埋め,「Create」を選択してプロジェクトを作成しました. 実行し,ブラウザからhttp://localhost: 24635/api/valuesにアクセスすると["value1","value2"]と表示されました. ポート番号は人にプロジェクトによって異なるようです. 初期状態ではこのURLにアクセスするとControllers/ValuesController.cspublic IEnumerable<string> Get()メソッドが呼ばれているようです. Controllers/ValuesController.csを以下に示します.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Test3.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

ジャンケンAPIの試作

ASP.NETのプロジェクトの初期動作を確認した後,以下のモデルとコントローラを実装し,ジャンケンAPIを試作しました.

モデルの構成

モデルはゲームのリストとゲームを操作するコマンドで構成されます. ゲームはプレイヤ2人の名前と出した手(グー,チョキまたはパー)を保持します. ゲームを操作するコマンドは以下の通りです.

  • make:
    • 入力:プレーヤー0の名前
    • 出力:ルームID
    • 新たなゲームを作成する
  • enter :
    • 入力:入室する部屋のルームID,プレーヤー1の名前
    • 出力:なし
    • 既存の部屋に入室する
  • tryPlay :
    • 入力:ルームID
    • 出力:プレイを開始できるかどうか
    • プレイできるか確認する
  • play :
    • 入力:ルームID,プレーヤー名,グー,チョキまたはパー
    • 出力:なし
    • グー,チョキまたはパーを出す
  • tryResult:
    • 入力:ルームID
    • 出力:勝敗を判定できるかどうか
    • 勝敗を判定できるか確認する
  • result :
    • 入力:ルームID
    • 出力:勝者と敗者
    • 勝敗を確認する

以上のモデルを実装したModelクラスを以下に示します.

using System;
using System.Collections.Generic;

namespace Aspdotnet.Models
{
    public enum Move
    {
        Rock,
        Paper,
        Scissors,
        None
    }

    public class Result
    {
        String Winner;
        String Loser;
        public Result(String winner, String loser)
        {
            Winner = winner;
            Loser = loser;
        }
        public bool IsTie() { return Winner == null && Loser == null; }
        public override string ToString()
        {
            return "Winner: " + Winner + ", " + "Loser: " + Loser;
        }
    }

    class Game
    {
        public String player0;
        public Move move0 = Move.None;
        public String player1;
        public Move move1 = Move.None;

        public bool IsReady()
        {
            return player0 != null && player1 != null && move0 == Move.None && move1 == Move.None;
        }

        public bool HasFinished()
        {
            return player0 != null && player1 != null && move0 != Move.None && move1 != Move.None;
        }

        public Result GetResult()
        {
            if (!HasFinished())
            {
                return null;
            }
            if (move1 == Move.Paper && move0 == Move.Scissors ||
                move1 == Move.Scissors && move0 == Move.Rock ||
                move1 == Move.Rock && move0 == Move.Paper)
            {
                return new Result(player0, player1);
            }
            if (move0 == Move.Paper && move1 == Move.Scissors ||
                move0 == Move.Scissors && move1 == Move.Rock ||
                move0 == Move.Rock && move1 == Move.Paper)
            {
                return new Result(player1, player0);
            }
            return new Result(null, null);
        }
    }

    public class Model
    {
        List<Game> games = new List<Game>();

        static Model instance = new Model();

        public static Model GetInstance() { return instance; }

        Model() { }

        public override string ToString()
        {
            string s = "";
            for (int i = 0; i < games.Count; ++i)
            {
                var g = games[i];
                s += i.ToString() + " : ";
                s += "player0 = " + g.player0 + "; ";
                s += "player1 = " + g.player1 + "; ";
                s += "move0 = " + g.move0 + "; ";
                s += "move1 = " + g.move1 + "; ";
            }
            return s;
        }
        public int Make(String player0)
        {
            if (player0 == null)
            {
                return -1;
            }
            int roomId = games.Count;
            var game = new Game();
            game.player0 = player0;
            games.Add(game);
            return roomId;
        }

        public void Enter(int roomId, String player1)
        {
            if (player1 == null)
            {
                return;
            }
            if (roomId < 0 || roomId >= games.Count)
            {
                return;
            }
            var game = games[roomId];
            game.player1 = player1;
        }

        public bool TryPlay(int roomId)
        {
            if (roomId < 0 || roomId >= games.Count)
            {
                return false;
            }
            var game = games[roomId];
            return game.IsReady();
        }

        public void Play(int roomId, String player, Move move)
        {
            if (roomId < 0 || roomId >= games.Count || move == Move.None)
            {
                return;
            }
            var game = games[roomId];
            if (game.player0 == player && game.move0 == Move.None)
            {
                game.move0 = move;
            }
            if (game.player1 == player && game.move1 == Move.None)
            {
                game.move1 = move;
            }
        }

        public bool TryResult(int roomId)
        {
            if (roomId < 0 || roomId >= games.Count)
            {
                return false;
            }
            return games[roomId].HasFinished();
        }

        public Result Result(int roomId)
        {
            if (roomId < 0 || roomId >= games.Count)
            {
                return null;
            }
            var game = games[roomId];
            if (!game.HasFinished())
            {
                return null;
            }
            return games[roomId].GetResult();
        }
    }
}
コントローラーの構成

クライアントがHTTPのGETメソッドを用いてジャンケンAPIにアクセスして,上のコマンドを実行できるようにします. コマンドへの入力はURLに含めることにしました. これを実装したJankenControllerクラスを以下に示します.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace Aspdotnet.Controllers
{
    [Route("api/[controller]")]
    public class JankenController : Controller
    {
        // GET api/janken/make/player0Name
        [HttpGet("make/{player0}")]
        public int GetMake(string player0)
        {
            var model = Models.Model.GetInstance();
            int roomId = model.Make(player0);
            Console.WriteLine(model);
            return roomId;
        }

        // GET api/janken/enter/roomId/player1Name
        [HttpGet("enter/{roomId}/{player1}")]
        public string GetEnter(int roomId, string player1)
        {
            var model = Models.Model.GetInstance();
            model.Enter(roomId, player1);
            Console.WriteLine(model);
            return "";
        }

        // GET api/janken/tryPlay/roomId/
        [HttpGet("tryPlay/{roomId}")]
        public bool GetTryPlay(int roomId)
        {
            var model = Models.Model.GetInstance();
            var isReady = model.TryPlay(roomId);
            Console.WriteLine(model);
            return isReady;
        }

        // GET api/janken/play/roomId/player0Name/Rock
        [HttpGet("play/{roomId}/{player}/{move}")]
        public string GetPlay(int roomId, string player, string move)
        {
            var model = Models.Model.GetInstance();
            Models.Move m = Models.Move.None;
            switch (move)
            {
                case "Rock":
                    m = Models.Move.Rock;
                    break;
                case "Paper":
                    m = Models.Move.Paper;
                    break;
                case "Scissors":
                    m = Models.Move.Scissors;
                    break;
                default:
                    return "";
            }
            model.Play(roomId, player, m);
            Console.WriteLine(model);
            return "";
        }

        // GET api/janken/tryResult/roomId/
        [HttpGet("tryResult/{roomId}")]
        public bool GetTryResult(int roomId)
        {
            var model = Models.Model.GetInstance();
            bool hasFinished = model.TryResult(roomId);
            Console.WriteLine(model);
            return hasFinished;
        }

        // GET api/janken/result/roomId/
        [HttpGet("result/{roomId}")]
        public string GetResult(int roomId)
        {
            var model = Models.Model.GetInstance();
            Models.Result result = model.Result(roomId);
            Console.WriteLine(model);
            return result.ToString();
        }
    }
}

試作したジャンケンAPIの動作例

最後に試作したジャンケンAPIの動作確認をしました. 動作例を以下に示します.

curl http://localhost:24635/api/janken/make/Sankasha1
# => 1
curl http://localhost:24635/api/janken/enter/0/Sankasha2
curl http://localhost:24635/api/janken/tryPlay/0
# => true
curl http://localhost:24635/api/janken/play/0/Sankasha1/Paper
curl http://localhost:24635/api/janken/play/0/Sankasha2/Rock
curl http://localhost:24635/api/janken/tryResult/0
# => true
curl http://localhost:24635/api/janken/result/0
# => Winner: Sankasha1, Loser: Sankasha2

3名の感想

参加者1

ASP.NETを通じて、HTTP通信を組み込んだソフトウェアをどう組み立てるか、どう実装した方が良いかを考える経験が出来ました

参加者2

見様見真似でとりあえず,動作するジャンケンAPIを作成することができた. その点では,やはりVisualStudioとASP.NETはすごいと思った. ただ,実際にVPSなどで動作させる場合にどうすれば良いか想像できなかった. 当たり前だが,今後ASP.NETを利用する場合には使い方をきちんと調べる必要がある.

ジャンケンAPIは,最初は単純なように思えたが,実際に試作してみると状態を管理するのは意外と面倒だった. また,入力が全てURLに含まれているため,不正なプレイが可能かもしれない.

ASP.NETお試し会を開催した結果,アドベントカレンダーを書くこともできため良かった.

参加者3

もう少し難しいと思ってたのですが,実装自体は予想以上に楽だったので驚きました.使い方次第でいろいろ面白いことが出来そうなので,自作ゲームで生かしてみたいです.

Jumpaku Othello

f:id:Jumpaku:20190917185325p:plain

https://othello.jumpaku.net/app/

はじめに

オセロWebサービス Jumpaku Othello を作成し,公開しました. 以下では,

  • Jumpaku Othelloとはどのようなサービスか?
  • なぜ作成したのか?
  • どのような言語,環境で開発したのか?

を述べた上で,関連するリンクを紹介します. 詳細なドキュメントについては,関連するリンクを参照してください.

Jumpaku Othelloとはどのようなサービスか?

Jumpaku Othelloの構成を以下の図に示します.

f:id:Jumpaku:20190917163829p:plain

このサービスはDocker上で動作するDockerコンテナであり,オセロWebアプリケーションとオセロWeb APIを提供します.

オセロWebアプリケーション

ブラウザでhttps://othello.jumpaku.net/app/にアクセスするとAIを相手にオセロの対戦をして遊ぶことができます. オセロのゲーム進行とAIに関する処理はオセロWeb APIを通してサーバ側で行われます.

オセロWeb API

Jumpaku Othelloは,オセロシステムとオセロAIによって構成されています. オセロシステムはオセロのゲーム進行を管理するモジュール,オセロAIは与えられた盤面に対する最善手を探索するモジュールです. それぞれのモジュールの機能の呼び出しはオセロWeb APIを通して行います. オセロWeb APIは,オセロシステムのAPIとオセロAIのAPIによって構成されるWeb APIです.

オセロシステムのAPI

オセロシステムのAPIには以下の3つがあります.

  • /v1/games/?action=make : 新規のゲームを作成する
  • /v1/games/?action=get : 既存のゲームの状態の確認する
  • /v1/games/?action=move : 既存のゲームの盤面に石を置く
オセロAIのAPI

オセロAIのAPIは次の1つだけです.

  • /v1/ai/move : 盤面情報を入力し,最善手を探索する

Dockerイメージ

以上のオセロWebサービスをサーバ上で動作させるためのDockerイメージ jumpaku-othello をDocker Hubで公開しました.

なぜ作ったのか?

Jumpaku Othelloを作成した理由は以下の2つです.

  • 強いオセロAIを作成したかったから
  • 自分のVPSで自作Webサービスを公開したかったから

強いオセロAIの作成

私は以前にもオセロAIの作成に挑戦したことがあります. その時はミニマックス法を理解できず,満足できるオセロAIを作成することができませんでした.

それから数年を経て,私の所属サークルでオセロAIコンテストが開催されることとなりました. 再びオセロAIを作成する機会を得ることとなったのです. 今回はオセロAIをミニマックス法に基づいて開発しました. 私はミニマックス法を理解できる程度に成長していました. 本当はオセロの対戦結果データを学習することも考えていたのですが,ミニマックス法に基づいて開発したオセロAIは既に私よりも強いAIとなっていたため,学習については見送る事としました.

自作Webサービスの公開

私はVPSの契約をして,NextcloudやGogs,Nexus Repository Managerといったサービスを動作させていました. しかし,自作のWebサービスを動作させたことがありませんでした. せっかくVPSの契約をしているのだから,そこで自作Webアプリを公開したい,という思いが常に心にありました. 弱くないオセロAIの開発に成功した私は,この機会に自作Webアプリの公開に挑戦する事としました.

どのような言語,環境で開発したのか?

オセロWebアプリケーション

言語はTypeScriptを用い,ビルドにはwebpackを利用しました. また,描画のためのライブラリとしてReactを利用しました.

オセロWeb API

言語はKotlinを用い,ビルドにはgradleを利用しました. また,WebフレームワークとしてKtorを利用しました.

関連するリンク

まとめ

  1. オセロシステムと弱くないオセロAIを作った
  2. WebアプリとWeb APIを提供するサービスとして自分のVPSで公開した
  3. ソースコードとDockerイメージも公開した

backup-with-nextcloud

概要

ファイル共有サービスの一つであるNextcloudと連携して,自分のサーバのファイルをバックアップするためのDockerイメージ backup-with-nextcloud を作成しました.

Nextcloud (https://nextcloud.com) は自分のサーバで動作させるファイル共有サービスです. 公式のDockerイメージ (https://hub.docker.com/_/nextcloud) もあります.

Docker (https://www.docker.com) はコンテナ型仮想環境のプラットフォームで,これによりサービスの動作環境の構築が行いやすくなります.

以下では

  1. backup-with-nextcloud とは何か?
  2. 何のために backup-with-nextcloud を作ったのか?
  3. backup-with-nextcloud はどんな仕組みで動くのか?

を説明します. また,具体的な動作例も示します.

backup-with-nextcloud とは何か?

backup-with-nextcloud はNextcloudと連携して自分のサーバのファイルをバックアップするためのDockerイメージで,次の機能を持ちます.

  • backup-with-nextcloud はあるサーバのバックアップしたいファイルを別のサーバのNextckoudと同期します.
  • backup-with-nextcloud では指定したスクリプトによってバックアップしたいファイルを生成することができます.
  • backup-with-nextcloud ではこれらの処理がcronによって繰り返し実行されます.

以上により,自分のサーバ上のファイルを別のNextcloudサーバと同期させることが可能となり,サービスのデータのバックアップすることができます.

backup-with-nextcloud のDockerイメージとDockerfileについてはそれぞれDocker Hub,GitHubで公開しています. 具体的な使い方は以下を参照してください.

何のために backup-with-nextcloud を作ったのか?

現在,私は契約しているVPSでいくつかのサービスを動作させており,今後これらを本格的に利用したいと考えています. ここで,これらのサービスを本格的に利用していくためにはデータをバックアップをすることが必須であると考えました.

私のVPSのサービスは全てDockerコンテナとして動作しています. また,サービスのデータはそれぞれのコンテナにマウントされたホストマシンのディレクトリに保存されています. これらのデータのバックアップは,

  1. バックアップデータ作成機能を持たないサービスについてはマウントされたホストマシンのディレクトリ内のファイル
  2. バックアップデータ作成機能を持つサービスについてははその機能を利用して作成したファイル

をNextcloudと同期することで実現できます. そこで,これらを実現するDockerイメージを作成しようと考えました.

backup-with-nextcloud はどんな仕組みで動くのか?

backup-with-nextcloudのDockerfileを以下に示します.

FROM debian:buster-slim

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install -y \
    cron \
    nextcloud-desktop-cmd \
&& rm -rf /var/lib/apt/lists/*

ENV CRON_EXP="0  *  *  *  *"
ENV CRON_USER="root"

RUN mkdir -p /backup/
RUN echo '#!/bin/bash' > /backup.sh && chmod +x /backup.sh

COPY ./sync-exclude.lst /sync-exclude.lst 
COPY ./entrypoint.sh /entrypoint.sh

CMD [ "/entrypoint.sh" ]

backup-with-nextcloudには定期的なバックアップを行うためのcronとNextcloudサーバとファイル同期を行うためのnextcloud-desktop-cmdがインストールされています. cronは,コマンドの定時実行のスケジュール管理を行うプログラムです. nextcloud-desktop-cmdはNextcloudのクライアント側でコマンドラインによる同期を行うプログラムです.

CRON_EXPはバックアップのスケジュールを設定する環境変数で,デフォルトでは1時間ごとにバックアップが実行されます. CRON_USERはバックアップを実行するユーザを設定する環境変数で,デフォルトではrootとなります. backup-with-nextcloudには他にもNextcloudサーバのURLを指定するNC_URL,Nextcloudのユーザを指定するNC_USER,Nextcloudのユーザのパスワードを指定するNC_PASSWORDという環境変数があります.

/backup/はバックアップするべきファイルを配置するためのディレクトリで,このディレクトリ内のファイルがNextcloudサーバと同期されます. 次の図に示すような構成で,サービスにマウントされたホストマシンのディレクトリを/backup/にもマウントすれば,前節の1.が実現できます.

f:id:Jumpaku:20190907044246p:plain

/backup.shはデフォルトでは何もしないシェルスクリプトですが,Nextcloudサーバとファイル同期を行う直前に実行されます. 次の図に示すような構成で,サービスのデータファイルを/backup/に複製するといったスクリプトを用意し,/backup.shとしてマウントすれば,前節の2.が実現できます.

f:id:Jumpaku:20190907201018p:plain

/entorypoint.shは以下のような/etc/crontabを作成し,cron -fを実行します.

$CRON_EXP $CRON_USER /backup.sh && nextcloudcmd --non-interactive --user $NC_USER --password $NC_PASSWORD --exclude /sync-exclude.lst /backup/ $NC_URL

動作例

Nextcloud

以下のコマンドによりnextcloudを起動します.

docker-compose -f nextcloud/docker-compose.yml up --build -d

この時のnextcloud/docker-compose.ymlを以下に示します.

version: '3'

services: 
  nextcloud:
    image: 'nextcloud:16'
    container_name: 'nextcloud'
    environment: 
      - 'SQLITE_DATABASE=nextcloud'
      - 'NEXTCLOUD_ADMIN_USER=nc_admin_user'
      - 'NEXTCLOUD_ADMIN_PASSWORD=nc_admin_password'
      - 'NEXTCLOUD_TRUSTED_DOMAINS=nextcloud'
    networks: 
      - 'nc_network'
    ports:
      - '8080:80'

networks: 
  nc_network:
    external: true

backup-with-nextcloud

以下のコマンドによりbackupserviceを起動します.

docker-compose -f service/docker-compose.yml up --build -d

この時のservice/docker-compose.ymlを以下に示します.

version: '3'

services: 
  service:
    build: './'
    volumes:
      - './data:/data/'
    networks: 
      - 'nc_network'

  backup:
    image: 'jumpaku/backup-with-nextcloud'
    volumes:
      - './data/:/backup/'
    environment: 
      - 'CRON_EXP=* * * * *'
      - 'CRON_USER=root'
      - "NC_URL=http://nextcloud/remote.php/webdav/"
      - 'NC_USER=nc_admin_user'
      - 'NC_PASSWORD=nc_admin_password'
    networks: 
      - 'nc_network'

networks: 
  nc_network:
    external: true

backupはbackup-with-nextcloudのコンテナ,serviceは毎分date >> /data/date.txtを実行するコンテナです. serviceは以下のservice/Dockerfileにより構築されます.

FROM debian:buster-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt update -y && apt-get install -y cron
RUN echo '* * * * * root date >> /data/date.txt' > /etc/crontab

CMD ["cron", "-f"]

以下のコマンドにより,serviceで生成されたファイルがnextcloudにバックアップされていることが確認できます.

cat service/data/date.txt
# => Mon Sep 16 06:07:01 UTC 2019
docker-compose -f nextcloud/docker-compose.yml exec nextcloud cat data/nc_admin_user/files/date.txt
# => Mon Sep 16 06:07:01 UTC 2019

まとめ

Nextcloudと連携して自分のサーバのファイルをバックアップするためのDockerイメージ backup-with-nextcloud を作成しました. backup-with-nextcloud はcronとnextcloud-desktop-cmdを利用してマウントされたディレクトリ内のファイルをバックアップします. backup-with-nextcloud はDocker Hub,GitHubで公開されています.

KotlinのDouble型

はじめに

本記事ではKotlinのDouble型の仕様に関して確認したことをまとめます.

等価演算子

import kotlin.Double.Companion.NaN

fun main() {
    println(NaN == NaN) // false
    println(NaN != NaN) // true
    println(NaN as Any == NaN) // true
    println(NaN as Any != NaN) // false

    println(0.0 == -0.0) // true
    println(0.0 as Any == -0.0) // false
}

Kotlinの等価演算子は両辺が静的にDouble型であるときはIEEE 754に従います. そのため,NaN == NaNfalseとなります. また,NaN != NaNtrueとなります.

しかし,NaN as Any == NaNは左辺がAny型であるため,equalsメソッドによってtrueとなります. また,NaN as Any != NaNは左辺がAny型であるため,falseとなります.

同様に,0.0 == -0.0IEEE 754に従ってtrueとなりますが,0.0 as Any == -0.0equalsメソッドによってfalseとなります.

関係演算子

import kotlin.Double.Companion.NEGATIVE_INFINITY as nInf
import kotlin.Double.Companion.POSITIVE_INFINITY as pInf
import kotlin.Double.Companion.NaN

fun main() {
    println(NaN < NaN) // false
    println(NaN > NaN) // false
    println(NaN < pInf) // false
    println(NaN > pInf) // false
    println(NaN < nInf) // false
    println(NaN > nInf) // false

    println(NaN <= NaN) // false
    println(NaN as Comparable<Double> <= NaN) // true

    println(-0.0 < 0.0) // false
    println(-0.0 as Comparable<Double> < 0.0) // true
}

Kotlinの関係演算子は両辺が静的にDouble型であるときはIEEE 754に従います. そのため,右辺または左辺がNaNであるときは,関係演算子の結果はfalseとなります. また,-0.0 < 0.0falseとなります. しかし,NaN as Comparable<Double> <= NaNは左辺がComparable<Double>型であるため,compareToメソッドを用いて判定され,結果はtrueとなります. 同様に,-0.0 as Comparable<Double> < 0.0は左辺がComparable<Double>型であるため,compareToメソッドを用いて判定され,結果はtrueとなります.

四則演算

import kotlin.Double.Companion.NEGATIVE_INFINITY as nInf
import kotlin.Double.Companion.POSITIVE_INFINITY as pInf

fun main() {

    // plus
    println(pInf + 1.0) // Infinity
    println(nInf + 1.0) // -Infinity

    println(pInf + pInf) // Infinity
    println(pInf + nInf) // NaN
    println(nInf + nInf) // -Infinity

    // times
    println(pInf * pInf) // Infinity
    println(pInf * 2.0) // Infinity
    println(pInf * 0.0) // NaN
    println(pInf * -0.0) // NaN
    println(pInf * -2.0) // -Infinity
    println(pInf * nInf) // -Infinity

    println(nInf * pInf) // -Infinity
    println(nInf * 2.0) // -Infinity
    println(nInf * 0.0) // NaN
    println(nInf * -0.0) // NaN
    println(nInf * -2.0) // Infinity
    println(nInf * nInf) // Infinity

    // minus
    println(pInf - 1.0) // Infinity
    println(nInf - 1.0) // -Infinity

    println(pInf - pInf) // NaN
    println(pInf - nInf) // Infinity
    println(nInf - pInf) // -Infinity
    println(nInf - nInf) // NaN

    // div
    println(pInf / pInf) // NaN
    println(pInf / 2.0) // Infinity
    println(pInf / 0.0) // Infinity
    println(pInf / -0.0) // -Infinity
    println(pInf / -2.0) // -Infinity
    println(pInf / nInf) // NaN

    println(pInf / pInf) // NaN
    println(2.0 / pInf) // 0.0
    println(0.0 / pInf) // 0.0
    println(-0.0 / pInf) // -0.0
    println(-2.0 / pInf) // -0.0
    println(nInf / pInf) // NaN

    println(pInf / nInf) // NaN
    println(2.0 / nInf) // -0.0
    println(0.0 / nInf) // -0.0
    println(-0.0 / nInf) // 0.0
    println(-2.0 / nInf) // 0.0
    println(nInf / nInf) // NaN
}

KotlinのDouble型の演算でオーバーフローが発生する場合は演算の結果はInfinityとなります. アンダーフローが発生する場合は演算の結果は-Infinityとなります. また,Int型の除算と異なり,Double型の除算では割る数が0.0であっても例外が発生しません.

pow

import kotlin.math.pow
import kotlin.Double.Companion.NEGATIVE_INFINITY as nInf
import kotlin.Double.Companion.POSITIVE_INFINITY as pInf
import kotlin.Double.Companion.NaN

fun main() {
    // a^0.0 == 1.0
    println(0.0.pow(0.0)) // 1.0
    println(NaN.pow(0.0)) // 1.0
    println(pInf.pow(0.0)) // 1.0
    println(nInf.pow(0.0)) // 1.0

    // a^1.0 == a
    println(0.0.pow(1.0)) // 0.0
    println(NaN.pow(1.0)) // NaN
    println(pInf.pow(1.0)) // Infinity
    println(nInf.pow(1.0)) // -Infinity
    
    // a^Infinity
    println(pInf.pow(pInf)) // Infinity
    println(2.0.pow(pInf)) // Infinity
    println(1.0.pow(pInf)) // NaN
    println(0.5.pow(pInf)) // 0.0
    println(0.0.pow(pInf)) // 0.0
    println((-0.0).pow(pInf)) // 0.0
    println((-0.5).pow(pInf)) // 0.0
    println((-1.0).pow(pInf)) // NaN
    println((-2.0).pow(pInf)) // Infinity
    println(nInf.pow(pInf)) // Infinity

    // a^-Infinity
    println(pInf.pow(nInf)) // 0.0
    println(2.0.pow(nInf)) // 0.0
    println(1.0.pow(nInf)) // NaN
    println(0.5.pow(nInf)) // Infinity
    println(0.0.pow(nInf)) // Infinity
    println((-0.0).pow(nInf)) // Infinity
    println((-0.5).pow(nInf)) // Infinity
    println((-1.0).pow(nInf)) // NaN
    println((-2.0).pow(nInf)) // 0.0
    println(nInf.pow(nInf)) // 0.0

    // a^b (a < 0.0)
    println((-2.0).pow(2.0)) // 4.0
    println((-2.0).pow(2.5)) // NaN
    println((-2.0).pow(3.0)) // -8.0
}

NaN.pow(0.0)NaNではなく1.01.0.pow(pInf)および1.0.pow(nInf)1.0ではなくNaN(-2.0).pow(pInf)nInf.pow(pInf)および(-0.5).pow(nInf)NaNではなくInfinitypInf.pow(nInf)NaNではなく0.0となりました. これらはJavaMath.powの規定に合致するものですが私の直感に反するものだったため気をつけなければいけないと感じました.

log

import kotlin.math.log
import kotlin.Double.Companion.NEGATIVE_INFINITY as nInf
import kotlin.Double.Companion.POSITIVE_INFINITY as pInf

fun main() {
    println(log(pInf, pInf)) // NaN
    println(log(2.0, pInf)) // 0.0
    println(log(1.0, pInf)) // 0.0
    println(log(0.5, pInf)) // -0.0
    println(log(0.0, pInf)) // NaN
    println(log(-0.0, pInf)) // NaN

    println(log(pInf, 2.0)) // Infinity
    println(log(2.0, 2.0)) // 1.0
    println(log(1.0, 2.0)) // 0.0
    println(log(0.5, 2.0)) // -1.0
    println(log(0.0, 2.0)) // -Infinity
    println(log(-0.0, 2.0)) // -Infinity

    println(log(pInf, 0.5)) // -Infinity
    println(log(2.0, 0.5)) // -1.0
    println(log(1.0, 0.5)) // -0.0
    println(log(0.5, 0.5)) // 1.0
    println(log(0.0, 0.5)) // Infinity
    println(log(-0.0, 0.5)) // Infinity
}

Kotlinのlogに関して,特に気になる結果はありませんでした.

リファレンス

学年割り電卓2のリリース

学年割り電卓2(スマホ用Webアプリ)をすぐに使ってみる --> https://jumpaku.github.io/GakunenWari/

はじめに

日本には「先輩は後輩よりも偉い」という意味不明の謎の文化があるらしい. 実際,私も大学生活において次のような場面に遭遇したことがある.

  1. サークル内でお喋りをしていたとき,後輩が先輩に対して敬語を使わなかったため空気が張り詰めた.
  2. サークルで食事をして割り勘をするときに,先輩が後輩よりも多く支払った.

ここで,2. のように先輩の支払額が後輩のものよりも多くなるように割り勘することを学年割りという. 学年割りは学年が上がるに従って支払額も増加させる必要があるが,暗算だけで「丁度良い」増加量を設定するのは困難である. そこで,これまでに学年割り電卓 GakunenWari Calculator - Jumpaku’s blog[1],学年割電卓に新機能 GakunenWari Calculator updated - Jumpaku’s blog[2]が開発され,学年割りを行うスマホ用Webアプリによって,暗算に頼らない学年割りが実現された. しかし,これらのスマホ用Webアプリでは「丁度良い」増加量を見つけるために,学年間の支払金額の差をテキストフィールドに入力し,計算ボタンを押す,という作業を繰り返しながら試行錯誤する必要があり,手間がかかった. また,端数の処理していなかったため,学年割り計算後の集金にも手間がかかった.

以下では,端数処理を含めた学年割り計算法を示した上で,これを[1], [2]に組み込み,新たに「学年割り電卓2」を開発する.

端数処理を含めた学年割り計算法

学年割りは学年\(i \ (i \in I, I = \{0, \ldots, 6\})\)が上がるに従ってその学年の参加者一人当たりの支払額\(x_i\)が増加するように\(x_i\)を計算するものである. ただし,学年\(i\)は以下のように定める.

学部1年 学部2年 学部3年 学部4年 修士1年 修士2年 博士以上
\(0\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\)

ここでは\(x_i\)を単調増加する等差数列として求めることとする. また,切捨て端数の上限を\(u\)を設定し,\(x_i\)に次の制約を与えることとする.

$$ \forall i \in I, \ x_i は u で割り切れる. $$

この端数処理より,\(x_i\)が切りの良い値となり,集金の際の手間が軽減される.

合計支払金額を\(T\),学年が\(i\)である参加者の人数を\(c_i\)とし, 参加者からそれぞれ\(x_i\)だけ集金しそれを合計した合計集金額を \( T' = \sum_{i \in I} x_i c_i \) とするとき, 端数処理によって必ずしも \(T = T'\) とはならない. ここでは\(T'\)が\(T\)を超えない限り最大になるように\(x_i\)を求めて,不足額\(r = T - T'\)とともに出力するものとする.

\(x_i\)は初項\(x_0\),公差\(d\)を用いて

$$ x_i = x_0 + id $$

と表される. \(x_i\)は\(u\)の倍数となるため,\(x_0 = a u\),\(d = b u\)を満たすパラメータ\(a \in \mathrm{Z}\),\(b \in \mathrm{Z}\)が存在し,\(x_i\)は

$$ x_i = u(a + ib) $$

と表すことができる. 従って\(T'\)は\(n = \sum_{i \in I} c_i\),\(C = \sum_{i \in I} i c_i\)を用いて

$$ T' = u a n + u b C $$

と表すことができる. ここで,\(b\)は学年毎の金額差を調節するパラメータであり,ユーザはこれを変更することで丁度良い増加量を見つけることができる. また,\(a\)は初項を決定する未知のパラメータである.

\(r = T - T'\)より

$$ a = \frac{T - r - u b C}{u n} $$

と変形すると\(a\)が整数であり,\(T'\)は\(T\)を超えない限り最大となるため,

$$ r = (T - u b C) \% (u n) $$

と\(r\)が得られ,\(a\)が定まる.

以上より,

  • 合計支払金額\(T\),
  • 端数の上限\(u > 0\),
  • 学年毎の金額差を調節するパラメータ\(b \geq 0\),
  • 学年が\(i\)である参加者の人数\(c_i\)

を入力とし,

  • 学年が\(i\)である参加者の一人当たりの支払額\(x_i\),
  • 不足額\(r\)

を出力とする,端数処理を含めた学年割り計算法が実現される.

学年割り電卓2

上の端数処理を含めた学年割り計算法を用いてスマホ用Webアプリ「学年割り電卓2」を開発した. 学年割り電卓2はHTML,TypeScript,CSSを用いて開発され,現在GitHub Pages上で公開されている(https://jumpaku.github.io/GakunenWari/).

学年割り電卓2のUIを以下に示す.

f:id:Jumpaku:20181224182122j:plain

まず,合計支払金額に\(T\)の値,切捨端数に\(u\)の値,それぞれの学年の人数に\(c_i\)の値を入力し, 次に,下部のスライダーを動かして\(b\)の値を設定すると, 支払額に\(x_i\)の値,不足額に\(r\)の値が表示される. ここで,\(b\)の値を設定するスライダーの変化に応じて,リアルタイムに支払額の表示が変化するため, 丁度良い増加量をすぐに見つけることができる.

また,表示される支払額は設定した端数の上限で割り切れることが保証されているため,集金時に細かい小銭をやり取りする手間もかからない.

学年割り電卓2のソースコードGitHub - Jumpaku/GakunenWari: 学年割り電卓で公開されている.

まとめ

  • 学年が上がるに従って支払額も増加する割り勘,すなわち,学年割りが行われる機会がある.
  • 学年割りを暗算無しに行うためのスマホ用Webアプリ「学年割り電卓2(https://jumpaku.github.io/GakunenWari/)」を開発した.
  • 学年割り電卓2は端数処理を行うため,それぞれ学年の参加者の一人当たりの支払額が切りの良い値となる.
  • 学年間の金額差を設定するスライダーがあり,学年間の金額差を設定しやすい.

謝辞

本記事はMuroran Institute of Technology Advent Calendar 2018 - Adventarの記事です.